From fade652abeb74cff8b558ae1750238844a644bbf Mon Sep 17 00:00:00 2001 From: Kirill Kovalev Date: Tue, 9 Jul 2024 11:12:43 +0300 Subject: [PATCH] Resource Indicators support (#12) * Added parameter iss return from Authorization endpoint * Added Resource Indicators support * Bump-up nuget refs * Fix some suggestions from Sonar --- Abblix.Jwt/Abblix.Jwt.csproj | 2 +- .../Abblix.Oidc.Server.Mvc.csproj | 2 +- .../ActionResults/CookieOptionsExtensions.cs | 1 + .../AuthorizationResponseFormatter.cs | 4 +- .../Model/AuthorizationRequest.cs | 4 +- .../Model/ClientRegistrationRequest.cs | 6 +- Abblix.Oidc.Server.Mvc/Model/TokenRequest.cs | 6 +- .../Common/AuthorizationContext.cs | 7 + .../Common/AuthorizationContextExtensions.cs | 17 ++- .../Common/Configuration/OidcOptions.cs | 36 ++++- .../Common/Constants/ResourceDefinition.cs | 33 +++++ .../AuthorizationRequestProcessor.cs | 32 +++-- .../Interfaces/ConsentRequired.cs | 25 +++- .../Interfaces/ValidAuthorizationRequest.cs | 15 +- .../AuthorizationValidationContext.cs | 38 ++++-- .../Authorization/Validation/ErrorFactory.cs | 26 +++- .../Validation/ResourceValidator.cs | 84 ++++++++++++ .../Validation/ScopeValidator.cs | 42 ++++-- .../Endpoints/ServiceCollectionExtensions.cs | 67 +++++++-- .../Token/Interfaces/AuthorizedGrant.cs | 13 +- .../ITokenAuthorizationContextEvaluator.cs | 41 ++++++ .../Token/Interfaces/TokenIssuedResponse.cs | 5 + .../Token/Interfaces/ValidTokenRequest.cs | 22 ++- .../TokenAuthorizationContextEvaluator.cs | 70 ++++++++++ .../Endpoints/Token/TokenRequestProcessor.cs | 58 ++++---- .../Endpoints/Token/TokenRequestValidator.cs | 72 ++-------- .../Validation/AuthorizationGrantValidator.cs | 61 +++++++++ .../Token/Validation/ClientValidator.cs | 50 +++++++ .../Validation/ITokenContextValidator.cs | 30 ++++ .../Token/Validation/ResourceValidator.cs | 78 +++++++++++ .../Token/Validation/ScopeValidator.cs | 73 ++++++++++ .../SyncTokenContextValidatorBase.cs | 33 +++++ .../TokenContextValidatorComposite.cs | 50 +++++++ .../Validation/TokenValidationContext.cs | 59 ++++++++ .../UserInfo/UserInfoRequestValidator.cs | 22 ++- .../IClientAuthenticator.cs | 2 +- .../Features/Consents/ConsentDefinition.cs | 15 ++ .../Features/Consents/IConsentService.cs | 1 + .../Consents/IUserConsentsProvider.cs | 25 ++++ .../Features/Consents/NullConsentService.cs | 36 ++++- .../Consents/PromptConsentDecorator.cs | 66 +++++++++ .../Features/Consents/UserConsents.cs | 25 ++++ .../ResourceIndicators/IResourceManager.cs | 43 ++++++ .../ResourceIndicators/ResourceManager.cs | 63 +++++++++ .../ResourceManagerExtensions.cs | 93 +++++++++++++ .../Features/ScopeManagement/IScopeManager.cs | 40 ++++++ .../Features/ScopeManagement/ScopeManager.cs | 75 ++++++++++ .../ScopeManagement/ScopeManagerExtensions.cs | 90 ++++++++++++ .../Features/ServiceCollectionExtensions.cs | 8 ++ .../Features/Tokens/AccessTokenService.cs | 1 - .../Features/UserInfo/ScopeClaimsProvider.cs | 28 ++-- .../Features/UserInfo/UserClaimsProvider.cs | 6 +- .../Model/AuthorizationRequest.cs | 3 +- Abblix.Oidc.Server/Model/TokenRequest.cs | 14 +- Abblix.Utils.UnitTests/SanitizedTests.cs | 2 +- .../SingleOrArrayConverterTests.cs | 73 ++++++++++ Abblix.Utils/Json/ArrayConverter.cs | 2 +- Abblix.Utils/Json/SingleOrArrayConverter.cs | 129 ++++++++++++++++++ 58 files changed, 1806 insertions(+), 218 deletions(-) create mode 100644 Abblix.Oidc.Server/Common/Constants/ResourceDefinition.cs create mode 100644 Abblix.Oidc.Server/Endpoints/Authorization/Validation/ResourceValidator.cs create mode 100644 Abblix.Oidc.Server/Endpoints/Token/Interfaces/ITokenAuthorizationContextEvaluator.cs create mode 100644 Abblix.Oidc.Server/Endpoints/Token/TokenAuthorizationContextEvaluator.cs create mode 100644 Abblix.Oidc.Server/Endpoints/Token/Validation/AuthorizationGrantValidator.cs create mode 100644 Abblix.Oidc.Server/Endpoints/Token/Validation/ClientValidator.cs create mode 100644 Abblix.Oidc.Server/Endpoints/Token/Validation/ITokenContextValidator.cs create mode 100644 Abblix.Oidc.Server/Endpoints/Token/Validation/ResourceValidator.cs create mode 100644 Abblix.Oidc.Server/Endpoints/Token/Validation/ScopeValidator.cs create mode 100644 Abblix.Oidc.Server/Endpoints/Token/Validation/SyncTokenContextValidatorBase.cs create mode 100644 Abblix.Oidc.Server/Endpoints/Token/Validation/TokenContextValidatorComposite.cs create mode 100644 Abblix.Oidc.Server/Endpoints/Token/Validation/TokenValidationContext.cs create mode 100644 Abblix.Oidc.Server/Features/Consents/ConsentDefinition.cs create mode 100644 Abblix.Oidc.Server/Features/Consents/IUserConsentsProvider.cs create mode 100644 Abblix.Oidc.Server/Features/Consents/PromptConsentDecorator.cs create mode 100644 Abblix.Oidc.Server/Features/Consents/UserConsents.cs create mode 100644 Abblix.Oidc.Server/Features/ResourceIndicators/IResourceManager.cs create mode 100644 Abblix.Oidc.Server/Features/ResourceIndicators/ResourceManager.cs create mode 100644 Abblix.Oidc.Server/Features/ResourceIndicators/ResourceManagerExtensions.cs create mode 100644 Abblix.Oidc.Server/Features/ScopeManagement/IScopeManager.cs create mode 100644 Abblix.Oidc.Server/Features/ScopeManagement/ScopeManager.cs create mode 100644 Abblix.Oidc.Server/Features/ScopeManagement/ScopeManagerExtensions.cs create mode 100644 Abblix.Utils.UnitTests/SingleOrArrayConverterTests.cs create mode 100644 Abblix.Utils/Json/SingleOrArrayConverter.cs diff --git a/Abblix.Jwt/Abblix.Jwt.csproj b/Abblix.Jwt/Abblix.Jwt.csproj index 12b740b3..7caef866 100644 --- a/Abblix.Jwt/Abblix.Jwt.csproj +++ b/Abblix.Jwt/Abblix.Jwt.csproj @@ -24,7 +24,7 @@ - + diff --git a/Abblix.Oidc.Server.Mvc/Abblix.Oidc.Server.Mvc.csproj b/Abblix.Oidc.Server.Mvc/Abblix.Oidc.Server.Mvc.csproj index ba8ae871..e9b859ea 100644 --- a/Abblix.Oidc.Server.Mvc/Abblix.Oidc.Server.Mvc.csproj +++ b/Abblix.Oidc.Server.Mvc/Abblix.Oidc.Server.Mvc.csproj @@ -35,7 +35,7 @@ - + diff --git a/Abblix.Oidc.Server.Mvc/ActionResults/CookieOptionsExtensions.cs b/Abblix.Oidc.Server.Mvc/ActionResults/CookieOptionsExtensions.cs index 6e07b6f4..d7c4cb5b 100644 --- a/Abblix.Oidc.Server.Mvc/ActionResults/CookieOptionsExtensions.cs +++ b/Abblix.Oidc.Server.Mvc/ActionResults/CookieOptionsExtensions.cs @@ -24,6 +24,7 @@ using Microsoft.AspNetCore.Http; namespace Abblix.Oidc.Server.Mvc.ActionResults; + public static class CookieOptionsExtensions { /// diff --git a/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationResponseFormatter.cs b/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationResponseFormatter.cs index 60739eb0..dd499c3e 100644 --- a/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationResponseFormatter.cs +++ b/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationResponseFormatter.cs @@ -113,7 +113,7 @@ public async Task FormatResponseAsync( return await RedirectAsync( _options.Value.LoginUri.NotNull(nameof(OidcOptions.LoginUri)), response.Model); - case SuccessfullyAuthenticated { Model.RedirectUri: not null } success: + case SuccessfullyAuthenticated { Model.RedirectUri: { } redirectUri } success: var modelResponse = new AuthorizationResponse { @@ -127,7 +127,7 @@ public async Task FormatResponseAsync( SessionState = success.SessionState, }; - var actionResult = ToActionResult(modelResponse, success.ResponseMode, success.Model.RedirectUri); + var actionResult = ToActionResult(modelResponse, success.ResponseMode, redirectUri); if (_sessionManagementService.Enabled && success.SessionId.HasValue() && diff --git a/Abblix.Oidc.Server.Mvc/Model/AuthorizationRequest.cs b/Abblix.Oidc.Server.Mvc/Model/AuthorizationRequest.cs index b23d96fe..9fa05202 100644 --- a/Abblix.Oidc.Server.Mvc/Model/AuthorizationRequest.cs +++ b/Abblix.Oidc.Server.Mvc/Model/AuthorizationRequest.cs @@ -194,7 +194,7 @@ public record AuthorizationRequest /// resource. /// [BindProperty(SupportsGet = true, Name = Parameters.Resource)] - public Uri[]? Resource { get; set; } + public Uri[]? Resources { get; set; } public Core.AuthorizationRequest Map() => new() { @@ -218,6 +218,6 @@ public record AuthorizationRequest CodeChallengeMethod = CodeChallengeMethod, IdTokenHint = IdTokenHint, ClaimsLocales = ClaimsLocales, - Resource = Resource, + Resources = Resources, }; } diff --git a/Abblix.Oidc.Server.Mvc/Model/ClientRegistrationRequest.cs b/Abblix.Oidc.Server.Mvc/Model/ClientRegistrationRequest.cs index 079fbdc7..351f3b5e 100644 --- a/Abblix.Oidc.Server.Mvc/Model/ClientRegistrationRequest.cs +++ b/Abblix.Oidc.Server.Mvc/Model/ClientRegistrationRequest.cs @@ -289,7 +289,7 @@ public record ClientRegistrationRequest /// PKCE enhances the security of the OAuth authorization code flow, particularly for public clients. /// [JsonPropertyName(Parameters.PkceRequired)] - public bool PkceRequired { get; set; } + public bool? PkceRequired { get; set; } = false; /// /// Indicates whether this client is allowed to request offline access. @@ -303,7 +303,7 @@ public record ClientRegistrationRequest /// This is relevant for scenarios where the client needs to be notified when the user logs out. /// [JsonPropertyName(Parameters.BackChannelLogoutSessionRequired)] - public bool BackChannelLogoutSessionRequired { get; set; } + public bool? BackChannelLogoutSessionRequired { get; set; } = false; /// /// URI used for back-channel logout. This URI is called by the OpenID Provider to initiate a logout for the client. @@ -324,7 +324,7 @@ public record ClientRegistrationRequest /// This is used to manage user sessions in scenarios involving multiple clients. /// [JsonPropertyName(Parameters.FrontChannelLogoutSessionRequired)] - public bool FrontChannelLogoutSessionRequired { get; set; } + public bool? FrontChannelLogoutSessionRequired { get; set; } = false; /// /// Array of URIs to which the OP will redirect the user's user agent after logging out. diff --git a/Abblix.Oidc.Server.Mvc/Model/TokenRequest.cs b/Abblix.Oidc.Server.Mvc/Model/TokenRequest.cs index 6fd463b9..d80b1541 100644 --- a/Abblix.Oidc.Server.Mvc/Model/TokenRequest.cs +++ b/Abblix.Oidc.Server.Mvc/Model/TokenRequest.cs @@ -96,14 +96,14 @@ public record TokenRequest /// The username of the resource owner, used in the password grant type. /// This represents the credentials of the user for whom the client is requesting the token. /// - [BindProperty(SupportsGet = true, Name = Parameters.Username)] + [BindProperty(Name = Parameters.Username)] public string? UserName { get; set; } /// /// The password of the resource owner, used in the password grant type. /// Along with the username, this forms the user credentials required for the password grant type. /// - [BindProperty(SupportsGet = true, Name = Parameters.Password)] + [BindProperty(Name = Parameters.Password)] public string? Password { get; set; } /// @@ -125,7 +125,7 @@ public Core.TokenRequest Map() GrantType = GrantType, Code = Code, Password = Password, - Resource = Resource, + Resources = Resource, Scope = Scope, RefreshToken = RefreshToken, RedirectUri = RedirectUri, diff --git a/Abblix.Oidc.Server/Common/AuthorizationContext.cs b/Abblix.Oidc.Server/Common/AuthorizationContext.cs index b32962b2..d83519ff 100644 --- a/Abblix.Oidc.Server/Common/AuthorizationContext.cs +++ b/Abblix.Oidc.Server/Common/AuthorizationContext.cs @@ -83,4 +83,11 @@ public record AuthorizationContext(string ClientId, string[] Scope, RequestedCla /// enhancing the security of PKCE by allowing the authorization server to verify the code exchange authenticity. /// public string? CodeChallengeMethod { get; init; } + + /// + /// The resources for which the authorization is granted. + /// These resources are typically URIs that identify specific services or data that the client is authorized + /// to access. + /// + public Uri[]? Resources { get; init; } } diff --git a/Abblix.Oidc.Server/Common/AuthorizationContextExtensions.cs b/Abblix.Oidc.Server/Common/AuthorizationContextExtensions.cs index c6444901..db8f401e 100644 --- a/Abblix.Oidc.Server/Common/AuthorizationContextExtensions.cs +++ b/Abblix.Oidc.Server/Common/AuthorizationContextExtensions.cs @@ -54,6 +54,9 @@ public static void ApplyTo(this AuthorizationContext context, JsonWebTokenPayloa payload.ClientId = context.ClientId; payload.Scope = context.Scope; payload.Nonce = context.Nonce; + payload.Audiences = context.Resources is { Length: > 0 } + ? Array.ConvertAll(context.Resources, res => res.OriginalString) + : new[] { context.ClientId }; payload[JwtClaimTypes.RequestedClaims] = JsonSerializer.SerializeToNode(context.RequestedClaims, JsonSerializerOptions); } @@ -70,9 +73,21 @@ public static void ApplyTo(this AuthorizationContext context, JsonWebTokenPayloa /// public static AuthorizationContext ToAuthorizationContext(this JsonWebTokenPayload payload) { + var resources = + payload.Audiences.Count() == 1 && payload.Audiences.Single() == payload.ClientId + ? null + : payload.Audiences + .Select(aud => Uri.TryCreate(aud, UriKind.Absolute, out var uri) ? uri : null) + .OfType() + .ToArray(); + return new AuthorizationContext( payload.ClientId.NotNull(nameof(payload.ClientId)), payload.Scope.NotNull(nameof(payload.Scope)).ToArray(), - payload[JwtClaimTypes.RequestedClaims].Deserialize(JsonSerializerOptions)); + payload[JwtClaimTypes.RequestedClaims].Deserialize(JsonSerializerOptions)) + { + Nonce = payload.Nonce, + Resources = resources, + }; } } diff --git a/Abblix.Oidc.Server/Common/Configuration/OidcOptions.cs b/Abblix.Oidc.Server/Common/Configuration/OidcOptions.cs index f26b13a2..7922178d 100644 --- a/Abblix.Oidc.Server/Common/Configuration/OidcOptions.cs +++ b/Abblix.Oidc.Server/Common/Configuration/OidcOptions.cs @@ -21,6 +21,7 @@ // info@abblix.com using Abblix.Jwt; +using Abblix.Oidc.Server.Common.Constants; using Abblix.Oidc.Server.Features.ClientInformation; using Abblix.Oidc.Server.Model; @@ -41,7 +42,7 @@ public record OidcOptions /// /// Represents the unique identifier of the OIDC server. - /// It is recommended to use a URL that is controlled by the entity operating the OIDC server, and it should be + /// It is recommended to use a URL controlled by the entity operating the OIDC server, and it should be /// consistent across different environments to maintain trust with client applications. /// public string? Issuer { get; set; } @@ -100,7 +101,7 @@ public record OidcOptions /// /// The collection of JSON Web Keys (JWK) used for signing tokens issued by the OIDC server. /// Signing tokens is a critical security measure that ensures the integrity and authenticity of the tokens. - /// These keys are used to digitally sign ID tokens, access tokens, and other JWTs issued by the server, + /// These keys are used to digitally sign ID tokens, access tokens, and other JWT tokens issued by the server, /// allowing clients to verify that the tokens have not been tampered with and were indeed issued by this server. /// It is recommended to rotate these keys periodically to maintain the security of the token signing process. /// @@ -129,9 +130,9 @@ public record OidcOptions /// /// The collection of JSON Web Keys (JWK) used for encrypting tokens or sensitive information sent to the clients. /// Encryption is essential for protecting sensitive data within tokens, especially when tokens are passed through - /// less secure channels or when storing tokens at the client side. These keys are utilized to encrypt ID tokens and, - /// optionally, access tokens when the OIDC server sends them to clients. Clients use the corresponding public keys - /// to decrypt the tokens and access the contained claims. + /// less secure channels or when storing tokens on the client side. + /// These keys are used to encrypt ID tokens and, optionally, access tokens when the OIDC server sends them to clients. + /// Clients use the corresponding public keys to decrypt the tokens and access the contained claims. /// public IReadOnlyCollection EncryptionKeys { get; set; } = Array.Empty(); @@ -146,12 +147,33 @@ public record OidcOptions /// /// A JWT used for licensing and configuration validation of the OIDC service. This token contains claims that the /// OIDC service uses to validate its configuration, features, and licensing status, ensuring the service operates - /// within its licensed capabilities. Proper validation of this token is crucial for the service's legal and functional - /// compliance. + /// within its licensed capabilities. Proper validation of this token is crucial for the service's legal and + /// functional compliance. /// public string? LicenseJwt { get; set; } + /// + /// The standard length of the authorization code generated by the server. + /// public int AuthorizationCodeLength { get; set; } = 64; + /// + /// The standard length of the request URI generated by the server for Pushed Authorization Requests (PAR). + /// public int RequestUriLength { get; set; } = 64; + + /// + /// The supported scopes and their respective claim types, which outline the access permissions and associated data + /// that clients can request. + /// This setting determines what information and operations are available to different clients based on the scopes + /// they request during authorization. + /// + public ScopeDefinition[]? Scopes { get; set; } + + /// + /// The resource definitions supported by the OIDC server. This setting outlines the resources that clients + /// can request access to during authorization, ensuring the OIDC server can enforce access control policies + /// and permissions based on these definitions. + /// + public ResourceDefinition[]? Resources { get; set; } } diff --git a/Abblix.Oidc.Server/Common/Constants/ResourceDefinition.cs b/Abblix.Oidc.Server/Common/Constants/ResourceDefinition.cs new file mode 100644 index 00000000..64038210 --- /dev/null +++ b/Abblix.Oidc.Server/Common/Constants/ResourceDefinition.cs @@ -0,0 +1,33 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +namespace Abblix.Oidc.Server.Common.Constants; + +/// +/// Represents a resource with associated scopes, defining the permissions and access levels within an application. +/// This record is typically used to configure and enforce authorization policies based on resource identifiers +/// and their corresponding scopes. +/// +/// The identifier for the resource, often a unique name or URL representing the resource. +/// A variable number of scope definitions associated with the resource. Each scope definition +/// specifies a scope and its related claims, detailing the access levels and permissions granted. +public record ResourceDefinition(Uri Resource, params ScopeDefinition[] Scopes); diff --git a/Abblix.Oidc.Server/Endpoints/Authorization/AuthorizationRequestProcessor.cs b/Abblix.Oidc.Server/Endpoints/Authorization/AuthorizationRequestProcessor.cs index 6f0a81bd..15414c9f 100644 --- a/Abblix.Oidc.Server/Endpoints/Authorization/AuthorizationRequestProcessor.cs +++ b/Abblix.Oidc.Server/Endpoints/Authorization/AuthorizationRequestProcessor.cs @@ -51,21 +51,21 @@ public class AuthorizationRequestProcessor : IAuthorizationRequestProcessor /// and identity token services, and time-related functionality. /// /// Service for handling user authentication. - /// Service for managing user consent. + /// Service for managing user consent. /// Service for generating and managing authorization codes. /// Service for creating access tokens. /// Service for generating identity tokens. /// Service for managing time-related operations. public AuthorizationRequestProcessor( IAuthSessionService authSessionService, - IConsentService consentService, + IUserConsentsProvider consentsProvider, IAuthorizationCodeService authorizationCodeService, IAccessTokenService accessTokenService, IIdentityTokenService identityTokenService, TimeProvider clock) { _authSessionService = authSessionService; - _consentService = consentService; + _consentsProvider = consentsProvider; _authorizationCodeService = authorizationCodeService; _accessTokenService = accessTokenService; _identityTokenService = identityTokenService; @@ -75,7 +75,7 @@ public AuthorizationRequestProcessor( private readonly IAccessTokenService _accessTokenService; private readonly IAuthorizationCodeService _authorizationCodeService; private readonly IAuthSessionService _authSessionService; - private readonly IConsentService _consentService; + private readonly IUserConsentsProvider _consentsProvider; private readonly IIdentityTokenService _identityTokenService; private readonly TimeProvider _clock; @@ -131,7 +131,8 @@ public async Task ProcessAsync(ValidAuthorizationRequest var authSession = authSessions.Single(); - if (model.Prompt == Prompts.Consent || await _consentService.IsConsentRequired(request, authSession)) + var userConsents = await _consentsProvider.GetUserConsentsAsync(request, authSession); + if (userConsents.Pending is { Scopes.Length: > 0 } or { Resources.Length: > 0 }) { if (model.Prompt == Prompts.None) { @@ -143,16 +144,24 @@ public async Task ProcessAsync(ValidAuthorizationRequest model.RedirectUri); } - return new ConsentRequired(model, authSession); + return new ConsentRequired(model, authSession, userConsents.Pending); } var clientId = request.ClientInfo.ClientId; - var authContext = new AuthorizationContext(clientId, model.Scope, model.Claims) + var grantedConsents = userConsents.Granted; + var scopes = grantedConsents.Scopes + .Concat(grantedConsents.Resources.SelectMany(rd => rd.Scopes)) + .Select(sd => sd.Scope) + .Distinct() + .ToArray(); + var resources = Array.ConvertAll(grantedConsents.Resources, resource => resource.Resource); + var authContext = new AuthorizationContext(clientId, scopes, model.Claims) { RedirectUri = model.RedirectUri, Nonce = model.Nonce, CodeChallenge = model.CodeChallenge, CodeChallengeMethod = model.CodeChallengeMethod, + Resources = resources, }; if (!authSession.AffectedClientIds.Contains(clientId)) @@ -179,27 +188,22 @@ public async Task ProcessAsync(ValidAuthorizationRequest if (tokenRequired) { result.TokenType = TokenTypes.Bearer; - - var accessToken = await _accessTokenService.CreateAccessTokenAsync( + result.AccessToken = await _accessTokenService.CreateAccessTokenAsync( authSession, authContext, request.ClientInfo); - - result.AccessToken = accessToken; } var idTokenRequired = request.Model.ResponseType.HasFlag(ResponseTypes.IdToken); if (idTokenRequired) { - var idToken = await _identityTokenService.CreateIdentityTokenAsync( + result.IdToken = await _identityTokenService.CreateIdentityTokenAsync( authSession, authContext, request.ClientInfo, !codeRequired && !tokenRequired, result.Code, result.AccessToken?.EncodedJwt); - - result.IdToken = idToken; } return result; diff --git a/Abblix.Oidc.Server/Endpoints/Authorization/Interfaces/ConsentRequired.cs b/Abblix.Oidc.Server/Endpoints/Authorization/Interfaces/ConsentRequired.cs index 6b7331dd..d2d2cd50 100644 --- a/Abblix.Oidc.Server/Endpoints/Authorization/Interfaces/ConsentRequired.cs +++ b/Abblix.Oidc.Server/Endpoints/Authorization/Interfaces/ConsentRequired.cs @@ -20,6 +20,8 @@ // CONTACT: For license inquiries or permissions, contact Abblix LLP at // info@abblix.com +using Abblix.Oidc.Server.Common.Constants; +using Abblix.Oidc.Server.Features.Consents; using Abblix.Oidc.Server.Features.UserAuthentication; using Abblix.Oidc.Server.Model; @@ -27,7 +29,24 @@ namespace Abblix.Oidc.Server.Endpoints.Authorization.Interfaces; /// -/// Means that a user is logged and it is to ask for his consent for authorization. +/// Represents a state where the user is authenticated but requires consent for further authorization. +/// This record is used to encapsulate the details needed to prompt the user for consent. /// -public record ConsentRequired(AuthorizationRequest Model, AuthSession AuthSession) - : AuthorizationResponse(Model); +/// The model of the authorization request prompting the need for user consent. +/// The authentication session associated with the user, detailing their authenticated state. +/// +/// +/// Defines the consents that are pending and require user approval. +/// This includes the specific scopes and resources that need user consent before proceeding with the authorization +/// process. +public record ConsentRequired(AuthorizationRequest Model, AuthSession AuthSession, ConsentDefinition RequiredUserConsents) + : AuthorizationResponse(Model) +{ + [Obsolete("Use constructor with RequiredUserConsents parameter instead")] + public ConsentRequired(AuthorizationRequest Model, AuthSession AuthSession) + : this(Model, AuthSession, new ConsentDefinition( + Array.Empty(), + Array.Empty())) + { + } +} diff --git a/Abblix.Oidc.Server/Endpoints/Authorization/Interfaces/ValidAuthorizationRequest.cs b/Abblix.Oidc.Server/Endpoints/Authorization/Interfaces/ValidAuthorizationRequest.cs index a4e7ff40..01cff731 100644 --- a/Abblix.Oidc.Server/Endpoints/Authorization/Interfaces/ValidAuthorizationRequest.cs +++ b/Abblix.Oidc.Server/Endpoints/Authorization/Interfaces/ValidAuthorizationRequest.cs @@ -20,6 +20,7 @@ // CONTACT: For license inquiries or permissions, contact Abblix LLP at // info@abblix.com +using Abblix.Oidc.Server.Common.Constants; using Abblix.Oidc.Server.Endpoints.Authorization.Validation; using Abblix.Oidc.Server.Features.ClientInformation; using Abblix.Oidc.Server.Model; @@ -39,8 +40,9 @@ public ValidAuthorizationRequest(AuthorizationValidationContext context) { Model = context.Request; ClientInfo = context.ClientInfo; + Scope = context.Scope; + Resources = context.Resources; } - /// /// The original or recovered request model that was validated. /// @@ -50,4 +52,15 @@ public ValidAuthorizationRequest(AuthorizationValidationContext context) /// Information about the client making the request, as determined during validation. /// public ClientInfo ClientInfo { get; init; } + + /// + /// The scope associated with the authorization request, indicating the permissions requested by the client. + /// + public ScopeDefinition[] Scope { get; set; } + + /// + /// The resources associated with the authorization request, detailing the specific resources the client + /// is requesting access to. + /// + public ResourceDefinition[] Resources { get; set; } } diff --git a/Abblix.Oidc.Server/Endpoints/Authorization/Validation/AuthorizationValidationContext.cs b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/AuthorizationValidationContext.cs index 59123716..f70ca7fa 100644 --- a/Abblix.Oidc.Server/Endpoints/Authorization/Validation/AuthorizationValidationContext.cs +++ b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/AuthorizationValidationContext.cs @@ -28,43 +28,55 @@ namespace Abblix.Oidc.Server.Endpoints.Authorization.Validation; /// -/// Represents a validation context containing information about the client, response mode, and flow type -/// that is used during the authorization request validation process. +/// Encapsulates the context necessary for validating an authorization request, including client details, +/// response modes, and the OAuth 2.0 flow type. /// public record AuthorizationValidationContext(AuthorizationRequest Request) { /// - /// The request object to validate. + /// The authorization request to be validated. This includes all the details provided by the client + /// for the authorization process. /// public AuthorizationRequest Request { get; set; } = Request; private ClientInfo? _clientInfo; /// - /// The ClientInfo object containing information about the client. It is a result of identifying the client - /// making the authorization request. + /// Provides details about the client making the authorization request. This includes identifying information + /// such as client ID and any other relevant data that has been registered with the authorization server. /// - /// Thrown when attempting to get a null value. + /// Thrown when trying to access this property before it is set. + /// public ClientInfo ClientInfo { get => _clientInfo.NotNull(nameof(ClientInfo)); set => _clientInfo = value; } /// - /// The response mode associated with the authorization request, determining how the authorization response - /// should be delivered to the client. + /// Specifies how the authorization response should be delivered to the client, e.g., via a direct query or fragment. /// public string ResponseMode = ResponseModes.Query; private FlowTypes? _flowType; /// - /// The flow type associated with the authorization request, indicating the OAuth 2.0 flow being utilized - /// (e.g., Authorization Code, Implicit). + /// Identifies the OAuth 2.0 flow used in the authorization request, such as Authorization Code or Implicit. /// - /// Thrown when attempting to get a null value. + /// Thrown when trying to access this property before it is set. + /// public FlowTypes FlowType { get => _flowType.NotNull(nameof(FlowType)); set => _flowType = value; } /// - /// The validated and approved redirect URI for the authorization response. - /// This URI must match one of the URIs registered by the client. + /// The redirect URI where the response to the authorization request should be sent. This URI must be one of the + /// registered URIs for the client to ensure security. /// public Uri? ValidRedirectUri { get; set; } + + /// + /// A collection of scope definitions applicable to the authorization request, determining the permissions granted. + /// + public ScopeDefinition[] Scope { get; set; } = Array.Empty(); + + /// + /// A collection of resource definitions that may be requested as part of the authorization process, + /// providing additional control over the accessible resources. + /// + public ResourceDefinition[] Resources { get; set; } = Array.Empty(); } diff --git a/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ErrorFactory.cs b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ErrorFactory.cs index 96a73711..865e1a64 100644 --- a/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ErrorFactory.cs +++ b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ErrorFactory.cs @@ -1,22 +1,22 @@ // Abblix OIDC Server Library // Copyright (c) Abblix LLP. All rights reserved. -// +// // DISCLAIMER: This software is provided 'as-is', without any express or implied // warranty. Use at your own risk. Abblix LLP is not liable for any damages // arising from the use of this software. -// +// // LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed // in any form outside of the official GitHub repository at: // https://github.com/Abblix/OIDC.Server. All development and modifications // must occur within the official repository and are managed solely by Abblix LLP. -// +// // Unauthorized use, modification, or distribution of this software is strictly // prohibited and may be subject to legal action. -// +// // For full licensing terms, please visit: -// +// // https://oidc.abblix.com/license -// +// // CONTACT: For license inquiries or permissions, contact Abblix LLP at // info@abblix.com @@ -119,4 +119,18 @@ public static AuthorizationRequestValidationError Error( description, context.ValidRedirectUri, context.ResponseMode); + + /// + /// Creates an indicating an invalid scope error. + /// This error type is used when the scopes requested by the client are not supported or are inappropriate + /// for the requested operation. + /// + /// The validation context associated with the request, providing additional context for + /// the error response. + /// A human-readable description of why the requested scopes are invalid. + /// An with details about the scope-related issue. + public static AuthorizationRequestValidationError InvalidScope( + this AuthorizationValidationContext context, + string description) + => context.Error(ErrorCodes.InvalidScope, description); } diff --git a/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ResourceValidator.cs b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ResourceValidator.cs new file mode 100644 index 00000000..98fe15af --- /dev/null +++ b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ResourceValidator.cs @@ -0,0 +1,84 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using Abblix.Oidc.Server.Common.Constants; +using Abblix.Oidc.Server.Endpoints.Authorization.Interfaces; +using Abblix.Oidc.Server.Features.ResourceIndicators; + +namespace Abblix.Oidc.Server.Endpoints.Authorization.Validation; + +/// +/// Validates resources specified in authorization requests to ensure they conform to registered definitions and policies. +/// This validator checks whether the resources requested in the authorization process are recognized by the system +/// and permitted for the requesting client, extending the base functionality of resource validation by incorporating +/// integration with the authorization context. +/// +public class ResourceValidator: SyncAuthorizationContextValidatorBase +{ + /// + /// Initializes a new instance of the class, setting up the resource manager + /// responsible for maintaining and validating the definitions of resources. + /// + /// The manager responsible for retrieving and validating resource information. + /// + public ResourceValidator(IResourceManager resourceManager) + { + _resourceManager = resourceManager; + } + + private readonly IResourceManager _resourceManager; + + /// + /// Performs the validation of resource identifiers specified in the authorization request against the allowed + /// resource definitions managed by the . This method ensures that the resources + /// requested are known to the system and align with security and access policies. + /// + /// The context containing the authorization request, which includes the resources to be + /// validated. + /// + /// An containing error details if validation fails, + /// or null if the validation is successful, indicating that all requested resources are recognized and permissible. + /// + protected override AuthorizationRequestValidationError? Validate(AuthorizationValidationContext context) + { + var request = context.Request; + + // Proceed with validation only if there are resources specified in the request. + if (request.Resources is { Length: > 0 }) + { + // Validate the requested resources using the resource manager. + if (!_resourceManager.Validate( + request.Resources, + request.Scope, + out var resources, + out var errorDescription)) + { + return context.Error(ErrorCodes.InvalidTarget, errorDescription); + } + + context.Resources = resources; + } + + // Return null indicating successful validation if there are no errors. + return null; + } +} diff --git a/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ScopeValidator.cs b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ScopeValidator.cs index 6940c23b..0a132501 100644 --- a/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ScopeValidator.cs +++ b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ScopeValidator.cs @@ -20,9 +20,9 @@ // CONTACT: For license inquiries or permissions, contact Abblix LLP at // info@abblix.com -using Abblix.Oidc.Server.Common; using Abblix.Oidc.Server.Common.Constants; using Abblix.Oidc.Server.Endpoints.Authorization.Interfaces; +using Abblix.Oidc.Server.Features.ScopeManagement; namespace Abblix.Oidc.Server.Endpoints.Authorization.Validation; @@ -34,11 +34,17 @@ namespace Abblix.Oidc.Server.Endpoints.Authorization.Validation; /// public class ScopeValidator : SyncAuthorizationContextValidatorBase { + public ScopeValidator(IScopeManager scopeManager) + { + _scopeManager = scopeManager; + } + + private readonly IScopeManager _scopeManager; + /// - /// Validates the scopes specified in the authorization request. - /// It checks the compatibility of requested scopes with the client's allowed scopes - /// and the OAuth flow type. For instance, it validates if offline access is requested - /// appropriately and if the client is authorized for such access. + /// Validates the scopes specified in the authorization request. It checks the compatibility of requested scopes + /// with the client's allowed scopes and the OAuth flow type. For instance, it validates if offline access is + /// requested appropriately and if the client is authorized for such access. /// /// The validation context containing client information and request details. /// @@ -46,16 +52,26 @@ public class ScopeValidator : SyncAuthorizationContextValidatorBase /// or null if the scopes in the request are valid. /// protected override AuthorizationRequestValidationError? Validate(AuthorizationValidationContext context) - { - if (context.Request.Scope.HasFlag(Scopes.OfflineAccess)) - { - if (context.FlowType == FlowTypes.Implicit) - return context.InvalidRequest("It is not allowed to request for offline access in implicit flow"); + { + if (context.Request.Scope.Contains(Scopes.OfflineAccess)) + { + if (context.FlowType == FlowTypes.Implicit) + return context.InvalidScope("It is not allowed to request for offline access in implicit flow"); + + if (context.ClientInfo.OfflineAccessAllowed != true) + return context.InvalidScope("This client is not allowed to request for offline access"); + } - if (context.ClientInfo.OfflineAccessAllowed != true) - return context.InvalidRequest("This client is not allowed to request for offline access"); - } + if (!_scopeManager.Validate( + context.Request.Scope, + context.Resources, + out var scopeDefinitions, + out var errorDescription)) + { + return context.InvalidScope(errorDescription); + } + context.Scope = scopeDefinitions; return null; } } diff --git a/Abblix.Oidc.Server/Endpoints/ServiceCollectionExtensions.cs b/Abblix.Oidc.Server/Endpoints/ServiceCollectionExtensions.cs index f578f45d..87acaa64 100644 --- a/Abblix.Oidc.Server/Endpoints/ServiceCollectionExtensions.cs +++ b/Abblix.Oidc.Server/Endpoints/ServiceCollectionExtensions.cs @@ -47,11 +47,12 @@ using Abblix.Oidc.Server.Endpoints.Token; using Abblix.Oidc.Server.Endpoints.Token.Grants; using Abblix.Oidc.Server.Endpoints.Token.Interfaces; +using Abblix.Oidc.Server.Endpoints.Token.Validation; using Abblix.Oidc.Server.Endpoints.UserInfo; using Abblix.Oidc.Server.Endpoints.UserInfo.Interfaces; using Microsoft.Extensions.DependencyInjection; -using ClientValidator = Abblix.Oidc.Server.Endpoints.Authorization.Validation.ClientValidator; -using PostLogoutRedirectUrisValidator = Abblix.Oidc.Server.Endpoints.DynamicClientManagement.Validation.PostLogoutRedirectUrisValidator; +using Microsoft.Extensions.DependencyInjection.Extensions; + namespace Abblix.Oidc.Server.Endpoints; @@ -76,16 +77,30 @@ public static IServiceCollection AddAuthorizationEndpoint(this IServiceCollectio .AddScoped(); } + /// + /// Registers authorization request fetchers and related services into the provided IServiceCollection. + /// This method adds implementations for various authorization request fetchers as singletons, ensuring + /// that they are efficiently reused throughout the application. It also composes these fetchers into a + /// composite fetcher to handle different types of authorization requests seamlessly. + /// + /// The IServiceCollection to which the services will be added. + /// The updated IServiceCollection with the added authorization request fetchers. public static IServiceCollection AddAuthorizationRequestFetchers(this IServiceCollection services) { return services + // Add a JSON object binder as a singleton + .AddSingleton() + + // Add individual authorization request fetchers as singletons .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() + + // Compose the individual fetchers into a composite fetcher .Compose(); } + /// /// Adds a series of validators for authorization context as a composite service to ensure comprehensive validation /// of authorization requests. @@ -102,12 +117,13 @@ public static IServiceCollection AddAuthorizationContextValidators(this IService { return services // compose AuthorizationContext validation as a pipeline of several IAuthorizationContextValidator - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .Compose(); } @@ -137,13 +153,38 @@ public static IServiceCollection AddPushedAuthorizationEndpoint(this IServiceCol /// The configured . public static IServiceCollection AddTokenEndpoint(this IServiceCollection services) { - return services + services .AddAuthorizationGrants() + .AddTokenContextValidators(); + + services.TryAddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.Decorate(); + + return services; + } - .AddScoped() - .AddScoped() - .AddScoped() - .Decorate(); + /// + /// Configures and registers a composite of token context validators into the service collection. + /// This method sets up a sequence of validators that perform various checks on token requests, + /// ensuring they comply with the necessary criteria before a token can be issued. + /// + /// The service collection to which the token context validators will be added. + /// The modified service collection with the registered token context validators. + public static IServiceCollection AddTokenContextValidators(this IServiceCollection services) + { + return services + // Register individual validators that will participate in a composite pattern. + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + // Combine all registered ITokenContextValidator into a single composite validator. + // This composite approach allows the application to apply multiple validation checks sequentially. + .Compose(); } /// @@ -290,7 +331,7 @@ private static IServiceCollection AddClientRegistrationContextValidators(this IS // compose ClientRegistrationContext validation as a pipeline of several IClientRegistrationContextValidator .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -317,8 +358,8 @@ public static IServiceCollection AddEndSessionContextValidators(this IServiceCol { return services .AddSingleton() - .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .Compose(); } diff --git a/Abblix.Oidc.Server/Endpoints/Token/Interfaces/AuthorizedGrant.cs b/Abblix.Oidc.Server/Endpoints/Token/Interfaces/AuthorizedGrant.cs index a403a04a..427c3bce 100644 --- a/Abblix.Oidc.Server/Endpoints/Token/Interfaces/AuthorizedGrant.cs +++ b/Abblix.Oidc.Server/Endpoints/Token/Interfaces/AuthorizedGrant.cs @@ -28,12 +28,19 @@ namespace Abblix.Oidc.Server.Endpoints.Token.Interfaces; /// /// Represents the successful result of an authorized grant operation, -/// including authentication session and authorization context. +/// encapsulating the details of the authentication session and the authorization context. /// -/// The authentication session associated with the grant. -/// The context of the authorization process. +/// The authentication session associated with the grant, detailing the user's authenticated +/// state. +/// The context of the authorization process, providing specific details such as the client ID, +/// requested scopes, and any other relevant authorization parameters. public record AuthorizedGrant(AuthSession AuthSession, AuthorizationContext Context) : GrantAuthorizationResult { + /// + /// An array of tokens that have been issued as part of this grant. + /// This may include access tokens, refresh tokens, or other types of tokens + /// depending on the authorization flow and client request. + /// public TokenInfo[]? IssuedTokens { get; init; } } diff --git a/Abblix.Oidc.Server/Endpoints/Token/Interfaces/ITokenAuthorizationContextEvaluator.cs b/Abblix.Oidc.Server/Endpoints/Token/Interfaces/ITokenAuthorizationContextEvaluator.cs new file mode 100644 index 00000000..8fb1f364 --- /dev/null +++ b/Abblix.Oidc.Server/Endpoints/Token/Interfaces/ITokenAuthorizationContextEvaluator.cs @@ -0,0 +1,41 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using Abblix.Oidc.Server.Common; + +namespace Abblix.Oidc.Server.Endpoints.Token.Interfaces; + +/// +/// Defines an evaluator for determining the based on token requests. +/// +public interface ITokenAuthorizationContextEvaluator +{ + /// + /// Evaluates and constructs a new by refining and reconciling the scopes and resources + /// from the original authorization request based on the current token request. + /// + /// The valid token request that contains the original authorization grant and any additional + /// token-specific requests. + /// An updated that reflects the actual scopes and resources that + /// should be considered during the token issuance process. + AuthorizationContext EvaluateAuthorizationContext(ValidTokenRequest request); +} diff --git a/Abblix.Oidc.Server/Endpoints/Token/Interfaces/TokenIssuedResponse.cs b/Abblix.Oidc.Server/Endpoints/Token/Interfaces/TokenIssuedResponse.cs index 4ad1c25b..b0bccb1c 100644 --- a/Abblix.Oidc.Server/Endpoints/Token/Interfaces/TokenIssuedResponse.cs +++ b/Abblix.Oidc.Server/Endpoints/Token/Interfaces/TokenIssuedResponse.cs @@ -43,4 +43,9 @@ public record TokenIssuedResponse(EncodedJsonWebToken AccessToken, string TokenT /// An ID token that provides identity information about the user. /// public EncodedJsonWebToken? IdToken { get; set; } + + /// + /// The scopes associated with the access token issued. Scopes indicate the permissions granted to the access token. + /// + public IEnumerable Scope => AccessToken.Token.Payload.Scope; } diff --git a/Abblix.Oidc.Server/Endpoints/Token/Interfaces/ValidTokenRequest.cs b/Abblix.Oidc.Server/Endpoints/Token/Interfaces/ValidTokenRequest.cs index 3ea6a8bb..c8e6fd6d 100644 --- a/Abblix.Oidc.Server/Endpoints/Token/Interfaces/ValidTokenRequest.cs +++ b/Abblix.Oidc.Server/Endpoints/Token/Interfaces/ValidTokenRequest.cs @@ -20,6 +20,8 @@ // CONTACT: For license inquiries or permissions, contact Abblix LLP at // info@abblix.com +using Abblix.Oidc.Server.Common.Constants; +using Abblix.Oidc.Server.Endpoints.Token.Validation; using Abblix.Oidc.Server.Features.ClientInformation; using Abblix.Oidc.Server.Model; @@ -35,7 +37,25 @@ namespace Abblix.Oidc.Server.Endpoints.Token.Interfaces; /// process. /// Information about the client making the token request, including client credentials and /// metadata. +/// /// The scopes associated with the token request, indicating the permissions +/// requested by the client. +/// The resources associated with the token request, +/// detailing the specific resources the client is requesting access to. public record ValidTokenRequest( TokenRequest Model, AuthorizedGrant AuthorizedGrant, - ClientInfo ClientInfo) : TokenRequestValidationResult; + ClientInfo ClientInfo, + ScopeDefinition[] Scope, + ResourceDefinition[] Resources) + : TokenRequestValidationResult +{ + public ValidTokenRequest(TokenValidationContext context) + : this( + context.Request, + context.AuthorizedGrant, + context.ClientInfo, + context.Scope, + context.Resources) + { + } +} diff --git a/Abblix.Oidc.Server/Endpoints/Token/TokenAuthorizationContextEvaluator.cs b/Abblix.Oidc.Server/Endpoints/Token/TokenAuthorizationContextEvaluator.cs new file mode 100644 index 00000000..6e5c0a48 --- /dev/null +++ b/Abblix.Oidc.Server/Endpoints/Token/TokenAuthorizationContextEvaluator.cs @@ -0,0 +1,70 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using Abblix.Oidc.Server.Common; +using Abblix.Oidc.Server.Endpoints.Token.Interfaces; + +namespace Abblix.Oidc.Server.Endpoints.Token; + +/// +/// Evaluates instances based on token requests. +/// +public class TokenAuthorizationContextEvaluator : ITokenAuthorizationContextEvaluator +{ + /// + /// Evaluates and constructs a new by refining and reconciling the scopes + /// and resources from the original authorization request based on the current token request. + /// + /// The valid token request that contains the original authorization grant and any additional + /// token-specific requests. + /// An updated that reflects the actual scopes and resources that + /// should be considered during the token issuance process. + public AuthorizationContext EvaluateAuthorizationContext(ValidTokenRequest request) + { + var authContext = request.AuthorizedGrant.Context; + + // Determine the effective scopes for the token request. + var scope = authContext.Scope; + if (scope is { Length: > 0 } && request.Scope is { Length: > 0 }) + { + scope = scope + .Intersect(from sd in request.Scope select sd.Scope, StringComparer.Ordinal) + .ToArray(); + } + + // Determine the effective resources for the token request. + var resources = authContext.Resources; + if (resources is { Length: > 0 } && request.Resources is { Length: > 0 }) + { + resources = resources + .Intersect(from rd in request.Resources select rd.Resource) + .ToArray(); + } + + // Return a new authorization context updated with the determined scopes and resources. + return authContext with + { + Scope = scope, + Resources = resources, + }; + } +} diff --git a/Abblix.Oidc.Server/Endpoints/Token/TokenRequestProcessor.cs b/Abblix.Oidc.Server/Endpoints/Token/TokenRequestProcessor.cs index 51b44b41..0ce7d8b6 100644 --- a/Abblix.Oidc.Server/Endpoints/Token/TokenRequestProcessor.cs +++ b/Abblix.Oidc.Server/Endpoints/Token/TokenRequestProcessor.cs @@ -30,83 +30,85 @@ namespace Abblix.Oidc.Server.Endpoints.Token; /// -/// Processes token requests in compliance with OAuth 2.0 and OpenID Connect standards. -/// This processor is responsible for handling various types of token requests (e.g., authorization code, refresh token) -/// and generating the appropriate token responses, including access tokens, refresh tokens and ID tokens. +/// Processes token requests in compliance with OAuth 2.0 and OpenID Connect standards, +/// handling various types of token requests such as authorization code and refresh token. +/// Generates the appropriate token responses including access tokens, refresh tokens, and ID tokens. /// public class TokenRequestProcessor : ITokenRequestProcessor { /// - /// Initializes a new instance of the class with services for token generation - /// and management. + /// Initializes a new instance of the class, equipped with services + /// for token generation and management. /// /// Service for creating and managing access tokens. /// Service for creating and managing refresh tokens. /// Service for creating and managing ID tokens in OpenID Connect flows. + /// Service for building the authorization context from a token request. public TokenRequestProcessor( IAccessTokenService accessTokenService, IRefreshTokenService refreshTokenService, - IIdentityTokenService identityTokenService) + IIdentityTokenService identityTokenService, + ITokenAuthorizationContextEvaluator tokenContextEvaluator) { _accessTokenService = accessTokenService; _refreshTokenService = refreshTokenService; _identityTokenService = identityTokenService; + _tokenContextEvaluator = tokenContextEvaluator; } private readonly IAccessTokenService _accessTokenService; private readonly IRefreshTokenService _refreshTokenService; private readonly IIdentityTokenService _identityTokenService; + private readonly ITokenAuthorizationContextEvaluator _tokenContextEvaluator; /// /// Asynchronously processes a valid token request, determining the necessary tokens to generate based on - /// the request's scope and grant type. Generates an access token for every request and, depending on the scope, + /// the request's scope and grant type. It generates an access token for every request and, depending on the scope, /// may also generate a refresh token and an ID token for OpenID Connect authentication. /// - /// - /// The validated token request containing client and authorization session information. + /// The validated token request containing client and authorization session information. + /// /// A task representing the asynchronous operation, yielding a containing /// the generated tokens. /// - /// Access tokens are generated for client authorization in resource access. - /// Refresh tokens are issued for long-lived sessions, allowing clients to obtain new access tokens without - /// re-authentication. ID tokens provide identity information about the user and are used in OpenID Connect - /// authentication flows. This method ensures the secure and compliant generation of these tokens as per OAuth 2.0 - /// and OpenID Connect standards. + /// Access tokens authorize clients for resource access; refresh tokens enable long-lived sessions by allowing + /// new access tokens to be obtained without re-authentication; ID tokens provide identity information about + /// the user, crucial for OpenID Connect authentication flows. This method ensures secure and compliant token + /// generation. /// public async Task ProcessAsync(ValidTokenRequest request) { - request.ClientInfo.CheckClient(); + var clientInfo = request.ClientInfo; + clientInfo.CheckClient(); + + var authContext = _tokenContextEvaluator.EvaluateAuthorizationContext(request); var accessToken = await _accessTokenService.CreateAccessTokenAsync( request.AuthorizedGrant.AuthSession, - request.AuthorizedGrant.Context, - request.ClientInfo); + authContext, + clientInfo); var response = new TokenIssuedResponse( accessToken, TokenTypes.Bearer, - request.ClientInfo.AccessTokenExpiresIn, + clientInfo.AccessTokenExpiresIn, TokenTypeIdentifiers.AccessToken); - if (request.AuthorizedGrant.Context.Scope.HasFlag(Scopes.OfflineAccess)) + if (authContext.Scope.HasFlag(Scopes.OfflineAccess)) { response.RefreshToken = await _refreshTokenService.CreateRefreshTokenAsync( request.AuthorizedGrant.AuthSession, request.AuthorizedGrant.Context, - request.ClientInfo, - request.AuthorizedGrant switch - { - RefreshTokenAuthorizedGrant grant => grant.RefreshToken, - _ => null, - }); + clientInfo, + request.AuthorizedGrant is RefreshTokenAuthorizedGrant { RefreshToken: var refreshToken } ? refreshToken : null); } - if (request.AuthorizedGrant.Context.Scope.HasFlag(Scopes.OpenId)) + if (authContext.Scope.HasFlag(Scopes.OpenId)) { response.IdToken = await _identityTokenService.CreateIdentityTokenAsync( request.AuthorizedGrant.AuthSession, - request.AuthorizedGrant.Context, - request.ClientInfo, + authContext, + clientInfo, false, null, accessToken.EncodedJwt); diff --git a/Abblix.Oidc.Server/Endpoints/Token/TokenRequestValidator.cs b/Abblix.Oidc.Server/Endpoints/Token/TokenRequestValidator.cs index 06612fcf..e5a32962 100644 --- a/Abblix.Oidc.Server/Endpoints/Token/TokenRequestValidator.cs +++ b/Abblix.Oidc.Server/Endpoints/Token/TokenRequestValidator.cs @@ -20,41 +20,27 @@ // 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.Endpoints.Token.Grants; using Abblix.Oidc.Server.Endpoints.Token.Interfaces; -using Abblix.Oidc.Server.Features.ClientAuthentication; +using Abblix.Oidc.Server.Endpoints.Token.Validation; using Abblix.Oidc.Server.Model; -using Abblix.Utils; namespace Abblix.Oidc.Server.Endpoints.Token; /// /// Validates token requests against OAuth 2.0 specifications, ensuring that requests are properly formed and authorized. -/// This class plays a critical role in the OAuth 2.0 authentication and authorization process by verifying the integrity and -/// authenticity of token requests according to the framework defined in RFC 6749. +/// This class plays a critical role in the OAuth 2.0 authentication and authorization process by verifying the integrity +/// and authenticity of token requests, according to the framework defined in RFC 6749. /// public class TokenRequestValidator : ITokenRequestValidator { - /// - /// Initializes a new instance of the class. - /// Sets up the validator with the necessary components for client authentication and handling different - /// authorization grant types. Client authentication follows the process described in RFC 6749, Section 2.3. - /// - /// The authenticator used for validating client requests. - /// A grant handler responsible for different types of authorization grants. - public TokenRequestValidator( - IClientAuthenticator clientAuthenticator, - IAuthorizationGrantHandler grantHandler) + public TokenRequestValidator(ITokenContextValidator validator) { - _clientAuthenticator = clientAuthenticator; - _grantHandler = grantHandler; + _validator = validator; } - private readonly IClientAuthenticator _clientAuthenticator; - private readonly IAuthorizationGrantHandler _grantHandler; + private readonly ITokenContextValidator _validator; + /// /// Asynchronously validates a token request against the OAuth 2.0 specifications. It checks for proper authorization @@ -69,46 +55,10 @@ public TokenRequestValidator( /// or contain error information specifying why the request was invalid. public async Task ValidateAsync(TokenRequest tokenRequest, ClientRequest clientRequest) { - if (tokenRequest.Resource != null) - { - foreach (var resource in tokenRequest.Resource) - { - if (!resource.IsAbsoluteUri) - { - return new TokenRequestError( - ErrorCodes.InvalidTarget, - "The resource must be absolute URI"); - } - if (resource.Fragment.HasValue()) - { - return new TokenRequestError( - ErrorCodes.InvalidTarget, - "The resource must not contain fragment"); - } - } - } - - var clientInfo = await _clientAuthenticator.TryAuthenticateClientAsync(clientRequest); - if (clientInfo == null) - { - return new TokenRequestError(ErrorCodes.InvalidClient, "The client is not authorized"); - } - - var result = await _grantHandler.AuthorizeAsync(tokenRequest, clientInfo); - return result switch - { - InvalidGrantResult { Error: var error, ErrorDescription: var description } - => new TokenRequestError(error, description), - - AuthorizedGrant { Context.RedirectUri: var redirectUri } when redirectUri != tokenRequest.RedirectUri - => new TokenRequestError( - ErrorCodes.InvalidGrant, - "The redirect Uri value does not match to the value used before"), - - AuthorizedGrant grant - => new ValidTokenRequest(tokenRequest, grant, clientInfo), + // Context creation is a critical step in the validation process, encapsulating all necessary data. + var context = new TokenValidationContext(tokenRequest, clientRequest); - _ => throw new UnexpectedTypeException(nameof(result), result.GetType()), - }; + // Delegating the validation to the assigned validator and handling null error response as valid request. + return await _validator.ValidateAsync(context) ?? (TokenRequestValidationResult)new ValidTokenRequest(context); } } diff --git a/Abblix.Oidc.Server/Endpoints/Token/Validation/AuthorizationGrantValidator.cs b/Abblix.Oidc.Server/Endpoints/Token/Validation/AuthorizationGrantValidator.cs new file mode 100644 index 00000000..78710c25 --- /dev/null +++ b/Abblix.Oidc.Server/Endpoints/Token/Validation/AuthorizationGrantValidator.cs @@ -0,0 +1,61 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// 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.Endpoints.Token.Grants; +using Abblix.Oidc.Server.Endpoints.Token.Interfaces; + +namespace Abblix.Oidc.Server.Endpoints.Token.Validation; + +public class AuthorizationGrantValidator: ITokenContextValidator +{ + public AuthorizationGrantValidator(IAuthorizationGrantHandler grantHandler) + { + _grantHandler = grantHandler; + } + + private readonly IAuthorizationGrantHandler _grantHandler; + + public async Task ValidateAsync(TokenValidationContext context) + { + var result = await _grantHandler.AuthorizeAsync(context.Request, context.ClientInfo); + switch (result) + { + case InvalidGrantResult { Error: var error, ErrorDescription: var description }: + return new TokenRequestError(error, description); + + 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"); + + case AuthorizedGrant grant: + context.AuthorizedGrant = grant; + return null; + + default: + throw new UnexpectedTypeException(nameof(result), result.GetType()); + } + } +} diff --git a/Abblix.Oidc.Server/Endpoints/Token/Validation/ClientValidator.cs b/Abblix.Oidc.Server/Endpoints/Token/Validation/ClientValidator.cs new file mode 100644 index 00000000..4ae3f43d --- /dev/null +++ b/Abblix.Oidc.Server/Endpoints/Token/Validation/ClientValidator.cs @@ -0,0 +1,50 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using Abblix.Oidc.Server.Common.Constants; +using Abblix.Oidc.Server.Endpoints.Token.Interfaces; +using Abblix.Oidc.Server.Features.ClientAuthentication; + +namespace Abblix.Oidc.Server.Endpoints.Token.Validation; + +public class ClientValidator: ITokenContextValidator +{ + public ClientValidator(IClientAuthenticator clientAuthenticator) + { + _clientAuthenticator = clientAuthenticator; + } + + private readonly IClientAuthenticator _clientAuthenticator; + + public async Task ValidateAsync(TokenValidationContext context) + { + var clientRequest = context.ClientRequest; + var clientInfo = await _clientAuthenticator.TryAuthenticateClientAsync(clientRequest); + if (clientInfo == null) + { + return new TokenRequestError(ErrorCodes.InvalidClient, "The client is not authorized"); + } + + context.ClientInfo = clientInfo; + return null; + } +} diff --git a/Abblix.Oidc.Server/Endpoints/Token/Validation/ITokenContextValidator.cs b/Abblix.Oidc.Server/Endpoints/Token/Validation/ITokenContextValidator.cs new file mode 100644 index 00000000..c12032cd --- /dev/null +++ b/Abblix.Oidc.Server/Endpoints/Token/Validation/ITokenContextValidator.cs @@ -0,0 +1,30 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using Abblix.Oidc.Server.Endpoints.Token.Interfaces; + +namespace Abblix.Oidc.Server.Endpoints.Token.Validation; + +public interface ITokenContextValidator +{ + Task ValidateAsync(TokenValidationContext context); +} diff --git a/Abblix.Oidc.Server/Endpoints/Token/Validation/ResourceValidator.cs b/Abblix.Oidc.Server/Endpoints/Token/Validation/ResourceValidator.cs new file mode 100644 index 00000000..d3927a1e --- /dev/null +++ b/Abblix.Oidc.Server/Endpoints/Token/Validation/ResourceValidator.cs @@ -0,0 +1,78 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using Abblix.Oidc.Server.Common.Constants; +using Abblix.Oidc.Server.Endpoints.Token.Interfaces; +using Abblix.Oidc.Server.Features.ResourceIndicators; + +namespace Abblix.Oidc.Server.Endpoints.Token.Validation; + +/// +/// Provides validation for resource-related data within token requests, ensuring that all requested resources are +/// recognized and appropriately scoped according to OAuth 2.0 and OpenID Connect standards. +/// +public class ResourceValidator: SyncTokenContextValidatorBase +{ + /// + /// Initializes a new instance of the class with a specific resource manager. + /// + /// The manager responsible for validating and managing resource definitions. + public ResourceValidator(IResourceManager resourceManager) + { + _resourceManager = resourceManager; + } + + private readonly IResourceManager _resourceManager; + + /// + /// Validates the resources specified in a token request against known resource definitions. + /// This validation ensures that only registered and approved resources are accessed by the client. + /// + /// The context of the token validation including the request and client information. + /// + /// A if the validation fails, indicating the nature of the error and providing + /// an error message; otherwise, null if the resource validation passes successfully. + /// + protected override TokenRequestError? Validate(TokenValidationContext context) + { + var request = context.Request; + + // Proceed with validation only if there are resources specified in the request. + if (request.Resources is { Length: > 0 }) + { + // Validate the requested resources using the resource manager. + if (!_resourceManager.Validate( + request.Resources, + request.Scope, + out var resources, + out var errorDescription)) + { + return new TokenRequestError(ErrorCodes.InvalidTarget, errorDescription); + } + + context.Resources = resources; + } + + // Return null indicating successful validation if there are no errors. + return null; + } +} diff --git a/Abblix.Oidc.Server/Endpoints/Token/Validation/ScopeValidator.cs b/Abblix.Oidc.Server/Endpoints/Token/Validation/ScopeValidator.cs new file mode 100644 index 00000000..a421f365 --- /dev/null +++ b/Abblix.Oidc.Server/Endpoints/Token/Validation/ScopeValidator.cs @@ -0,0 +1,73 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using Abblix.Oidc.Server.Common.Constants; +using Abblix.Oidc.Server.Endpoints.Token.Interfaces; +using Abblix.Oidc.Server.Features.ScopeManagement; + +namespace Abblix.Oidc.Server.Endpoints.Token.Validation; + +/// +/// Validates the scopes specified in token requests using a scope manager to ensure their validity and availability. +/// This validator checks whether each requested scope is recognized and authorized for use, ensuring that clients +/// only receive permissions appropriate to their needs and in compliance with server policies. +/// +public class ScopeValidator: SyncTokenContextValidatorBase +{ + /// + /// Initializes a new instance of the class, equipping it with a scope manager + /// to validate requested scopes. + /// + /// The manager responsible for maintaining and validating scope definitions. + public ScopeValidator(IScopeManager scopeManager) + { + _scopeManager = scopeManager; + } + + private readonly IScopeManager _scopeManager; + + /// + /// Validates the scopes specified in the token request context. This method ensures that all requested scopes + /// are recognized by the scope manager and are permissible for the requesting client. + /// + /// The context containing the token request information, + /// including the scopes to be validated. + /// + /// A if any of the requested scopes are invalid or not permitted, + /// including an error code and a message describing the issue; + /// otherwise, returns null indicating that all requested scopes are valid. + /// + protected override TokenRequestError? Validate(TokenValidationContext context) + { + if (!_scopeManager.Validate( + context.Request.Scope, + context.Resources, + out var scopeDefinitions, + out var errorDescription)) + { + return new TokenRequestError(ErrorCodes.InvalidScope, errorDescription); + } + + context.Scope = scopeDefinitions; + return null; + } +} diff --git a/Abblix.Oidc.Server/Endpoints/Token/Validation/SyncTokenContextValidatorBase.cs b/Abblix.Oidc.Server/Endpoints/Token/Validation/SyncTokenContextValidatorBase.cs new file mode 100644 index 00000000..1ac60369 --- /dev/null +++ b/Abblix.Oidc.Server/Endpoints/Token/Validation/SyncTokenContextValidatorBase.cs @@ -0,0 +1,33 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using Abblix.Oidc.Server.Endpoints.Token.Interfaces; + +namespace Abblix.Oidc.Server.Endpoints.Token.Validation; + +public abstract class SyncTokenContextValidatorBase : ITokenContextValidator +{ + public Task ValidateAsync(TokenValidationContext context) + => Task.FromResult(Validate(context)); + + protected abstract TokenRequestError? Validate(TokenValidationContext context); +} \ No newline at end of file diff --git a/Abblix.Oidc.Server/Endpoints/Token/Validation/TokenContextValidatorComposite.cs b/Abblix.Oidc.Server/Endpoints/Token/Validation/TokenContextValidatorComposite.cs new file mode 100644 index 00000000..bacd252e --- /dev/null +++ b/Abblix.Oidc.Server/Endpoints/Token/Validation/TokenContextValidatorComposite.cs @@ -0,0 +1,50 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using Abblix.Oidc.Server.Endpoints.Token.Interfaces; + +namespace Abblix.Oidc.Server.Endpoints.Token.Validation; + +public class TokenContextValidatorComposite : ITokenContextValidator +{ + public TokenContextValidatorComposite(ITokenContextValidator[] validators) + { + _validators = validators; + } + + /// + /// The array of validators representing the steps in the validation process. + /// + private readonly ITokenContextValidator[] _validators; + + public async Task ValidateAsync(TokenValidationContext context) + { + foreach (var validator in _validators) + { + var error = await validator.ValidateAsync(context); + if (error != null) + return error; + } + + return null; + } +} diff --git a/Abblix.Oidc.Server/Endpoints/Token/Validation/TokenValidationContext.cs b/Abblix.Oidc.Server/Endpoints/Token/Validation/TokenValidationContext.cs new file mode 100644 index 00000000..a488a71f --- /dev/null +++ b/Abblix.Oidc.Server/Endpoints/Token/Validation/TokenValidationContext.cs @@ -0,0 +1,59 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using Abblix.Oidc.Server.Common.Constants; +using Abblix.Oidc.Server.Endpoints.Token.Interfaces; +using Abblix.Oidc.Server.Features.ClientInformation; +using Abblix.Oidc.Server.Model; + +namespace Abblix.Oidc.Server.Endpoints.Token.Validation; + +/// +/// Encapsulates the context required for validating token requests, including client and authorization grant details. +/// +public record TokenValidationContext(TokenRequest Request, ClientRequest ClientRequest) +{ + /// + /// Information about the client making the request, derived from the client authentication process. + /// + public ClientInfo ClientInfo { get; set; } + + /// + /// Represents the result of an authorized grant, containing both the session and context of the authorization. + /// This object is essential for ensuring that the grant is valid and for extracting any additional information + /// needed for token generation. + /// + public AuthorizedGrant AuthorizedGrant { get; set; } + + /// + /// Defines the scope of access requested or authorized. This array of scope definitions helps in determining + /// the extent of access granted to the client and any constraints or conditions applied to the token. + /// + public ScopeDefinition[] Scope { get; set; } = Array.Empty(); + + /// + /// Specifies additional resources that the client has requested or that have been included in the authorization. + /// These definitions provide context on the resources that are accessible with the issued token, enhancing + /// the token's utility for fine-grained access control. + /// + public ResourceDefinition[] Resources { get; set; } = Array.Empty(); +} diff --git a/Abblix.Oidc.Server/Endpoints/UserInfo/UserInfoRequestValidator.cs b/Abblix.Oidc.Server/Endpoints/UserInfo/UserInfoRequestValidator.cs index 37065ca7..f081c798 100644 --- a/Abblix.Oidc.Server/Endpoints/UserInfo/UserInfoRequestValidator.cs +++ b/Abblix.Oidc.Server/Endpoints/UserInfo/UserInfoRequestValidator.cs @@ -71,7 +71,9 @@ public UserInfoRequestValidator( /// Additional client request information for contextual validation. /// A representing the asynchronous operation, /// which upon completion will yield a . - public async Task ValidateAsync(UserInfoRequest userInfoRequest, ClientRequest clientRequest) + public async Task ValidateAsync( + UserInfoRequest userInfoRequest, + ClientRequest clientRequest) { string jwtAccessToken; var authorizationHeader = clientRequest.AuthorizationHeader; @@ -79,7 +81,9 @@ public async Task ValidateAsync(UserInfoRequest { if (authorizationHeader.Scheme != TokenTypes.Bearer) { - return new UserInfoRequestError(ErrorCodes.InvalidGrant, $"The scheme name '{authorizationHeader.Scheme}' is not supported"); + return new UserInfoRequestError( + ErrorCodes.InvalidGrant, + $"The scheme name '{authorizationHeader.Scheme}' is not supported"); } if (userInfoRequest.AccessToken != null) @@ -118,10 +122,12 @@ public async Task ValidateAsync(UserInfoRequest switch (result) { - case ValidJsonWebToken { Token: { Header.Type: var tokenType } token }: - if (tokenType != JwtTypes.AccessToken) - return new UserInfoRequestError(ErrorCodes.InvalidGrant, $"Invalid token type: {tokenType}"); + case ValidJsonWebToken { Token.Header.Type: var tokenType } when tokenType != JwtTypes.AccessToken: + return new UserInfoRequestError( + ErrorCodes.InvalidGrant, + $"Invalid token type: {tokenType}"); + case ValidJsonWebToken { Token: var token }: (authSession, authContext) = await _accessTokenService.AuthenticateByAccessTokenAsync(token); break; @@ -134,7 +140,11 @@ public async Task ValidateAsync(UserInfoRequest var clientInfo = await _clientInfoProvider.TryFindClientAsync(authContext.ClientId).WithLicenseCheck(); if (clientInfo == null) - return new UserInfoRequestError(ErrorCodes.InvalidGrant, $"The client '{authContext.ClientId}' is not found"); + { + return new UserInfoRequestError( + ErrorCodes.InvalidGrant, + $"The client '{authContext.ClientId}' is not found"); + } return new ValidUserInfoRequest(userInfoRequest, authSession, authContext, clientInfo); } diff --git a/Abblix.Oidc.Server/Features/ClientAuthentication/IClientAuthenticator.cs b/Abblix.Oidc.Server/Features/ClientAuthentication/IClientAuthenticator.cs index c9346e49..e2536f0a 100644 --- a/Abblix.Oidc.Server/Features/ClientAuthentication/IClientAuthenticator.cs +++ b/Abblix.Oidc.Server/Features/ClientAuthentication/IClientAuthenticator.cs @@ -32,7 +32,7 @@ public interface IClientAuthenticator { /// /// Specifies the authentication methods supported by this authenticator. - /// This property should return a value that identify the authentication scheme + /// This property should return a value that identifies the authentication scheme /// (e.g., "client_secret_basic", "private_key_jwt") supported by the implementer. /// IEnumerable ClientAuthenticationMethodsSupported { get; } diff --git a/Abblix.Oidc.Server/Features/Consents/ConsentDefinition.cs b/Abblix.Oidc.Server/Features/Consents/ConsentDefinition.cs new file mode 100644 index 00000000..3a04b635 --- /dev/null +++ b/Abblix.Oidc.Server/Features/Consents/ConsentDefinition.cs @@ -0,0 +1,15 @@ +using Abblix.Oidc.Server.Common.Constants; + +namespace Abblix.Oidc.Server.Features.Consents; + +/// +/// Defines the details of user consents required for specific scopes and resources. +/// This record is used to manage and validate user consent for accessing specific scopes and resources, +/// ensuring that consent is explicitly granted according to the requirements of the application and compliance +/// standards. +/// +/// An array of that represents the scopes for which user consent +/// is needed. +/// An array of that represents the resources for which +/// user consent is needed. +public record ConsentDefinition(ScopeDefinition[] Scopes, ResourceDefinition[] Resources); diff --git a/Abblix.Oidc.Server/Features/Consents/IConsentService.cs b/Abblix.Oidc.Server/Features/Consents/IConsentService.cs index ee23fd83..e69431f4 100644 --- a/Abblix.Oidc.Server/Features/Consents/IConsentService.cs +++ b/Abblix.Oidc.Server/Features/Consents/IConsentService.cs @@ -28,6 +28,7 @@ namespace Abblix.Oidc.Server.Features.Consents; /// /// Provides methods to determine whether user consent is to proceed with authentication. /// +[Obsolete("Use IConsentProvider instead")] public interface IConsentService { /// diff --git a/Abblix.Oidc.Server/Features/Consents/IUserConsentsProvider.cs b/Abblix.Oidc.Server/Features/Consents/IUserConsentsProvider.cs new file mode 100644 index 00000000..f2cec746 --- /dev/null +++ b/Abblix.Oidc.Server/Features/Consents/IUserConsentsProvider.cs @@ -0,0 +1,25 @@ +using Abblix.Oidc.Server.Endpoints.Authorization.Interfaces; +using Abblix.Oidc.Server.Features.UserAuthentication; + +namespace Abblix.Oidc.Server.Features.Consents; + +/// +/// Defines an interface for a service that provides user consents. This service is responsible for retrieving +/// and managing user consent decisions related to authorization requests. It ensures that the application adheres +/// to user preferences and legal requirements concerning data access and processing. +/// +public interface IUserConsentsProvider +{ + /// + /// Asynchronously retrieves the user consents for a given authorization request and authentication session. + /// This method is essential for determining which scopes and resources the user has consented to, enabling + /// the application to respect user permissions and comply with data protection regulations. + /// + /// The validated authorization request containing the scopes and resources for which + /// consent may be required. + /// The current authentication session that provides context about the authenticated user, + /// potentially influencing consent retrieval based on the user's settings or previous consent decisions. + /// A task that resolves to an instance of , containing detailed information + /// about the consents granted or denied by the user. + Task GetUserConsentsAsync(ValidAuthorizationRequest request, AuthSession authSession); +} diff --git a/Abblix.Oidc.Server/Features/Consents/NullConsentService.cs b/Abblix.Oidc.Server/Features/Consents/NullConsentService.cs index 4d242d4b..383fbc70 100644 --- a/Abblix.Oidc.Server/Features/Consents/NullConsentService.cs +++ b/Abblix.Oidc.Server/Features/Consents/NullConsentService.cs @@ -28,11 +28,37 @@ namespace Abblix.Oidc.Server.Features.Consents; /// /// Implements the very basic consent service not requiring any consents. /// -/// -/// Replace it with your own implementation in case you need to require a consent from a user. +/// /// +/// This implementation assumes that no consents are necessary for any operations, effectively bypassing +/// consent-related checks and workflows. If your application requires user consent for accessing specific scopes +/// or resources, replace this service with a custom implementation that appropriately handles such requirements. /// -public class NullConsentService : IConsentService +public class NullConsentService : IUserConsentsProvider, IConsentService { - public Task IsConsentRequired(ValidAuthorizationRequest request, AuthSession authSession) - => Task.FromResult(false); + /// + /// Retrieves the user consents for a given authorization request and authentication session. + /// + /// The validated authorization request for which to retrieve consents. + /// The authentication session associated with the request. + /// A task that resolves to an instance of , containing details about + /// the consents granted by the user. This implementation automatically assumes all consents are granted. + public Task GetUserConsentsAsync(ValidAuthorizationRequest request, AuthSession authSession) + { + var userConsents = new UserConsents { Granted = new(request.Scope, request.Resources) }; + return Task.FromResult(userConsents); + } + + /// + /// Determines whether consent is required for a given authorization request and authentication session. + /// + /// The validated authorization request that might require consent. + /// The authentication session associated with the request, + /// containing user-specific data. + /// A task that resolves to a boolean indicating whether user consent is needed. This implementation + /// always returns false, suggesting consent is never required. + public async Task IsConsentRequired(ValidAuthorizationRequest request, AuthSession authSession) + { + var userConsents = await GetUserConsentsAsync(request, authSession); + return userConsents.Pending is { Scopes.Length: > 0 } or { Resources.Length: > 0 }; + } } diff --git a/Abblix.Oidc.Server/Features/Consents/PromptConsentDecorator.cs b/Abblix.Oidc.Server/Features/Consents/PromptConsentDecorator.cs new file mode 100644 index 00000000..5ba04959 --- /dev/null +++ b/Abblix.Oidc.Server/Features/Consents/PromptConsentDecorator.cs @@ -0,0 +1,66 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using Abblix.Oidc.Server.Common.Constants; +using Abblix.Oidc.Server.Endpoints.Authorization.Interfaces; +using Abblix.Oidc.Server.Features.UserAuthentication; + +namespace Abblix.Oidc.Server.Features.Consents; + +/// +/// A decorator for the that enforces the prompt for consent when required. +/// This class intercepts the consent retrieval process to inject mandatory consent prompts based on the authorization +/// request details. +/// +public class PromptConsentDecorator: IUserConsentsProvider +{ + /// + /// Initializes a new instance of the class, wrapping an existing consents' + /// provider. + /// + /// The inner-to - delegate calls to when no explicit + /// prompting is needed. + public PromptConsentDecorator(IUserConsentsProvider inner) + { + _inner = inner; + } + + private readonly IUserConsentsProvider _inner; + + /// + /// Retrieves user consents, injecting a mandatory prompt for consent if specified by the authorization request. + /// + /// The validated authorization request containing details that may require user interaction + /// for consent. + /// The current authentication session that might affect how consents are handled. + /// A task that resolves to an instance of , which will include any consents + /// that are pending based on the authorization request parameters. + public async Task GetUserConsentsAsync(ValidAuthorizationRequest request, AuthSession authSession) + => request.Model.Prompt switch + { + // If the 'consent' prompt is explicitly requested, force all scopes and resources to be pending consent. + Prompts.Consent => new UserConsents { Pending = new(request.Scope, request.Resources) }, + + // Otherwise, defer to the inner consents' provider. + _ => await _inner.GetUserConsentsAsync(request, authSession), + }; +} diff --git a/Abblix.Oidc.Server/Features/Consents/UserConsents.cs b/Abblix.Oidc.Server/Features/Consents/UserConsents.cs new file mode 100644 index 00000000..cd4d9b78 --- /dev/null +++ b/Abblix.Oidc.Server/Features/Consents/UserConsents.cs @@ -0,0 +1,25 @@ +using Abblix.Oidc.Server.Common.Constants; + +namespace Abblix.Oidc.Server.Features.Consents; + +/// +/// Represents the state of user consents in an authorization flow, categorizing them into granted, denied, and pending. +/// +public record UserConsents +{ + /// + /// The consents that have been explicitly granted by the user. + /// These consents cover scopes and resources the user has agreed to provide access to. + /// + public ConsentDefinition Granted { get; set; } = new( + Array.Empty(), + Array.Empty()); + + /// + /// The consents that are still pending a decision by the user. + /// These include scopes and resources that have been requested but not yet explicitly approved or denied. + /// + public ConsentDefinition Pending { get; set; } = new( + Array.Empty(), + Array.Empty()); +}; diff --git a/Abblix.Oidc.Server/Features/ResourceIndicators/IResourceManager.cs b/Abblix.Oidc.Server/Features/ResourceIndicators/IResourceManager.cs new file mode 100644 index 00000000..877cf60c --- /dev/null +++ b/Abblix.Oidc.Server/Features/ResourceIndicators/IResourceManager.cs @@ -0,0 +1,43 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using System.Diagnostics.CodeAnalysis; +using Abblix.Oidc.Server.Common.Constants; + +namespace Abblix.Oidc.Server.Features.ResourceIndicators; + +/// +/// Provides an interface for managing and retrieving resource definitions. This interface is essential for +/// ensuring that requests for resources are validated against registered and recognized definitions, supporting +/// the enforcement of policies related to resource access and permissions. +/// +public interface IResourceManager +{ + /// + /// Attempts to retrieve the resource definition associated with the specified URI. + /// + /// The URI identifying the resource for which the definition is requested. + /// When this method returns, contains the resource definition associated with + /// the specified URI, if the resource is found; otherwise, null. This parameter is passed uninitialized. + /// true if the resource definition is found; otherwise, false. + bool TryGet(Uri resource, [MaybeNullWhen(false)] out ResourceDefinition definition); +} diff --git a/Abblix.Oidc.Server/Features/ResourceIndicators/ResourceManager.cs b/Abblix.Oidc.Server/Features/ResourceIndicators/ResourceManager.cs new file mode 100644 index 00000000..d1e7678d --- /dev/null +++ b/Abblix.Oidc.Server/Features/ResourceIndicators/ResourceManager.cs @@ -0,0 +1,63 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using System.Diagnostics.CodeAnalysis; +using Abblix.Oidc.Server.Common.Configuration; +using Abblix.Oidc.Server.Common.Constants; +using Microsoft.Extensions.Options; + +namespace Abblix.Oidc.Server.Features.ResourceIndicators; + +/// +/// Manages resource definitions, ensuring they are registered and retrievable based on their URIs. +/// +public class ResourceManager : IResourceManager +{ + /// + /// Initializes a new instance of the class, + /// registering resources defined in the OIDC options. + /// + /// The OIDC options containing resource definitions to be registered. + public ResourceManager(IOptions options) + { + if (options.Value.Resources != null) + Array.ForEach(options.Value.Resources, AddResource); + } + + private readonly Dictionary _resources = new(); + + /// + /// Adds a resource definition to the internal collection. + /// + /// The resource definition to be added. + private void AddResource(ResourceDefinition resource) => _resources.Add(resource.Resource, resource); + + /// + /// Attempts to retrieve the resource definition associated with the specified URI. + /// + /// The URI identifying the resource for which the definition is requested. + /// When this method returns, contains the resource definition associated with + /// the specified URI, if the resource is found; otherwise, null. This parameter is passed uninitialized. + /// true if the resource definition is found; otherwise, false. + public bool TryGet(Uri resource, [MaybeNullWhen(false)] out ResourceDefinition definition) + => _resources.TryGetValue(resource, out definition); +} diff --git a/Abblix.Oidc.Server/Features/ResourceIndicators/ResourceManagerExtensions.cs b/Abblix.Oidc.Server/Features/ResourceIndicators/ResourceManagerExtensions.cs new file mode 100644 index 00000000..a732b965 --- /dev/null +++ b/Abblix.Oidc.Server/Features/ResourceIndicators/ResourceManagerExtensions.cs @@ -0,0 +1,93 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using System.Diagnostics.CodeAnalysis; +using Abblix.Oidc.Server.Common.Constants; +using Abblix.Utils; + +namespace Abblix.Oidc.Server.Features.ResourceIndicators; + +/// +/// Provides extension methods for resource validation, leveraging a resource manager to ensure the validity and +/// permissibility of requested resources. +/// +public static class ResourceManagerExtensions +{ + /// + /// Validates requested resources against registered resource definitions to confirm their validity and + /// authorization. + /// This method ensures that resources and the requested scopes within those resources are registered and allowed. + /// + /// The resource manager that maintains the definitions of resources. + /// A collection of URIs representing the resources being requested. + /// A collection of scope identifiers associated with the request. + /// Outputs an array of objects if + /// the validation is successful, otherwise null. + /// Outputs a string describing the reason for validation failure, + /// otherwise null if the validation is successful. + /// True if all requested resources and their corresponding scopes are valid and permissible, + /// false otherwise. + public static bool Validate( + this IResourceManager resourceManager, + IEnumerable resources, + IEnumerable scopes, + [MaybeNullWhen(false)] out ResourceDefinition[] resourceDefinitions, + [MaybeNullWhen(true)] out string errorDescription) + { + resourceDefinitions = null; + errorDescription = null; + + var resourceList = new List(); + var scopeSet = scopes.ToHashSet(StringComparer.Ordinal); + + foreach (var resource in resources) + { + if (!resource.IsAbsoluteUri) + { + errorDescription = "The resource must be absolute URI"; + return false; + } + + if (resource.Fragment.HasValue()) + { + errorDescription = "The requested resource must not contain fragment part"; + return false; + } + + if (!resourceManager.TryGet(resource, out var definition)) + { + errorDescription = "The requested resource is unknown"; + return false; + } + + // Filter the scopes of the resource to include only those that are requested + var requestedScopes = definition.Scopes + .Where(def => scopeSet.Contains(def.Scope)) + .ToArray(); + + resourceList.Add(definition with { Scopes = requestedScopes }); + } + + resourceDefinitions = resourceList.ToArray(); + return true; + } +} diff --git a/Abblix.Oidc.Server/Features/ScopeManagement/IScopeManager.cs b/Abblix.Oidc.Server/Features/ScopeManagement/IScopeManager.cs new file mode 100644 index 00000000..5edd5730 --- /dev/null +++ b/Abblix.Oidc.Server/Features/ScopeManagement/IScopeManager.cs @@ -0,0 +1,40 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using System.Diagnostics.CodeAnalysis; +using Abblix.Oidc.Server.Common.Constants; + +namespace Abblix.Oidc.Server.Features.ScopeManagement; + +/// +/// Defines a contract for managing and retrieving scope definitions. +/// +public interface IScopeManager: IEnumerable +{ + /// + /// Attempts to retrieve the definition of a specified scope. + /// + /// The scope identifier to retrieve the definition for. + /// Outputs the if the scope exists, otherwise null. + /// True if the scope exists and the definition is retrieved, false otherwise. + bool TryGet(string scope, [MaybeNullWhen(false)] out ScopeDefinition definition); +} diff --git a/Abblix.Oidc.Server/Features/ScopeManagement/ScopeManager.cs b/Abblix.Oidc.Server/Features/ScopeManagement/ScopeManager.cs new file mode 100644 index 00000000..d965a216 --- /dev/null +++ b/Abblix.Oidc.Server/Features/ScopeManagement/ScopeManager.cs @@ -0,0 +1,75 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using Abblix.Oidc.Server.Common.Configuration; +using Abblix.Oidc.Server.Common.Constants; +using Microsoft.Extensions.Options; + +namespace Abblix.Oidc.Server.Features.ScopeManagement; + +/// +/// Manages the registration and retrieval of scope definitions, providing a mechanism to validate requested scopes +/// against predefined or configured scopes. +/// +public class ScopeManager : IScopeManager +{ + /// + /// Initializes a new instance of the class with default standard scopes and additional + /// custom scopes provided through configuration. + /// + /// The options containing OIDC configuration, including additional custom scopes. + public ScopeManager(IOptions options) + { + Add(StandardScopes.OpenId); + Add(StandardScopes.Profile); + Add(StandardScopes.Email); + Add(StandardScopes.Address); + Add(StandardScopes.Phone); + Add(StandardScopes.OfflineAccess); + + if (options.Value.Scopes != null) + Array.ForEach(options.Value.Scopes, Add); + } + + private readonly Dictionary _scopes = new(StringComparer.Ordinal); + + /// + /// Adds a new scope definition to the manager. + /// + /// The scope definition to add. + private void Add(ScopeDefinition scope) => _scopes.TryAdd(scope.Scope, scope); + + /// + /// Attempts to retrieve the definition of a specified scope. + /// + /// The scope identifier to retrieve the definition for. + /// Outputs the if the scope exists, otherwise null. + /// True if the scope exists and the definition is retrieved, false otherwise. + public bool TryGet(string scope, [MaybeNullWhen(false)] out ScopeDefinition definition) + => _scopes.TryGetValue(scope, out definition); + + public IEnumerator GetEnumerator() => _scopes.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/Abblix.Oidc.Server/Features/ScopeManagement/ScopeManagerExtensions.cs b/Abblix.Oidc.Server/Features/ScopeManagement/ScopeManagerExtensions.cs new file mode 100644 index 00000000..f16c5e0b --- /dev/null +++ b/Abblix.Oidc.Server/Features/ScopeManagement/ScopeManagerExtensions.cs @@ -0,0 +1,90 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using System.Diagnostics.CodeAnalysis; +using Abblix.Oidc.Server.Common.Constants; + +namespace Abblix.Oidc.Server.Features.ScopeManagement; + +/// +/// Provides extension methods for scope validation, leveraging a scope manager and resource definitions +/// to ensure the validity and permissibility of requested scopes. +/// +public static class ScopeManagerExtensions +{ + /// + /// Validates the requested scopes against registered scope definitions and resource definitions to confirm + /// their validity and authorization. + /// This method ensures that scopes are either recognized by the scope manager or included in the resource + /// definitions. + /// + /// The scope manager that maintains the definitions of scopes. + /// A collection of scope identifiers to be validated. + /// An optional array of objects to validate scopes + /// against. + /// Outputs an array of objects if + /// the validation is successful, otherwise null. + /// Outputs a string describing the reason for validation failure, + /// otherwise null if the validation is successful. + /// True if all requested scopes are valid and permissible, false otherwise. + public static bool Validate( + this IScopeManager scopeManager, + IEnumerable scopes, + ResourceDefinition[]? resources, + [MaybeNullWhen(false)] out ScopeDefinition[] scopeDefinitions, + [MaybeNullWhen(true)] out string errorDescription) + { + scopeDefinitions = null; + errorDescription = null; + + var scopeList = new List(); + + // Create a hash set of resource scopes if resources are provided and not empty + var resourceScopes = resources is { Length: > 0 } + ? resources + .SelectMany(rd => rd.Scopes, (_, sd) => sd.Scope) + .ToHashSet(StringComparer.Ordinal) + : null; + + foreach (var scope in scopes) + { + // Check if the scope is recognized by the scope manager + if (scopeManager.TryGet(scope, out var scopeDefinition)) + { + scopeList.Add(scopeDefinition); + } + // Check if the scope is part of the resource scopes + else if (resourceScopes != null && resourceScopes.Contains(scope)) + { + // skip it + } + else + { + errorDescription = "The scope is not available"; + return false; + } + } + + scopeDefinitions = scopeList.ToArray(); + return true; + } +} diff --git a/Abblix.Oidc.Server/Features/ServiceCollectionExtensions.cs b/Abblix.Oidc.Server/Features/ServiceCollectionExtensions.cs index 8ce57d2a..08a1a791 100644 --- a/Abblix.Oidc.Server/Features/ServiceCollectionExtensions.cs +++ b/Abblix.Oidc.Server/Features/ServiceCollectionExtensions.cs @@ -34,6 +34,8 @@ using Abblix.Oidc.Server.Features.Licensing; using Abblix.Oidc.Server.Features.LogoutNotification; using Abblix.Oidc.Server.Features.RandomGenerators; +using Abblix.Oidc.Server.Features.ResourceIndicators; +using Abblix.Oidc.Server.Features.ScopeManagement; using Abblix.Oidc.Server.Features.SessionManagement; using Abblix.Oidc.Server.Features.Storages; using Abblix.Oidc.Server.Features.Tokens; @@ -95,6 +97,10 @@ public static IServiceCollection AddClientInformation(this IServiceCollection se public static IServiceCollection AddCommonServices(this IServiceCollection services) { services.TryAddSingleton(); + + services.TryAddSingleton(); + services.Decorate(); + services.TryAddSingleton(TimeProvider.System); services.TryAddSingleton(); services.TryAddSingleton(); @@ -376,6 +382,8 @@ public static IServiceCollection AddUserInfo(this IServiceCollection services) services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); return services; } } diff --git a/Abblix.Oidc.Server/Features/Tokens/AccessTokenService.cs b/Abblix.Oidc.Server/Features/Tokens/AccessTokenService.cs index 1038a27a..83191724 100644 --- a/Abblix.Oidc.Server/Features/Tokens/AccessTokenService.cs +++ b/Abblix.Oidc.Server/Features/Tokens/AccessTokenService.cs @@ -107,7 +107,6 @@ public async Task CreateAccessTokenAsync( NotBefore = issuedAt, ExpiresAt = issuedAt + clientInfo.AccessTokenExpiresIn, Issuer = LicenseChecker.CheckIssuer(_issuerProvider.GetIssuer()), - Audiences = new[] { clientInfo.ClientId }, }, }; diff --git a/Abblix.Oidc.Server/Features/UserInfo/ScopeClaimsProvider.cs b/Abblix.Oidc.Server/Features/UserInfo/ScopeClaimsProvider.cs index 36eb784c..c63f3351 100644 --- a/Abblix.Oidc.Server/Features/UserInfo/ScopeClaimsProvider.cs +++ b/Abblix.Oidc.Server/Features/UserInfo/ScopeClaimsProvider.cs @@ -21,7 +21,7 @@ // info@abblix.com using Abblix.Jwt; -using Abblix.Oidc.Server.Common.Constants; +using Abblix.Oidc.Server.Features.ScopeManagement; namespace Abblix.Oidc.Server.Features.UserInfo; @@ -32,18 +32,12 @@ namespace Abblix.Oidc.Server.Features.UserInfo; /// public class ScopeClaimsProvider : IScopeClaimsProvider { - /// - /// A mapping from scopes to the respective arrays of claim types that each scope encompasses. - /// - private readonly Dictionary _scopeToClaimsMap = new[] + public ScopeClaimsProvider(IScopeManager scopeManager) { - StandardScopes.OpenId, - StandardScopes.Profile, - StandardScopes.Email, - StandardScopes.Address, - StandardScopes.Phone, - StandardScopes.OfflineAccess, - }.ToDictionary(definition => definition.Scope, definition => definition.ClaimTypes, StringComparer.OrdinalIgnoreCase); + _scopeManager = scopeManager; + } + + private readonly IScopeManager _scopeManager; /// /// Retrieves the specific claims associated with the requested scopes and any additional requested claims. @@ -57,7 +51,9 @@ public IEnumerable GetRequestedClaims( IEnumerable? requestedClaims) { var claimNames = scopes - .SelectMany(scope => _scopeToClaimsMap.TryGetValue(scope, out var claims) ? claims : Array.Empty()) + .SelectMany(scope => _scopeManager.TryGet(scope, out var scopeDefinition) + ? scopeDefinition.ClaimTypes + : Array.Empty()) .Prepend(JwtClaimTypes.Subject); if (requestedClaims != null) @@ -71,10 +67,12 @@ public IEnumerable GetRequestedClaims( /// /// A collection of all the scopes supported by this provider. /// - public IEnumerable ScopesSupported => _scopeToClaimsMap.Keys; + public IEnumerable ScopesSupported + => _scopeManager.Select(def => def.Scope); /// /// A collection of all the claims that can be provided by this provider. /// - public IEnumerable ClaimsSupported => _scopeToClaimsMap.Values.SelectMany(claims => claims); + public IEnumerable ClaimsSupported + => _scopeManager.SelectMany(def => def.ClaimTypes).Distinct(StringComparer.Ordinal); } diff --git a/Abblix.Oidc.Server/Features/UserInfo/UserClaimsProvider.cs b/Abblix.Oidc.Server/Features/UserInfo/UserClaimsProvider.cs index d618b769..56a9e460 100644 --- a/Abblix.Oidc.Server/Features/UserInfo/UserClaimsProvider.cs +++ b/Abblix.Oidc.Server/Features/UserInfo/UserClaimsProvider.cs @@ -84,11 +84,11 @@ public UserClaimsProvider( ClientInfo clientInfo) { var claimNames = _scopeClaimsProvider.GetRequestedClaims( - scope, requestedClaims?.Select(claim => claim.Key)); + scope, requestedClaims?.Select(claim => claim.Key)) + .Distinct(StringComparer.Ordinal); var userInfo = await _userInfoProvider.GetUserInfoAsync( - authSession.Subject, - claimNames.Distinct(StringComparer.Ordinal)); + authSession.Subject, claimNames); if (userInfo == null) { _logger.LogWarning("The user claims were not found by subject value"); diff --git a/Abblix.Oidc.Server/Model/AuthorizationRequest.cs b/Abblix.Oidc.Server/Model/AuthorizationRequest.cs index e68491da..a3be487f 100644 --- a/Abblix.Oidc.Server/Model/AuthorizationRequest.cs +++ b/Abblix.Oidc.Server/Model/AuthorizationRequest.cs @@ -178,7 +178,8 @@ public record AuthorizationRequest /// resource. /// [JsonPropertyName(Parameters.Resource)] - public Uri[]? Resource { get; set; } + [JsonConverter(typeof(SingleOrArrayConverter))] + public Uri[]? Resources { get; set; } public static class Parameters { diff --git a/Abblix.Oidc.Server/Model/TokenRequest.cs b/Abblix.Oidc.Server/Model/TokenRequest.cs index a8d8e8bc..44b0dda2 100644 --- a/Abblix.Oidc.Server/Model/TokenRequest.cs +++ b/Abblix.Oidc.Server/Model/TokenRequest.cs @@ -23,11 +23,13 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Abblix.Oidc.Server.Common.Constants; +using Abblix.Utils.Json; namespace Abblix.Oidc.Server.Model; /// -/// Represents a request for obtaining various types of tokens (e.g., access token, refresh token) from the authorization server. +/// Represents a request to get various types of tokens +/// (e.g., access token, refresh token) from the authorization server. /// public record TokenRequest { @@ -79,7 +81,8 @@ public static class Parameters /// Defined in RFC 8707. /// [JsonPropertyName(Parameters.Resource)] - public Uri[]? Resource { get; set; } + [JsonConverter(typeof(SingleOrArrayConverter))] + public Uri[]? Resources { get; set; } /// /// The refresh token used to obtain a new access token. Required for the refresh token grant type. @@ -91,12 +94,7 @@ public static class Parameters /// The scope of the access request, expressed as a list of space-delimited, case-sensitive strings. /// [JsonPropertyName(Parameters.Scope)] - [AllowedValues( - Scopes.OpenId, - Scopes.Profile, - Scopes.Email, - Scopes.Phone, - Scopes.OfflineAccess)] + [AllowedValues(Scopes.OpenId, Scopes.Profile, Scopes.Email, Scopes.Phone, Scopes.OfflineAccess)] public string[] Scope { get; set; } = Array.Empty(); /// diff --git a/Abblix.Utils.UnitTests/SanitizedTests.cs b/Abblix.Utils.UnitTests/SanitizedTests.cs index 099144ed..aa9613d3 100644 --- a/Abblix.Utils.UnitTests/SanitizedTests.cs +++ b/Abblix.Utils.UnitTests/SanitizedTests.cs @@ -156,7 +156,7 @@ public void ToString_ShouldHandleNullInput() { const string? input = null; var sanitizedValue = new Sanitized(input); - Assert.Null(sanitizedValue.ToString()); + Assert.Equal(string.Empty, sanitizedValue.ToString()); } /// diff --git a/Abblix.Utils.UnitTests/SingleOrArrayConverterTests.cs b/Abblix.Utils.UnitTests/SingleOrArrayConverterTests.cs new file mode 100644 index 00000000..0af80302 --- /dev/null +++ b/Abblix.Utils.UnitTests/SingleOrArrayConverterTests.cs @@ -0,0 +1,73 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using System.Text; +using System.Text.Json; +using Abblix.Utils.Json; + +namespace Abblix.Utils.UnitTests; + +public class SingleOrArrayConverterTests +{ + private readonly SingleOrArrayConverter _converter = new(); + + [Theory] + [InlineData("\"singleString\"", new[] { "singleString" })] + [InlineData("[\"string1\", \"string2\"]", new[] { "string1", "string2" })] + [InlineData("null", null)] + public void Read_ValidJson_ReturnsExpectedArray(string json, string[]? expected) + { + var reader = new Utf8JsonReader(new ReadOnlySpan(Encoding.UTF8.GetBytes(json))); + reader.Read(); // Move to the first token + + var result = _converter.Read(ref reader, typeof(string[]), new JsonSerializerOptions()); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("123")] + [InlineData("{\"key\":\"value\"}")] + public void Read_InvalidJson_ThrowsJsonException(string json) + { + Assert.Throws(() => + { + var reader = new Utf8JsonReader(new ReadOnlySpan(Encoding.UTF8.GetBytes(json))); + reader.Read(); // Move to the first token + return _converter.Read(ref reader, typeof(string[]), new JsonSerializerOptions()); + }); + } + + [Theory] + [InlineData(new[] { "singleString" }, "\"singleString\"")] + [InlineData(new[] { "string1", "string2" }, "[\"string1\",\"string2\"]")] + [InlineData(null, "null")] + public void Write_ValidArray_WritesExpectedJson(string[]? value, string expectedJson) + { + using var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream)) + _converter.Write(writer, value, new JsonSerializerOptions()); + + var json = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expectedJson, json); + } +} diff --git a/Abblix.Utils/Json/ArrayConverter.cs b/Abblix.Utils/Json/ArrayConverter.cs index e2db12ae..8b3c75d2 100644 --- a/Abblix.Utils/Json/ArrayConverter.cs +++ b/Abblix.Utils/Json/ArrayConverter.cs @@ -27,7 +27,7 @@ namespace Abblix.Utils.Json; /// /// A custom JSON converter that handles the serialization and deserialization of arrays of a specific type. -/// Utilizes a specified element converter for individual elements of the array. +/// It uses a specified element converter for individual elements of the array. /// /// The type of the elements in the array. /// The type of the converter used for the elements in the array. diff --git a/Abblix.Utils/Json/SingleOrArrayConverter.cs b/Abblix.Utils/Json/SingleOrArrayConverter.cs new file mode 100644 index 00000000..e860b24d --- /dev/null +++ b/Abblix.Utils/Json/SingleOrArrayConverter.cs @@ -0,0 +1,129 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +namespace Abblix.Utils.Json; + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +/// +/// A JSON converter that handles deserialization and serialization of a JSON value +/// that could either be a single string or an array of strings. +/// +public class SingleOrArrayConverter : JsonConverter +{ + /// + /// Reads and converts the JSON to a string array. + /// If the JSON token is a single string, it returns an array containing one element. + /// If it is an array of strings, it converts each element and returns them in an array. + /// + /// The reader from which to read the JSON document. + /// The type to convert. Expected to be a string array. + /// Options for the serializer. + /// An array of strings parsed from the JSON input. + /// Thrown if an unexpected token type is encountered. + + public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var elementType = typeof(T); + var converter = (JsonConverter)options.GetConverter(elementType) + ?? throw new JsonException($"No converter found for {elementType}"); + + switch (reader.TokenType) + { + case JsonTokenType.Null: + return null; + + case JsonTokenType.String: + return new[] { ReadFrom(ref reader, elementType, converter, options) }; + + case JsonTokenType.StartArray: + break; + + default: + throw new JsonException("Unexpected token type."); + } + + var values = new List(); + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.EndArray: + break; + + case JsonTokenType.String: + values.Add(ReadFrom(ref reader, elementType, converter, options)); + break; + + default: + throw new JsonException("Unexpected token type in array."); + } + } + return values.ToArray(); + + static T ReadFrom(ref Utf8JsonReader reader, Type elementType, JsonConverter converter, JsonSerializerOptions options) + { + return converter.Read(ref reader, elementType, options) + ?? throw new JsonException("Null values are not allowed"); + } + } + + /// + /// Writes a string array to a JSON writer. + /// If the array contains a single string, it writes it as a single string value. + /// If it contains multiple strings, it writes them as an array of strings. + /// + /// The writer to which the JSON will be written. + /// The string array to write. + /// Options for the serializer. + /// Thrown if the writer or value is null. + public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options) + { + var elementType = typeof(T); + var converter = (JsonConverter)options.GetConverter(elementType) + ?? throw new JsonException($"No converter found for {elementType}"); + + if (value == null) + { + writer.WriteNullValue(); + return; + } + + switch (value.Length) + { + case 1: + converter.Write(writer, value[0], options); + break; + + default: + writer.WriteStartArray(); + foreach (var item in value) + { + converter.Write(writer, item, options); + } + writer.WriteEndArray(); + break; + } + } +}