Skip to content

Commit

Permalink
Updated IUserDeviceAuthenticationHandler return type
Browse files Browse the repository at this point in the history
  • Loading branch information
kirill-abblix committed Oct 2, 2024
1 parent bebd1b8 commit 9ad814c
Show file tree
Hide file tree
Showing 16 changed files with 278 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
// CONTACT: For license inquiries or permissions, contact Abblix LLP at
// info@abblix.com

using Abblix.Oidc.Server.Common.Constants;
using Abblix.Oidc.Server.Common.Exceptions;
using Abblix.Oidc.Server.Model;
using Abblix.Oidc.Server.Mvc.Formatters.Interfaces;
Expand Down Expand Up @@ -58,23 +57,22 @@ public Task<ActionResult> FormatResponseAsync(
{
return Task.FromResult<ActionResult>(response switch
{
// If the authentication was successful, return a 200 OK response with the success details.
// If the authentication was successful, return a 200 OK response with the success details
BackChannelAuthenticationSuccess success => new OkObjectResult(success),

// If the error indicates an invalid client, return a 401 Unauthorized response.
BackChannelAuthenticationError { Error: ErrorCodes.InvalidClient, ErrorDescription: var description }
=> new UnauthorizedObjectResult(new ErrorResponse(ErrorCodes.InvalidClient, description)),
// If the error indicates an invalid client, return a 401 Unauthorized response
BackChannelAuthenticationUnauthorized { Error: var error, ErrorDescription: var description }
=> new UnauthorizedObjectResult(new ErrorResponse(error, description)),

// If access was denied, return a 403 Forbidden response.
BackChannelAuthenticationError { Error: ErrorCodes.AccessDenied, ErrorDescription: var description }
=> new ObjectResult(new ErrorResponse(ErrorCodes.InvalidClient, description))
{ StatusCode = StatusCodes.Status403Forbidden },
// If access was denied, return a 403 Forbidden response
BackChannelAuthenticationForbidden { Error: var error, ErrorDescription: var description }
=> new ObjectResult(new ErrorResponse(error, description)) { StatusCode = StatusCodes.Status403Forbidden },

// For any other type of error, return a 400 Bad Request response.
// For any other type of error, return a 400 Bad Request response
BackChannelAuthenticationError { Error: var error, ErrorDescription: var description }
=> new BadRequestObjectResult(new ErrorResponse(error, description)),

// If the response type is unexpected, throw an exception for further debugging.
// If the response type is unexpected, throw an exception for further debugging
_ => throw new UnexpectedTypeException(nameof(response), response.GetType()),
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
// CONTACT: For license inquiries or permissions, contact Abblix LLP at
// info@abblix.com

using Abblix.Oidc.Server.Model;
using Abblix.Oidc.Server.Mvc.Binders;
using Microsoft.AspNetCore.Mvc;
using Core = Abblix.Oidc.Server.Model;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@

using Abblix.Oidc.Server.Common;
using Abblix.Oidc.Server.Common.Configuration;
using Abblix.Oidc.Server.Common.Constants;
using Abblix.Oidc.Server.Common.Exceptions;
using Abblix.Oidc.Server.Endpoints.BackChannelAuthentication.Interfaces;
using Abblix.Oidc.Server.Endpoints.Token.Interfaces;
using Abblix.Oidc.Server.Features.BackChannelAuthentication;
using Abblix.Oidc.Server.Features.BackChannelAuthentication.Interfaces;
using Abblix.Oidc.Server.Features.Licensing;
using Abblix.Oidc.Server.Features.UserAuthentication;
using Abblix.Oidc.Server.Model;
using Microsoft.Extensions.Options;
using BackChannelAuthenticationRequest = Abblix.Oidc.Server.Features.BackChannelAuthentication.BackChannelAuthenticationRequest;
Expand Down Expand Up @@ -53,19 +57,23 @@ public class BackChannelAuthenticationRequestProcessor : IBackChannelAuthenticat
/// <param name="options">Configuration options related to backchannel authentication.</param>
/// <param name="userDeviceAuthenticationHandler">Handler for initiating authentication on the user's device.
/// </param>
/// <param name="timeProvider"></param>
public BackChannelAuthenticationRequestProcessor(
IBackChannelAuthenticationStorage storage,
IOptionsSnapshot<OidcOptions> options,
IUserDeviceAuthenticationHandler userDeviceAuthenticationHandler)
IUserDeviceAuthenticationHandler userDeviceAuthenticationHandler,
TimeProvider timeProvider)
{
_storage = storage;
_options = options;
_userDeviceAuthenticationHandler = userDeviceAuthenticationHandler;
_timeProvider = timeProvider;
}

private readonly IBackChannelAuthenticationStorage _storage;
private readonly IOptionsSnapshot<OidcOptions> _options;
private readonly IUserDeviceAuthenticationHandler _userDeviceAuthenticationHandler;
private readonly TimeProvider _timeProvider;

/// <inheritdoc />
/// <summary>
Expand All @@ -83,29 +91,57 @@ public async Task<BackChannelAuthenticationResponse> ProcessAsync(ValidBackChann
// Validate the client's license or eligibility for making the backchannel authentication request.
request.ClientInfo.CheckClientLicense();

// Initiate the authentication flow on the user's device and retrieve the associated session.
var authSession = await _userDeviceAuthenticationHandler.InitiateAuthenticationAsync(request);
AuthorizedGrant authorizedGrant;

// Construct an authorization context that encapsulates client, scope, and resource information.
var authContext = new AuthorizationContext(
request.ClientInfo.ClientId,
request.Scope,
request.Resources,
request.Model.Claims);
// Initiate the authentication flow on the user's device to retrieve the associated session
var authResult = await _userDeviceAuthenticationHandler.InitiateAuthenticationAsync(request);
switch (authResult)
{
case Result<AuthSession>.Success(var authSession):
// Create the authorization context with details from the request
var authContext = new AuthorizationContext(
request.ClientInfo.ClientId,
request.Scope,
request.Resources,
request.Model.Claims);

authorizedGrant = new AuthorizedGrant(authSession, authContext);
break;

// Client authentication failed (e.g., invalid client credentials, unknown client,
// no client authentication included, or unsupported authentication method)
case Result<AuthSession>.Error(ErrorCodes.UnauthorizedClient, var description):
return new BackChannelAuthenticationUnauthorized(ErrorCodes.AccessDenied, description);

// The resource owner or OpenID Provider denied the request
case Result<AuthSession>.Error(ErrorCodes.AccessDenied, var description):
return new BackChannelAuthenticationForbidden(ErrorCodes.AccessDenied, description);

// Return a generic error response for other issues
case Result<AuthSession>.Error(var error, var description):
return new BackChannelAuthenticationError(error, description);

// Treat any unexpected results as exceptions
default:
throw new UnexpectedTypeException(nameof(authResult), authResult.GetType());
}

var pollingInterval = _options.Value.BackChannelAuthentication.PollingInterval;

// Persist the backchannel authentication request with an initial pending status and
// the associated grant details.
// Persist the backchannel authentication request with an initial pending status
var authenticationRequestId = await _storage.StoreAsync(
new BackChannelAuthenticationRequest(new AuthorizedGrant(authSession, authContext)),
new BackChannelAuthenticationRequest(authorizedGrant)
{
Status = BackChannelAuthenticationStatus.Pending,
NextPollAt = _timeProvider.GetUtcNow() + pollingInterval,
},
request.ExpiresIn);

// Return a success response including the generated authentication request ID,
// the request expiration time, and the polling interval for checking the status.
return new BackChannelAuthenticationSuccess
{
AuthenticationRequestId = authenticationRequestId,
ExpiresIn = request.ExpiresIn,
Interval = _options.Value.BackChannelAuthentication.PollingInterval,
Interval = pollingInterval,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public ClientValidator(IClientAuthenticator clientAuthenticator)
if (!clientInfo.AllowedGrantTypes.Contains(GrantTypes.Ciba))
{
return new BackChannelAuthenticationValidationError(
ErrorCodes.UnauthorizedClient, "The client does not allow the given grant type");
ErrorCodes.UnauthorizedClient, "The Client is not authorized to use this authentication flow");
}

context.ClientInfo = clientInfo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ public UserIdentityValidator(
context.LoginHintToken = loginHintToken;
break;

// If JWT validation fails, return an error
case (JwtValidationError { Error: JwtError.InvalidToken }, _):
break;

// If JWT validation fails, return an error
case (JwtValidationError, _):
return new BackChannelAuthenticationValidationError(
ErrorCodes.InvalidRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ namespace Abblix.Oidc.Server.Endpoints.Token.Grants;
/// <summary>
/// Handles the authorization process for backchannel authentication requests under the Client-Initiated Backchannel
/// Authentication (CIBA) grant type.
/// This handler is responsible for validating the token request based on the backchannel authentication flow, ensuring
/// This handler validates the token request based on the backchannel authentication flow, ensuring
/// that the client is authorized and that the user has been authenticated before tokens are issued.
/// </summary>
public class BackChannelAuthenticationGrantHandler : IAuthorizationGrantHandler
Expand All @@ -44,16 +44,20 @@ public class BackChannelAuthenticationGrantHandler : IAuthorizationGrantHandler
/// </summary>
/// <param name="storage">Service for storing and retrieving backchannel authentication requests.</param>
/// <param name="parameterValidator">The service to validate request parameters.</param>
/// <param name="timeProvider">Provides access to the current time.</param>
public BackChannelAuthenticationGrantHandler(
IBackChannelAuthenticationStorage storage,
IParameterValidator parameterValidator)
IParameterValidator parameterValidator,
TimeProvider timeProvider)
{
_storage = storage;
_parameterValidator = parameterValidator;
_timeProvider = timeProvider;
}

private readonly IBackChannelAuthenticationStorage _storage;
private readonly IParameterValidator _parameterValidator;
private readonly TimeProvider _timeProvider;

/// <summary>
/// Specifies the grant types supported by this handler, specifically the "CIBA" (Client-Initiated Backchannel
Expand Down Expand Up @@ -82,44 +86,58 @@ public IEnumerable<string> GrantTypesSupported
/// </returns>
public async Task<GrantAuthorizationResult> AuthorizeAsync(TokenRequest request, ClientInfo clientInfo)
{
// Check if the request contains a valid authentication request ID.
// Check if the request contains a valid authentication request ID
_parameterValidator.Required(request.AuthenticationRequestId, nameof(request.AuthenticationRequestId));

// Try to retrieve the corresponding backchannel authentication request from storage.
// Try to retrieve the corresponding backchannel authentication request from storage
var authenticationRequest = await _storage.TryGetAsync(request.AuthenticationRequestId);

// Determine the outcome of the authorization based on the state of the backchannel authentication request.
return authenticationRequest switch
// Determine the outcome of the authorization based on the state of the backchannel authentication request
switch (authenticationRequest)
{
// If the request is not found or has expired, return an error indicating token expiration.
null => new InvalidGrantResult(
ErrorCodes.ExpiredToken,
"The authentication request has expired."),
// If the user has been authenticated, remove the request and return the authorized grant
case { Status: BackChannelAuthenticationStatus.Authenticated, AuthorizedGrant: { } authorizedGrant }:
await _storage.RemoveAsync(request.AuthenticationRequestId);
return authorizedGrant;

// If the client making the request is not the same as the one that initiated the authentication,
// return an unauthorized error.
{ AuthorizedGrant.Context.ClientId: var clientId } when clientId != clientInfo.ClientId
=> new InvalidGrantResult(
// If the request is not found or has expired, return an error indicating token expiration
case null:
return new InvalidGrantResult(
ErrorCodes.ExpiredToken,
"The authentication request has expired");

// If the client making the request is not the same as the one that initiated the authentication
case { AuthorizedGrant.Context.ClientId: var clientId } when clientId != clientInfo.ClientId:
return new InvalidGrantResult(
ErrorCodes.UnauthorizedClient,
"The authentication request was started by another client."),
"The authentication request was started by another client");

// If the user has not yet been authenticated and the request is still pending,
// return an error indicating that authorization is pending.
{ Status: BackChannelAuthenticationStatus.Pending } => new InvalidGrantResult(
ErrorCodes.AuthorizationPending,
"The authorization request is still pending as the user hasn't been authenticated."),
// If the request is still pending and not yet time to poll again
case { Status: BackChannelAuthenticationStatus.Pending, NextPollAt: {} nextPollAt }
when _timeProvider.GetUtcNow() < nextPollAt:

// If the user denied the authentication request, return an error indicating access is denied.
{ Status: BackChannelAuthenticationStatus.Denied } => new InvalidGrantResult(
ErrorCodes.AccessDenied,
"The authorization request is denied by the user."),
return new InvalidGrantResult(
ErrorCodes.SlowDown,
"The authorization request is still pending as the user hasn't been authenticated");

// If the user has not yet been authenticated and the request is still pending,
// return an error indicating that authorization is pending
case { Status: BackChannelAuthenticationStatus.Pending }:
return new InvalidGrantResult(
ErrorCodes.AuthorizationPending,
"The authorization request is still pending. " +
"The polling interval must be increased by at least 5 seconds for all subsequent requests.");

// If the user has been authenticated, return the authorized grant for token issuance.
{ Status: BackChannelAuthenticationStatus.Authenticated } => authenticationRequest.AuthorizedGrant,
// If the user denied the authentication request, return an error indicating access is denied
case { Status: BackChannelAuthenticationStatus.Denied }:
return new InvalidGrantResult(
ErrorCodes.AccessDenied,
"The authorization request is denied by the user.");

// Handle any unexpected statuses by throwing an exception.
_ => throw new InvalidOperationException(
$"The authentication request status is unexpected: {authenticationRequest.Status}.")
};
// Capture any unexpected case as an exception
default:
throw new InvalidOperationException(
$"The authentication request status is unexpected: {authenticationRequest.Status}.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ namespace Abblix.Oidc.Server.Endpoints.Token.Validation;

/// <summary>
/// Validates the authorization grant in the context of a token request, ensuring that the request is authorized
/// and that the associated redirect URI matches the one used during the initial authorization request.
/// and that the associated redirect URI matches the one used during the initial authorization request
/// </summary>
/// <remarks>
/// This validator interacts with the <see cref="IAuthorizationGrantHandler"/> to perform the necessary checks
/// on the authorization grant. It ensures that the token request is made for an authorized grant and verifies
/// the consistency of the redirect URI. If the grant is valid and authorized, it updates the validation context.
/// the consistency of the redirect URI. If the grant is valid and authorized, it updates the validation context
/// </remarks>
public class AuthorizationGrantValidator: ITokenContextValidator
{
Expand Down Expand Up @@ -63,6 +63,7 @@ public AuthorizationGrantValidator(IAuthorizationGrantHandler grantHandler)
/// </returns>
public async Task<TokenRequestError?> ValidateAsync(TokenValidationContext context)
{
// Ensure the client is authorized to use the requested grant type
if (!context.ClientInfo.AllowedGrantTypes.Contains(context.Request.GrantType))
{
return new TokenRequestError(
Expand All @@ -73,19 +74,25 @@ public AuthorizationGrantValidator(IAuthorizationGrantHandler grantHandler)
var result = await _grantHandler.AuthorizeAsync(context.Request, context.ClientInfo);
switch (result)
{
// Deny the request if the grant is invalid, providing the specific reason for rejection
case InvalidGrantResult { Error: var error, ErrorDescription: var description }:
return new TokenRequestError(error, description);

// Ensure that the redirect URI used matches the original authorization request
// to prevent redirection attacks
case AuthorizedGrant { Context.RedirectUri: var redirectUri }
when redirectUri != context.Request.RedirectUri:
return new TokenRequestError(
ErrorCodes.InvalidGrant,
"The redirect Uri value does not match to the value used before");

// Accept the request if the grant is valid, and securely store it in the validation context
// for further processing
case AuthorizedGrant grant:
context.AuthorizedGrant = grant;
return null;

// Handle any unexpected results in a way that ensures predictability in the flow
default:
throw new UnexpectedTypeException(nameof(result), result.GetType());
}
Expand Down
Loading

0 comments on commit 9ad814c

Please sign in to comment.