Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/feature/ciba' into feature/ciba
Browse files Browse the repository at this point in the history
# Conflicts:
#	Abblix.Oidc.Server/Model/ConfigurationResponse.cs
  • Loading branch information
kirill-abblix committed Sep 30, 2024
2 parents 59fd6f8 + 358ac72 commit 45205cd
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 52 deletions.
2 changes: 1 addition & 1 deletion Abblix.Oidc.Server.Mvc/Controllers/DiscoveryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public Task<ActionResult<ConfigurationResponse>> ConfigurationAsync(
BackChannelAuthenticationEndpoint = Resolve(Path.BackChannelAuthentication, OidcEndpoints.BackChannelAuthentication),
BackChannelTokenDeliveryModesSupported = options.Value.BackChannelAuthentication.TokenDeliveryModesSupported,
BackChannelUserCodeParameterSupported = options.Value.BackChannelAuthentication.UserCodeParameterSupported,
BackChannelAuthenticationRequestSigningAlgValuesSupported = jwtCreator.SigningAlgValuesSupported,
BackChannelAuthenticationRequestSigningAlgValuesSupported = jwtValidator.SigningAlgValuesSupported,
};

return Task.FromResult<ActionResult<ConfigurationResponse>>(response);
Expand Down
16 changes: 15 additions & 1 deletion Abblix.Oidc.Server.Mvc/Model/ClientRegistrationRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -334,21 +334,35 @@ public record ClientRegistrationRequest
[ElementsRequired]
public Uri[] PostLogoutRedirectUris { get; set; } = Array.Empty<Uri>();

/// <summary>
/// The backchannel token delivery mode to be used by this client. This determines how tokens are delivered
/// during backchannel authentication.
/// </summary>
[JsonPropertyName(Parameters.BackChannelTokenDeliveryMode)]
[AllowedValues(
BackchannelTokenDeliveryModes.Ping,
BackchannelTokenDeliveryModes.Poll,
BackchannelTokenDeliveryModes.Push)]
public string? BackChannelTokenDeliveryMode { get; set; }

/// <summary>
/// The endpoint where backchannel client notifications are sent for this client.
/// </summary>
[JsonPropertyName(Parameters.BackChannelClientNotificationEndpoint)]
[AbsoluteUri]
public Uri? BackChannelClientNotificationEndpoint { get; set; }

/// <summary>
/// The signing algorithm used for backchannel authentication requests sent to this client.
/// </summary>
[JsonPropertyName(Parameters.BackChannelAuthenticationRequestSigningAlg)]
public string? BackChannelAuthenticationRequestSigningAlg { get; set; }

/// <summary>
/// Indicates whether the backchannel authentication process supports user codes for this client.
/// </summary>
[JsonPropertyName(Parameters.BackChannelUserCodeParameter)]
public bool? BackChannelUserCodeParameter { get; set; }
public bool BackChannelUserCodeParameter { get; set; } = false;

/// <summary>
/// Maps the properties of this client registration request to a <see cref="Core.ClientRegistrationRequest"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,66 @@ public FlowTypeValidator(ILogger<FlowTypeValidator> logger)
protected override AuthorizationRequestValidationError? Validate(AuthorizationValidationContext context)
{
var responseType = context.Request.ResponseType;
if (!TryDetectFlowType(responseType, out var flowType, out var responseMode))
{
_logger.LogWarning("The response type {@ResponseType} is not valid", new object?[] { responseType });

context.ResponseMode = context.Request.ResponseMode ?? ResponseModes.Query;
if (!ResponseTypeAllowed(context))
{
_logger.LogWarning("The response type {@ResponseType} is not allowed for the client", new object?[] { responseType });
return UnsupportedResponseType("The response type is not allowed for the client");
}

return context.Error(
ErrorCodes.UnsupportedResponseType,
"The response type is not supported");
if (!TryDetectFlowType(responseType, out var flowType, out var responseMode))
{
_logger.LogWarning("The response type {@ResponseType} is not valid", new object?[] { responseType });
return UnsupportedResponseType("The response type is not supported");
}

context.FlowType = flowType;
context.ResponseMode = responseMode;
return null;

AuthorizationRequestValidationError UnsupportedResponseType(string message)
{
context.ResponseMode = context.Request.ResponseMode ?? ResponseModes.Query;

return context.Error(
ErrorCodes.UnsupportedResponseType,
message);
}
}

/// <summary>
/// Validates whether the requested response type in an authorization request matches any of the allowed
/// response types registered for the client.
/// This ensures that the client is using a valid and permitted OAuth/OpenID Connect flow.
/// </summary>
/// <param name="context">The authorization validation context containing the client and request details.</param>
/// <returns>
/// A boolean indicating whether the requested response type is allowed for the client.
/// </returns>
private static bool ResponseTypeAllowed(AuthorizationValidationContext context)
{
var responseType = context.Request.ResponseType;

// If the response type is not specified, it means the request is invalid
if (responseType == null)
return false;

// Define the string comparer commonly used for comparison
var responseTypeComparer = StringComparer.Ordinal;

// Sort the requested response type array to ensure order-independent comparison
Array.Sort(responseType, responseTypeComparer);

// Check if any of the allowed response types exactly match the pre-sorted requested response type
return context.ClientInfo.AllowedResponseTypes.Any(
allowedResponseType =>
{
// Sort the allowed response type array to ensure consistent comparison regardless of the order
Array.Sort(allowedResponseType, responseTypeComparer);

// Use sequence comparison to check if the allowed response type matches the requested one
return allowedResponseType.SequenceEqual(responseType, responseTypeComparer);
});
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,62 @@

namespace Abblix.Oidc.Server.Endpoints.BackChannelAuthentication.Validation;

public class UserCodeValidator: IBackChannelAuthenticationContextValidator
/// <summary>
/// Validates the presence of a UserCode in backchannel authentication requests, based on the client
/// and provider configuration. This validator ensures that if the client or provider requires the
/// UserCode parameter for backchannel authentication, it is included in the request.
/// </summary>
public class UserCodeValidator : IBackChannelAuthenticationContextValidator
{
/// <summary>
/// Initializes a new instance of the <see cref="UserCodeValidator"/> class.
/// The constructor accepts OIDC options, allowing the validator to access the configuration
/// settings that determine if the UserCode parameter is required.
/// </summary>
/// <param name="options">
/// The OIDC options used to configure the behavior of the backchannel authentication process.</param>
public UserCodeValidator(IOptions<OidcOptions> options)
{
_options = options;
}

private readonly IOptions<OidcOptions> _options;

/// <summary>
/// Asynchronously validates the UserCode parameter in the context of a backchannel authentication request.
/// If the UserCode is required but not present, the method returns an error. Otherwise, it returns null.
/// </summary>
/// <param name="context">
/// The validation context containing the authentication request and client information.</param>
/// <returns>
/// A task that represents the asynchronous operation, returning an error if validation fails,
/// or null if successful.</returns>
public Task<BackChannelAuthenticationValidationError?> ValidateAsync(BackChannelAuthenticationValidationContext context)
=> Task.FromResult(Validate(context));

/// <summary>
/// Performs the actual validation of the UserCode parameter. Checks whether the provider and client require
/// the UserCode parameter for the current request and ensures that it is present in the request.
/// </summary>
/// <param name="context">The validation context containing the backchannel authentication request details.</param>
/// <returns>
/// A <see cref="BackChannelAuthenticationValidationError"/> if the UserCode is missing when required,
/// or null otherwise.</returns>
private BackChannelAuthenticationValidationError? Validate(BackChannelAuthenticationValidationContext context)
{
// Check if the provider and client both require the UserCode parameter.
var requireUserCode = _options.Value.BackChannelAuthentication.UserCodeParameterSupported &&
context.ClientInfo.BackChannelUserCodeParameter;
context.ClientInfo.BackChannelUserCodeParameter;

// Return an error if UserCode is required but missing from the request.
if (requireUserCode && string.IsNullOrEmpty(context.Request.UserCode))
{
return new BackChannelAuthenticationValidationError(
ErrorCodes.MissingUserCode,
"The UserCode parameter is missing.");
}

// If no errors, return null (indicating a successful validation).
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
using Abblix.Oidc.Server.Common.Exceptions;
using Abblix.Oidc.Server.Endpoints.BackChannelAuthentication.Interfaces;
using Abblix.Oidc.Server.Features.Tokens.Validation;
using Abblix.Oidc.Server.Model;
using Abblix.Utils;

namespace Abblix.Oidc.Server.Endpoints.BackChannelAuthentication.Validation;
Expand All @@ -48,70 +49,72 @@ public UserIdentityValidator(
private readonly IClientJwtValidator _clientJwtValidator;

/// <summary>
/// Validates the user's identity based on the available identity hints (e.g., login hint, login hint token, ID token hint).
/// It ensures that only one hint is present, and processes the provided hint to establish the user's identity.
/// Validates the user's identity based on the provided identity hints, such as login hint, login hint token,
/// or ID token hint. It ensures that only one identity hint is present and attempts to process the hint
/// to confirm the user's identity.
/// </summary>
/// <param name="context">The validation context containing the backchannel authentication request.</param>
/// <param name="context">Contains the backchannel authentication request and client information.</param>
/// <returns>
/// A <see cref="BackChannelAuthenticationValidationError"/> if the identity validation fails,
/// Returns a <see cref="BackChannelAuthenticationValidationError"/> if the identity validation fails,
/// or null if the identity is successfully validated.
/// </returns>
public async Task<BackChannelAuthenticationValidationError?> ValidateAsync(
BackChannelAuthenticationValidationContext context)
{
var request = context.Request;

// Count the number of identity hints provided (LoginHint, LoginHintToken, IdTokenHint)
// Count the number of identity hints (LoginHint, LoginHintToken, IdTokenHint) provided in the request
var userIdentityCount = new[]
{
request.LoginHint,
request.LoginHintToken,
request.IdTokenHint,
request.LoginHint, // Regular login hint
request.LoginHintToken, // JWT-based login hint token
request.IdTokenHint // ID token hint provided by the client
}
.Count(id => id.HasValue());

// Ensure exactly one identity hint is provided
switch (userIdentityCount)
{
case 1:
break; // Valid scenario
break; // Valid scenario: exactly one hint is provided

case 0:
// No identity hint is present; return an error indicating the user's identity is unknown
return new BackChannelAuthenticationValidationError(
ErrorCodes.InvalidRequest, "The user's identity is unknown.");

default:
// Multiple identity hints provided; return an error indicating ambiguity
return new BackChannelAuthenticationValidationError(
ErrorCodes.InvalidRequest,
"User identity is not determined due to conflicting hints.");
}

// Validate the LoginHintToken if present and configured to parse as JWT
// Validate the LoginHintToken if it is provided and the client is configured to parse it as a JWT
if (request.LoginHintToken.HasValue() && context.ClientInfo.ParseLoginHintTokenAsJwt)
{
var (loginHintTokenResult, clientInfo) = await _clientJwtValidator.ValidateAsync(request.LoginHintToken);
switch (loginHintTokenResult, clientInfo)
{
// Successful validation and the client matches
case (ValidJsonWebToken { Token: var loginHintToken }, { ClientId: var clientId})
when clientId == context.ClientInfo.ClientId:

context.LoginHintToken = loginHintToken;
break;

// The token was issued for another client
case (ValidJsonWebToken, not null):
case (ValidJsonWebToken, { ClientId: var clientId})
when clientId != context.ClientInfo.ClientId:

return new BackChannelAuthenticationValidationError(
ErrorCodes.InvalidRequest,
"LoginHintToken issued by another client.");

// JWT validation failed
// If the token is valid and issued for the correct client, store it in the validation context
case (ValidJsonWebToken { Token: var loginHintToken }, _):
context.LoginHintToken = loginHintToken;
break;

// If JWT validation fails, return an error
case (JwtValidationError, _):
return new BackChannelAuthenticationValidationError(
ErrorCodes.InvalidRequest,
"LoginHintToken validation failed.");

// Unexpected cases should result in an exception
default:
throw new InvalidOperationException("Something went wrong.");
}
Expand All @@ -123,10 +126,12 @@ public UserIdentityValidator(
var idTokenResult = await ValidateIdTokenHint(context, request.IdTokenHint);
switch (idTokenResult)
{
// If successful, store the validated token in the context
case Result<JsonWebToken>.Success(var idToken):
context.IdToken = idToken;
break;

// If validation fails, return the error with the appropriate message
case Result<JsonWebToken>.Error(var error, var description):
return new BackChannelAuthenticationValidationError(error, description);
}
Expand All @@ -148,32 +153,35 @@ private async Task<Result<JsonWebToken>> ValidateIdTokenHint(
BackChannelAuthenticationValidationContext context,
string idTokenHint)
{
// Validate the ID token hint but skip lifetime validation
// Validate the ID token hint, ensuring that it is a well-formed token except validation of its lifetime.
var result = await _idTokenValidator.ValidateAsync(
idTokenHint,
ValidationOptions.Default & ~ValidationOptions.ValidateLifetime);

// Check if the token was issued for the correct client
// Analyze the validation result, checking if the token was issued for the correct client
switch (result)
{
// If the token's audience doesn't match the client specified in the validation context, return an error.
case ValidJsonWebToken { Token.Payload.Audiences: var audiences }
when !audiences.Contains(context.ClientInfo.ClientId, StringComparer.Ordinal):

return InvalidRequest(
return new ErrorResponse(
ErrorCodes.InvalidRequest,
"The id token hint contains token issued for the client other than specified");

// If the token validation resulted in an error, return an invalid request error response.
case JwtValidationError:
return InvalidRequest("The id token hint contains invalid token");
return new ErrorResponse(
ErrorCodes.InvalidRequest,
"The id token hint contains invalid token");

// If the token is valid, return it as the successful result.
case ValidJsonWebToken { Token: var idToken }:
return idToken;

// If none of the above cases match, an unexpected result occurred, so throw an exception.
default:
throw new UnexpectedTypeException(nameof(result), result.GetType());
}

// Helper method to generate an error response for invalid requests
Result<JsonWebToken>.Error InvalidRequest(string description)
=> new (ErrorCodes.InvalidRequest, description);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ private ClientInfo ToClientInfo(
SubjectType = model.SubjectType,
SectorIdentifier = sectorIdentifier,
PostLogoutRedirectUris = model.PostLogoutRedirectUris,
BackChannelTokenDeliveryMode = model.BackChannelTokenDeliveryMode,
BackChannelClientNotificationEndpoint = model.BackChannelClientNotificationEndpoint,
BackChannelAuthenticationRequestSigningAlg = model.BackChannelAuthenticationRequestSigningAlg,
BackChannelUserCodeParameter = model.BackChannelUserCodeParameter,
};

if (model.UserInfoSignedResponseAlg.HasValue())
Expand Down
Loading

0 comments on commit 45205cd

Please sign in to comment.