diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..641b49f8 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,93 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master", "develop" ] + pull_request: + branches: [ "master", "develop" ] + schedule: + - cron: '0 0 * * 1' # Weekly schedule at midnight UTC on Mondays + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: csharp + build-mode: autobuild + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/Abblix.DependencyInjection/Abblix.DependencyInjection.csproj b/Abblix.DependencyInjection/Abblix.DependencyInjection.csproj index cd1ba096..6beffc4c 100644 --- a/Abblix.DependencyInjection/Abblix.DependencyInjection.csproj +++ b/Abblix.DependencyInjection/Abblix.DependencyInjection.csproj @@ -20,6 +20,8 @@ For detailed release notes, visit: https://github.com/Abblix/Oidc.Server/releases Abblix.png true + 1.1.0.0 + 1.1.0.0 diff --git a/Abblix.Jwt.UnitTests/Abblix.Jwt.UnitTests.csproj b/Abblix.Jwt.UnitTests/Abblix.Jwt.UnitTests.csproj index f7c1ef1a..ff1a425e 100644 --- a/Abblix.Jwt.UnitTests/Abblix.Jwt.UnitTests.csproj +++ b/Abblix.Jwt.UnitTests/Abblix.Jwt.UnitTests.csproj @@ -10,9 +10,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Abblix.Jwt.UnitTests/JwtEncryptionTests.cs b/Abblix.Jwt.UnitTests/JwtEncryptionTests.cs index 9bc4302c..fde7d790 100644 --- a/Abblix.Jwt.UnitTests/JwtEncryptionTests.cs +++ b/Abblix.Jwt.UnitTests/JwtEncryptionTests.cs @@ -66,8 +66,8 @@ public async Task JwtFullCycleTest() { ValidateAudience = aud => Task.FromResult(token.Payload.Audiences.SequenceEqual(aud)), ValidateIssuer = iss => Task.FromResult(iss == token.Payload.Issuer), - ResolveTokenDecryptionKeys = _ => new [] { EncryptingKey }.AsAsync(), - ResolveIssuerSigningKeys = _ => new [] { SigningKey }.AsAsync(), + ResolveTokenDecryptionKeys = _ => new [] { EncryptingKey }.ToAsyncEnumerable(), + ResolveIssuerSigningKeys = _ => new [] { SigningKey }.ToAsyncEnumerable(), }; var result = Assert.IsType(await validator.ValidateAsync(jwt, parameters)); diff --git a/Abblix.Jwt/Abblix.Jwt.csproj b/Abblix.Jwt/Abblix.Jwt.csproj index 0357cc7b..f1fbc345 100644 --- a/Abblix.Jwt/Abblix.Jwt.csproj +++ b/Abblix.Jwt/Abblix.Jwt.csproj @@ -20,11 +20,14 @@ For detailed release notes, visit: https://github.com/Abblix/Oidc.Server/releases Abblix.png true + 1.1.0.0 + 1.1.0.0 - + + diff --git a/Abblix.Jwt/JsonWebKeyFactory.cs b/Abblix.Jwt/JsonWebKeyFactory.cs index 1ee7b019..2b3e964f 100644 --- a/Abblix.Jwt/JsonWebKeyFactory.cs +++ b/Abblix.Jwt/JsonWebKeyFactory.cs @@ -21,6 +21,7 @@ // info@abblix.com using System.Security.Cryptography; +using Abblix.Utils; using Microsoft.IdentityModel.Tokens; namespace Abblix.Jwt; @@ -53,12 +54,14 @@ public static class JsonWebKeyFactory rsa.KeySize = keySize; var parameters = rsa.ExportParameters(true); + var parametersExponent = parameters.Exponent; var key = new JsonWebKey { KeyType = "RSA", + KeyId = parameters.ToKeyId(), Algorithm = algorithm, Usage = usage, - RsaExponent = parameters.Exponent, + RsaExponent = parametersExponent, RsaModulus = parameters.Modulus, PrivateKey = parameters.D, FirstPrimeFactor = parameters.P, @@ -70,4 +73,26 @@ public static class JsonWebKeyFactory return key; } + + private static string ToKeyId(this RSAParameters parameters) + { + var keyMaterial = (parameters.Modulus, parameters.Exponent) switch + { + ({} modulus, {} exponent) => modulus.Concat(exponent), + ({} modulus, null) => modulus, + (null, {} exponent) => exponent, + (null, null) => Array.Empty(), + }; + + // Compute the SHA-256 hash of the concatenated string + return SHA256.HashData(keyMaterial).ToHexString(); + } + + private static byte[] Concat(this byte[] modulus, byte[] exponent) + { + var buffer = new byte[modulus.Length + exponent.Length]; + Array.Copy(modulus, buffer, modulus.Length); + Array.Copy(exponent, 0, buffer, modulus.Length, exponent.Length); + return buffer; + } } diff --git a/Abblix.Jwt/JsonWebTokenValidator.cs b/Abblix.Jwt/JsonWebTokenValidator.cs index 712e4763..c6c9b762 100644 --- a/Abblix.Jwt/JsonWebTokenValidator.cs +++ b/Abblix.Jwt/JsonWebTokenValidator.cs @@ -96,9 +96,9 @@ private static JwtValidationResult Validate(string jwt, ValidationParameters par var signingKeys = resolveIssuerSigningKeys(securityToken.Issuer); if (keyId.HasValue()) - signingKeys = signingKeys.WhereAsync(key => key.KeyId == keyId); + signingKeys = signingKeys.Where(key => key.KeyId == keyId); - return signingKeys.SelectAsync(key => key.ToSecurityKey()).ToListAsync().Result; + return signingKeys.Select(key => key.ToSecurityKey()).ToListAsync().Result; }; } @@ -109,9 +109,9 @@ private static JwtValidationResult Validate(string jwt, ValidationParameters par var decryptionKeys = resolveTokenDecryptionKeys(securityToken.Issuer); if (keyId.HasValue()) - decryptionKeys = decryptionKeys.WhereAsync(key => key.KeyId == keyId); + decryptionKeys = decryptionKeys.Where(key => key.KeyId == keyId); - return decryptionKeys.SelectAsync(key => key.ToSecurityKey()).ToListAsync().Result; + return decryptionKeys.Select(key => key.ToSecurityKey()).ToListAsync().Result; }; var handler = new JwtSecurityTokenHandler(); diff --git a/Abblix.Oidc.Server.Mvc/Abblix.Oidc.Server.Mvc.csproj b/Abblix.Oidc.Server.Mvc/Abblix.Oidc.Server.Mvc.csproj index ba8ae871..ffc9480b 100644 --- a/Abblix.Oidc.Server.Mvc/Abblix.Oidc.Server.Mvc.csproj +++ b/Abblix.Oidc.Server.Mvc/Abblix.Oidc.Server.Mvc.csproj @@ -22,6 +22,8 @@ For detailed release notes, visit: https://github.com/Abblix/Oidc.Server/releases Abblix.png true + 1.1.0.0 + 1.1.0.0 @@ -35,7 +37,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/Attributes/AbsoluteUriAttribute.cs b/Abblix.Oidc.Server.Mvc/Attributes/AbsoluteUriAttribute.cs index c5301585..85bca900 100644 --- a/Abblix.Oidc.Server.Mvc/Attributes/AbsoluteUriAttribute.cs +++ b/Abblix.Oidc.Server.Mvc/Attributes/AbsoluteUriAttribute.cs @@ -47,9 +47,9 @@ public sealed class AbsoluteUriAttribute : ValidationAttribute /// Determines whether the specified value of the object is valid. /// /// The value of the object to validate. - /// The context information about the object being validated. + /// The context information about the object being validated. /// if the value is a valid absolute URI, otherwise an error . - protected override ValidationResult? IsValid(object? value, ValidationContext context) + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => value switch { null => ValidationResult.Success, // absence is OK, apply [Required] if needed @@ -57,14 +57,14 @@ public sealed class AbsoluteUriAttribute : ValidationAttribute string str when string.IsNullOrEmpty(str) => ValidationResult.Success, Uri uri when string.IsNullOrEmpty(uri.OriginalString) => ValidationResult.Success, - string str when Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var uri) => IsValid(uri, context), + string str when Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var uri) => IsValid(uri, validationContext), Uri { IsAbsoluteUri: true, Scheme: var scheme } when RequireScheme.HasValue() && !string.Equals(RequireScheme, scheme, StringComparison.OrdinalIgnoreCase) - => new ValidationResult($"{context.GetName()} value must use {RequireScheme} scheme."), + => new ValidationResult($"{validationContext.GetName()} value must use {RequireScheme} scheme."), Uri { IsAbsoluteUri: true } => ValidationResult.Success, - Uri => new ValidationResult($"{context.GetName()} value is not absolute."), - _ => new ValidationResult($"{context.GetName()} is not Uri, but {value.GetType().Name}."), + Uri => new ValidationResult($"{validationContext.GetName()} value is not absolute."), + _ => new ValidationResult($"{validationContext.GetName()} is not Uri, but {value.GetType().Name}."), }; } diff --git a/Abblix.Oidc.Server.Mvc/Attributes/AllowedValuesAttribute.cs b/Abblix.Oidc.Server.Mvc/Attributes/AllowedValuesAttribute.cs index 67e3c3d7..1596b981 100644 --- a/Abblix.Oidc.Server.Mvc/Attributes/AllowedValuesAttribute.cs +++ b/Abblix.Oidc.Server.Mvc/Attributes/AllowedValuesAttribute.cs @@ -51,23 +51,23 @@ public AllowedValuesAttribute(params string[] allowedValues) /// Determines whether the specified value of the object is valid. /// /// The value of the object to validate. - /// The context information about the object being validated. + /// The context information about the object being validated. /// if the value is among the allowed values, /// otherwise an error . /// - protected override ValidationResult? IsValid(object? value, ValidationContext context) + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { return value switch { null => ValidationResult.Success, - string[][] stringValues => IsValid(stringValues.SelectMany(stringValue => stringValue), context), - string[] stringValues => IsValid(stringValues, context), + string[][] stringValues => IsValid(stringValues.SelectMany(stringValue => stringValue)), + string[] stringValues => IsValid(stringValues), string stringValue => IsValid(stringValue), _ => throw new InvalidOperationException($"The type {value.GetType()} is not supported by {nameof(AllowedValuesAttribute)}"), }; } - private ValidationResult? IsValid(IEnumerable values, ValidationContext context) + private ValidationResult? IsValid(IEnumerable values) { foreach (var value in values) { diff --git a/Abblix.Oidc.Server.Mvc/Attributes/ConditionalRequiredAttribute.cs b/Abblix.Oidc.Server.Mvc/Attributes/ConditionalRequiredAttribute.cs index 2f4ef2e1..30f64e62 100644 --- a/Abblix.Oidc.Server.Mvc/Attributes/ConditionalRequiredAttribute.cs +++ b/Abblix.Oidc.Server.Mvc/Attributes/ConditionalRequiredAttribute.cs @@ -38,17 +38,17 @@ public abstract class ConditionalRequiredAttribute : RequiredAttribute /// Determines whether the specified value of the object is valid based on a custom condition. /// /// The value of the object to validate. - /// The context information about the object being validated. + /// The context information about the object being validated. /// /// if the condition is not met or if the value is valid as per the base ; /// otherwise, an error . /// - protected override ValidationResult? IsValid(object? value, ValidationContext context) + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { - if (!IsRequired(context.ObjectInstance)) + if (!IsRequired(validationContext.ObjectInstance)) return ValidationResult.Success; - return base.IsValid(value, context); + return base.IsValid(value, validationContext); } /// diff --git a/Abblix.Oidc.Server.Mvc/AutoPostFormatter.cs b/Abblix.Oidc.Server.Mvc/AutoPostFormatter.cs index 62ecdac2..562e6200 100644 --- a/Abblix.Oidc.Server.Mvc/AutoPostFormatter.cs +++ b/Abblix.Oidc.Server.Mvc/AutoPostFormatter.cs @@ -57,14 +57,14 @@ public AutoPostFormatter(IParametersProvider parametersProvider, Uri action) /// This method overrides the base class implementation to write an HTML form with the specified parameters. /// /// The context for the output formatter. - /// The encoding to use for the response. + /// The encoding to use for the response. /// A task that represents the asynchronous write operation. - public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding encoding) + public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) { if (context.Object == null) return; - var settings = new XmlWriterSettings { Async = true, Encoding = encoding }; + var settings = new XmlWriterSettings { Async = true, Encoding = selectedEncoding }; await using var writer = XmlWriter.Create(context.HttpContext.Response.Body, settings); var parameters = _parametersProvider.GetParameters(context.Object); diff --git a/Abblix.Oidc.Server.Mvc/Controllers/DiscoveryController.cs b/Abblix.Oidc.Server.Mvc/Controllers/DiscoveryController.cs index f252c772..cb1b5f51 100644 --- a/Abblix.Oidc.Server.Mvc/Controllers/DiscoveryController.cs +++ b/Abblix.Oidc.Server.Mvc/Controllers/DiscoveryController.cs @@ -34,7 +34,6 @@ using Abblix.Oidc.Server.Features.LogoutNotification; using Abblix.Oidc.Server.Features.UserInfo; using Abblix.Oidc.Server.Model; -using Abblix.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -163,8 +162,8 @@ public async Task> KeysAsync( if (!options.Value.EnabledEndpoints.HasFlag(OidcEndpoints.Keys)) return NotFound(); - var keys = await serviceKeysProvider.GetSigningKeys().ToListAsync(); - return Json(new JsonWebKeySet(keys.ToArray())); + var keys = await serviceKeysProvider.GetSigningKeys().ToArrayAsync(); + return Json(new JsonWebKeySet(keys)); } private static JsonResult Json(object response) => new( diff --git a/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationErrorFormatter.cs b/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationErrorFormatter.cs index 560be8de..16d3a5a1 100644 --- a/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationErrorFormatter.cs +++ b/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationErrorFormatter.cs @@ -22,6 +22,7 @@ using Abblix.Oidc.Server.Common.Constants; using Abblix.Oidc.Server.Endpoints.Authorization.Interfaces; +using Abblix.Oidc.Server.Features.Issuer; using Abblix.Oidc.Server.Model; using Abblix.Oidc.Server.Mvc.Binders; using Abblix.Utils; @@ -37,15 +38,23 @@ namespace Abblix.Oidc.Server.Mvc.Formatters; public class AuthorizationErrorFormatter { /// - /// Initializes a new instance of with the necessary parameter provider. + /// Initializes a new instance of with necessary dependencies for + /// response parameter handling. /// - /// The provider for extracting and formatting response parameters. - public AuthorizationErrorFormatter(IParametersProvider parametersProvider) + /// The provider for extracting and formatting response parameters, + /// which includes details like state and error descriptions. + /// The provider for the issuer URL, ensuring the 'iss' claim is correctly + /// included in error responses if applicable. + public AuthorizationErrorFormatter( + IParametersProvider parametersProvider, + IIssuerProvider issuerProvider) { _parametersProvider = parametersProvider; + _issuerProvider = issuerProvider; } private readonly IParametersProvider _parametersProvider; + protected readonly IIssuerProvider _issuerProvider; /// /// Asynchronously formats an authorization error response into an HTTP action result, @@ -54,12 +63,8 @@ public AuthorizationErrorFormatter(IParametersProvider parametersProvider) /// The original authorization request that led to the error. /// The authorization error to be formatted. /// A task that resolves to the formatted HTTP action result. - public Task FormatResponseAsync( - AuthorizationRequest request, - AuthorizationError error) - { - return Task.FromResult(FormatResponse(request, error)); - } + public Task FormatResponseAsync(AuthorizationRequest request, AuthorizationError error) + => Task.FromResult(FormatResponse(request, error)); /// /// Internally formats the authorization error response based on the request context and the error's properties, @@ -72,18 +77,18 @@ private ActionResult FormatResponse(AuthorizationRequest request, AuthorizationE { switch (error) { - case { RedirectUri: not null }: + case { RedirectUri: {} redirectUri }: var response = new AuthorizationResponse { + State = request.State, + Issuer = _issuerProvider.GetIssuer(), Error = error.Error, ErrorDescription = error.ErrorDescription, ErrorUri = error.ErrorUri, - - State = request.State, }; - return ToActionResult(response, error.ResponseMode, error.RedirectUri); + return ToActionResult(response, error.ResponseMode, redirectUri); default: return new BadRequestObjectResult(new ErrorResponse(error.Error, error.ErrorDescription)); diff --git a/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationResponseFormatter.cs b/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationResponseFormatter.cs index bf16322b..dd499c3e 100644 --- a/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationResponseFormatter.cs +++ b/Abblix.Oidc.Server.Mvc/Formatters/AuthorizationResponseFormatter.cs @@ -24,6 +24,7 @@ using Abblix.Oidc.Server.Common.Constants; using Abblix.Oidc.Server.Common.Exceptions; using Abblix.Oidc.Server.Endpoints.Authorization.Interfaces; +using Abblix.Oidc.Server.Features.Issuer; using Abblix.Oidc.Server.Features.SessionManagement; using Abblix.Oidc.Server.Features.Storages; using Abblix.Oidc.Server.Model; @@ -59,14 +60,15 @@ internal class AuthorizationResponseFormatter : AuthorizationErrorFormatter, IAu /// The service responsible for managing user sessions within the authorization process. /// /// Accessor to obtain the current HTTP context, facilitating access to request and response objects. - - public AuthorizationResponseFormatter( - IOptions options, + /// + /// Provides issuer information crucial for generating consistent authorization responses. + public AuthorizationResponseFormatter(IOptions options, IAuthorizationRequestStorage authorizationRequestStorage, IParametersProvider parametersProvider, ISessionManagementService sessionManagementService, - IHttpContextAccessor httpContextAccessor) - : base(parametersProvider) + IHttpContextAccessor httpContextAccessor, + IIssuerProvider issuerProvider) + : base(parametersProvider, issuerProvider) { _options = options; _authorizationRequestStorage = authorizationRequestStorage; @@ -111,24 +113,21 @@ 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 { State = response.Model.State, + Issuer = _issuerProvider.GetIssuer(), Scope = string.Join(' ', response.Model.Scope), - Code = success.Code, - TokenType = success.TokenType, AccessToken = success.AccessToken?.EncodedJwt, - IdToken = success.IdToken?.EncodedJwt, - SessionState = success.SessionState, }; - var actionResult = ToActionResult(modelResponse, success.ResponseMode, response.Model.RedirectUri); + var actionResult = ToActionResult(modelResponse, success.ResponseMode, redirectUri); if (_sessionManagementService.Enabled && success.SessionId.HasValue() && diff --git a/Abblix.Oidc.Server.Mvc/Formatters/PushedAuthorizationResponseFormatter.cs b/Abblix.Oidc.Server.Mvc/Formatters/PushedAuthorizationResponseFormatter.cs index e39c8280..9be11227 100644 --- a/Abblix.Oidc.Server.Mvc/Formatters/PushedAuthorizationResponseFormatter.cs +++ b/Abblix.Oidc.Server.Mvc/Formatters/PushedAuthorizationResponseFormatter.cs @@ -23,6 +23,7 @@ using Abblix.Oidc.Server.Common.Exceptions; using Abblix.Oidc.Server.Endpoints.Authorization.Interfaces; using Abblix.Oidc.Server.Endpoints.PushedAuthorization.Interfaces; +using Abblix.Oidc.Server.Features.Issuer; using Abblix.Oidc.Server.Model; using Abblix.Oidc.Server.Mvc.Binders; using Abblix.Oidc.Server.Mvc.Formatters.Interfaces; @@ -39,11 +40,12 @@ public class PushedAuthorizationResponseFormatter : AuthorizationErrorFormatter, { /// /// Initializes a new instance of the class - /// with the specified parameters provider. + /// with the specified parameters' provider. /// /// Provides access to parameters used in formatting the response. - public PushedAuthorizationResponseFormatter(IParametersProvider parametersProvider) - : base(parametersProvider) + /// Provides access to the issuer information used in responses. + public PushedAuthorizationResponseFormatter(IParametersProvider parametersProvider, IIssuerProvider issuerProvider) + : base(parametersProvider, issuerProvider) { } diff --git a/Abblix.Oidc.Server.Mvc/Formatters/TokenResponseFormatter.cs b/Abblix.Oidc.Server.Mvc/Formatters/TokenResponseFormatter.cs index 14d2bdae..eee3ac66 100644 --- a/Abblix.Oidc.Server.Mvc/Formatters/TokenResponseFormatter.cs +++ b/Abblix.Oidc.Server.Mvc/Formatters/TokenResponseFormatter.cs @@ -72,8 +72,8 @@ private static ActionResult FormatResponse(Endpoints.Token.Interf IdToken = success.IdToken?.EncodedJwt, }; - case TokenErrorResponse error: - return new BadRequestObjectResult(error); + case TokenErrorResponse { Error: var error, ErrorDescription: var description }: + return new BadRequestObjectResult(new ErrorResponse(error, description)); default: throw new ArgumentOutOfRangeException(nameof(response)); diff --git a/Abblix.Oidc.Server.Mvc/Model/AuthorizationRequest.cs b/Abblix.Oidc.Server.Mvc/Model/AuthorizationRequest.cs index eca2b4ed..9fa05202 100644 --- a/Abblix.Oidc.Server.Mvc/Model/AuthorizationRequest.cs +++ b/Abblix.Oidc.Server.Mvc/Model/AuthorizationRequest.cs @@ -42,7 +42,6 @@ public record AuthorizationRequest /// The specific scopes requested determine the access privileges granted. /// [BindProperty(SupportsGet = true, Name = Parameters.Scope)] - [AllowedValues(Scopes.OpenId, Scopes.Profile, Scopes.Email, Scopes.Phone, Scopes.Address, Scopes.OfflineAccess)] [ModelBinder(typeof(SpaceSeparatedValuesBinder))] public string[] Scope { get; init; } = Array.Empty(); @@ -195,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() { @@ -219,6 +218,6 @@ public record AuthorizationRequest CodeChallengeMethod = CodeChallengeMethod, IdTokenHint = IdTokenHint, ClaimsLocales = ClaimsLocales, - Resource = Resource, + Resources = Resources, }; } diff --git a/Abblix.Oidc.Server.Mvc/Model/AuthorizationResponse.cs b/Abblix.Oidc.Server.Mvc/Model/AuthorizationResponse.cs index ebd63b23..16dc5c38 100644 --- a/Abblix.Oidc.Server.Mvc/Model/AuthorizationResponse.cs +++ b/Abblix.Oidc.Server.Mvc/Model/AuthorizationResponse.cs @@ -26,6 +26,8 @@ namespace Abblix.Oidc.Server.Mvc.Model; /// /// Represents an authorization response containing the result of an authorization request. +/// This includes potential error details, codes, tokens, and additional related data used +/// in both success and error states of OAuth 2.0 and OpenID Connect authorization processes. /// public record AuthorizationResponse { @@ -46,35 +48,39 @@ private static class Parameters public const string Scope = "scope"; public const string SessionState = "session_state"; + + public const string Issuer = "iss"; } /// - /// The error code if the authorization request has failed. + /// The error code if the authorization request has failed, identifying the specific error that occurred. /// [JsonPropertyName(Parameters.Error)] public string? Error { init; get; } /// - /// The human-readable description of the error if the authorization request has failed. + /// A human-readable explanation of the error, providing detailed insight into why the authorization request failed. /// [JsonPropertyName(Parameters.ErrorDescription)] public string? ErrorDescription { init; get; } /// - /// The URI for more information about the error if the authorization request has failed. + /// A URI to a web resource that provides more information about the error, helping clients understand or + /// mitigate the issue. /// [JsonPropertyName(Parameters.ErrorUri)] public Uri? ErrorUri { init; get; } /// - /// The state parameter that was included in the initial authorization request. + /// The state parameter originally provided by the client in the authorization request, returned unaltered in + /// the response to maintain state between the client and the authorization server. /// [JsonPropertyName(Parameters.State)] public string? State { init; get; } /// - /// The authorization code generated by the authorization server. - /// This code is used in the authorization code flow of OAuth 2.0 to obtain an access token. + /// The authorization code generated by the authorization server. This code must be exchanged for an access token + /// at the token endpoint as part of the authorization code flow. /// [JsonPropertyName(Parameters.Code)] public string? Code { get; set; } @@ -114,4 +120,11 @@ private static class Parameters /// [JsonPropertyName(Parameters.SessionState)] public string? SessionState { get; set; } + + /// + /// The issuer of the response, which is typically the URL of the authorization server from + /// which the response originated. + /// + [JsonPropertyName(Parameters.Issuer)] + public string? Issuer { get; set; } } 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/EndSessionRequest.cs b/Abblix.Oidc.Server.Mvc/Model/EndSessionRequest.cs index ed66c59b..9b60795e 100644 --- a/Abblix.Oidc.Server.Mvc/Model/EndSessionRequest.cs +++ b/Abblix.Oidc.Server.Mvc/Model/EndSessionRequest.cs @@ -83,7 +83,7 @@ public record EndSessionRequest /// This is typically used to prevent accidental logouts and ensure intentional user actions. /// [BindProperty(SupportsGet = true, Name = Parameters.Confirmed)] - public bool Confirmed { get; set; } = false; + public bool? Confirmed { get; set; } = false; /// /// Maps the properties of this end session request to a object. 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.Tests/Abblix.Oidc.Server.Tests.csproj b/Abblix.Oidc.Server.Tests/Abblix.Oidc.Server.Tests.csproj index 8fd4e6c7..c8b1b580 100644 --- a/Abblix.Oidc.Server.Tests/Abblix.Oidc.Server.Tests.csproj +++ b/Abblix.Oidc.Server.Tests/Abblix.Oidc.Server.Tests.csproj @@ -7,9 +7,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Abblix.Oidc.Server.UnitTests/Abblix.Oidc.Server.UnitTests.csproj b/Abblix.Oidc.Server.UnitTests/Abblix.Oidc.Server.UnitTests.csproj index c632d31a..37e28aad 100644 --- a/Abblix.Oidc.Server.UnitTests/Abblix.Oidc.Server.UnitTests.csproj +++ b/Abblix.Oidc.Server.UnitTests/Abblix.Oidc.Server.UnitTests.csproj @@ -6,9 +6,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Abblix.Oidc.Server/Abblix.Oidc.Server.csproj b/Abblix.Oidc.Server/Abblix.Oidc.Server.csproj index cf29dcef..e783e19e 100644 --- a/Abblix.Oidc.Server/Abblix.Oidc.Server.csproj +++ b/Abblix.Oidc.Server/Abblix.Oidc.Server.csproj @@ -19,6 +19,8 @@ For detailed release notes, visit: https://github.com/Abblix/Oidc.Server/releases Abblix.png true + 1.1.0.0 + 1.1.0.0 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/Configuration/RefreshTokenOptions.cs b/Abblix.Oidc.Server/Common/Configuration/RefreshTokenOptions.cs index 6b5ed9ad..c383344c 100644 --- a/Abblix.Oidc.Server/Common/Configuration/RefreshTokenOptions.cs +++ b/Abblix.Oidc.Server/Common/Configuration/RefreshTokenOptions.cs @@ -30,12 +30,12 @@ public record struct RefreshTokenOptions() /// /// Sets the absolute period of expiration for refresh tokens. /// - public TimeSpan AbsoluteExpiresIn { get; init; } = TimeSpan.FromDays(30); + public TimeSpan AbsoluteExpiresIn { get; init; } = TimeSpan.FromHours(8); /// /// Sets the sliding (call-to-call relative) period of expiration for refresh tokens. /// - public TimeSpan? SlidingExpiresIn { get; init; } = TimeSpan.FromDays(15); + public TimeSpan? SlidingExpiresIn { get; init; } = TimeSpan.FromHours(1); /// /// Allows to reuse refresh tokens after the first usage. 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/Common/Implementation/OidcOptionsKeysProvider.cs b/Abblix.Oidc.Server/Common/Implementation/OidcOptionsKeysProvider.cs index b4ce2621..5e646f18 100644 --- a/Abblix.Oidc.Server/Common/Implementation/OidcOptionsKeysProvider.cs +++ b/Abblix.Oidc.Server/Common/Implementation/OidcOptionsKeysProvider.cs @@ -23,7 +23,6 @@ using Abblix.Jwt; using Abblix.Oidc.Server.Common.Configuration; using Abblix.Oidc.Server.Common.Interfaces; -using Abblix.Utils; using Microsoft.Extensions.Options; @@ -57,7 +56,7 @@ public IAsyncEnumerable GetEncryptionKeys(bool includePrivateKeys) from jwk in _options.Value.EncryptionKeys select jwk.Sanitize(includePrivateKeys); - return jsonWebKeys.AsAsync(); + return jsonWebKeys.ToAsyncEnumerable(); } /// @@ -71,6 +70,6 @@ public IAsyncEnumerable GetSigningKeys(bool includePrivateKeys) from jwk in _options.Value.SigningKeys select jwk.Sanitize(includePrivateKeys); - return jsonWebKeys.AsAsync(); + return jsonWebKeys.ToAsyncEnumerable(); } } diff --git a/Abblix.Oidc.Server/Common/JsonWebKeyExtensions.cs b/Abblix.Oidc.Server/Common/JsonWebKeyExtensions.cs index 379c8978..ce7d36ac 100644 --- a/Abblix.Oidc.Server/Common/JsonWebKeyExtensions.cs +++ b/Abblix.Oidc.Server/Common/JsonWebKeyExtensions.cs @@ -37,14 +37,14 @@ public static class JsonWebKeyExtensions /// The algorithm to match. Returns null if is provided. /// The first with the specified algorithm or null if not found. - public static Task FirstByAlgorithmAsync(this IAsyncEnumerable credentials, string? alg) + public static async Task FirstByAlgorithmAsync(this IAsyncEnumerable credentials, string? alg) { if (alg == SigningAlgorithms.None) - return Task.FromResult(null); + return null; if (alg.HasValue()) - credentials = credentials.WhereAsync(key => key.Algorithm == alg); + credentials = credentials.Where(key => key.Algorithm == alg); - return credentials.FirstOrDefaultAsync(); + return await credentials.FirstOrDefaultAsync(); } } diff --git a/Abblix.Oidc.Server/Endpoints/Authorization/AuthorizationRequestProcessor.cs b/Abblix.Oidc.Server/Endpoints/Authorization/AuthorizationRequestProcessor.cs index aa84dda4..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,33 +188,28 @@ 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; } - private Task> GetAvailableAuthSessionsAsync(AuthorizationRequest model) + private ValueTask> GetAvailableAuthSessionsAsync(AuthorizationRequest model) { var authSessions = _authSessionService.GetAvailableAuthSessions(); @@ -214,14 +218,14 @@ private Task> GetAvailableAuthSessionsAsync(AuthorizationReque // skip all sessions older than max_age value var minAuthenticationTime = _clock.GetUtcNow() - model.MaxAge; authSessions = authSessions - .WhereAsync(session => minAuthenticationTime < session.AuthenticationTime); + .Where(session => minAuthenticationTime < session.AuthenticationTime); } var acrValues = model.AcrValues; if (acrValues is { Length: > 0 }) { authSessions = authSessions - .WhereAsync(session => session.AuthContextClassRef.HasValue() && + .Where(session => session.AuthContextClassRef.HasValue() && acrValues.Contains(session.AuthContextClassRef)); } 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/SuccessfullyAuthenticated.cs b/Abblix.Oidc.Server/Endpoints/Authorization/Interfaces/SuccessfullyAuthenticated.cs index a0f98231..a84ef07f 100644 --- a/Abblix.Oidc.Server/Endpoints/Authorization/Interfaces/SuccessfullyAuthenticated.cs +++ b/Abblix.Oidc.Server/Endpoints/Authorization/Interfaces/SuccessfullyAuthenticated.cs @@ -34,7 +34,11 @@ namespace Abblix.Oidc.Server.Endpoints.Authorization.Interfaces; /// An optional session identifier that may be used for session management. /// Identifiers of the clients that are affected by or related to this authentication /// process. -public record SuccessfullyAuthenticated(AuthorizationRequest Model, string ResponseMode, string? SessionId, ICollection AffectedClientIds) +public record SuccessfullyAuthenticated( + AuthorizationRequest Model, + string ResponseMode, + string? SessionId, + ICollection AffectedClientIds) : AuthorizationResponse(Model) { /// 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/ClientValidator.cs b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ClientValidator.cs index 85ae0af4..1df8d2d9 100644 --- a/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ClientValidator.cs +++ b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ClientValidator.cs @@ -73,7 +73,7 @@ public ClientValidator( var clientInfo = await _clientInfoProvider.TryFindClientAsync(clientId.NotNull(nameof(clientId))).WithLicenseCheck(); if (clientInfo == null) { - _logger.LogWarning("The client with id {ClientId} was not found", clientId); + _logger.LogWarning("The client with id {ClientId} was not found", new Sanitized(clientId)); return context.InvalidRequest("The client is not authorized"); } 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/PkceValidator.cs b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/PkceValidator.cs index 03224c39..a4218893 100644 --- a/Abblix.Oidc.Server/Endpoints/Authorization/Validation/PkceValidator.cs +++ b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/PkceValidator.cs @@ -29,7 +29,7 @@ namespace Abblix.Oidc.Server.Endpoints.Authorization.Validation; /// /// Validates the PKCE (Proof Key for Code Exchange) parameters in an authorization request. -/// PKCE adds an additional layer of security for the OAuth 2.0 authorization code flow, +/// PKCE adds another layer of security for the OAuth 2.0 authorization code flow, /// particularly in public clients. It ensures that the authorization request conforms to /// the standards defined in RFC 7636 (specifically, see Section 4.3 for client validation requirements). /// @@ -55,7 +55,7 @@ public class PkceValidator : SyncAuthorizationContextValidatorBase return context.InvalidRequest("The client is not allowed PKCE plain method"); } } - else if (context.ClientInfo.PkceRequired) + else if (context.ClientInfo.PkceRequired ?? true) { return context.InvalidRequest("The client requires PKCE code challenge"); } 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 8c7acb39..0a132501 100644 --- a/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ScopeValidator.cs +++ b/Abblix.Oidc.Server/Endpoints/Authorization/Validation/ScopeValidator.cs @@ -20,11 +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; @@ -36,28 +34,44 @@ 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. /// - /// An if the scope validation fails, + /// An if the scope validation fails, /// or null if the scopes in the request are valid. /// - protected override AuthorizationRequestValidationError? Validate(AuthorizationValidationContext context) + 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) - 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); } - return null; - } + context.Scope = scopeDefinitions; + return null; + } } diff --git a/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/RegisterClientRequestProcessor.cs b/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/RegisterClientRequestProcessor.cs index 7c702f2a..6228834f 100644 --- a/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/RegisterClientRequestProcessor.cs +++ b/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/RegisterClientRequestProcessor.cs @@ -186,14 +186,14 @@ private ClientInfo ToClientInfo( { clientInfo.BackChannelLogout = new BackChannelLogoutOptions( model.BackChannelLogoutUri, - model.BackChannelLogoutSessionRequired); + model.BackChannelLogoutSessionRequired ?? false); } if (model.FrontChannelLogoutUri != null) { clientInfo.FrontChannelLogout = new FrontChannelLogoutOptions( model.FrontChannelLogoutUri, - model.FrontChannelLogoutSessionRequired); + model.FrontChannelLogoutSessionRequired ?? false); } return clientInfo; diff --git a/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/Validation/ClientIdValidator.cs b/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/Validation/ClientIdValidator.cs index 2eaced85..967b10bb 100644 --- a/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/Validation/ClientIdValidator.cs +++ b/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/Validation/ClientIdValidator.cs @@ -62,7 +62,7 @@ public ClientIdValidator( var clientInfo = await _clientInfoProvider.TryFindClientAsync(clientId).WithLicenseCheck(); if (clientInfo != null) { - _logger.LogWarning("The client with id {ClientId} is already registered", clientId); + _logger.LogWarning("The client with id {ClientId} is already registered", new Sanitized(clientId)); return ErrorFactory.InvalidClientMetadata($"The client with id={clientId} is already registered"); } } diff --git a/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/Validation/SubjectTypeValidator.cs b/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/Validation/SubjectTypeValidator.cs index 9b50ec03..e93bd68b 100644 --- a/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/Validation/SubjectTypeValidator.cs +++ b/Abblix.Oidc.Server/Endpoints/DynamicClientManagement/Validation/SubjectTypeValidator.cs @@ -23,6 +23,7 @@ using System.Net.Http.Json; using Abblix.Oidc.Server.Common.Constants; using Abblix.Oidc.Server.Endpoints.DynamicClientManagement.Interfaces; +using Abblix.Utils; using Microsoft.Extensions.Logging; using static Abblix.Oidc.Server.Model.ClientRegistrationRequest; @@ -88,7 +89,7 @@ public SubjectTypeValidator( catch (Exception ex) { _logger.LogWarning(ex, "Unable to receive content of {SectorIdentifierUri}", - sectorIdentifierUri); + new Sanitized(sectorIdentifierUri)); return ErrorFactory.InvalidClientMetadata( $"Unable to receive content of {Parameters.SectorIdentifierUri}"); } @@ -109,7 +110,7 @@ public SubjectTypeValidator( if (missingUris.Length > 0) { _logger.LogWarning("The following URIs are present in the {SectorIdentifierUri}, but missing from the Redirect URIs: {@MissingUris}", - sectorIdentifierUri, + new Sanitized(sectorIdentifierUri), missingUris); return ErrorFactory.InvalidClientMetadata( diff --git a/Abblix.Oidc.Server/Endpoints/EndSession/Validation/ClientValidator.cs b/Abblix.Oidc.Server/Endpoints/EndSession/Validation/ClientValidator.cs index 6a0b411f..0ab8f964 100644 --- a/Abblix.Oidc.Server/Endpoints/EndSession/Validation/ClientValidator.cs +++ b/Abblix.Oidc.Server/Endpoints/EndSession/Validation/ClientValidator.cs @@ -67,7 +67,7 @@ public ClientValidator( var clientInfo = await _clientInfoProvider.TryFindClientAsync(context.ClientId).WithLicenseCheck(); if (clientInfo == null) { - _logger.LogWarning("The client with id {ClientId} was not found", context.ClientId); + _logger.LogWarning("The client with id {ClientId} was not found", new Sanitized(context.ClientId)); return new EndSessionRequestValidationError( ErrorCodes.UnauthorizedClient, "The client is not authorized"); diff --git a/Abblix.Oidc.Server/Endpoints/EndSession/Validation/ConfirmationValidator.cs b/Abblix.Oidc.Server/Endpoints/EndSession/Validation/ConfirmationValidator.cs index 84e47975..aa8f76b0 100644 --- a/Abblix.Oidc.Server/Endpoints/EndSession/Validation/ConfirmationValidator.cs +++ b/Abblix.Oidc.Server/Endpoints/EndSession/Validation/ConfirmationValidator.cs @@ -44,7 +44,7 @@ public class ConfirmationValidator: IEndSessionContextValidator { var request = context.Request; - if (!request.Confirmed && !request.IdTokenHint.HasValue()) + if (request.Confirmed != true && !request.IdTokenHint.HasValue()) { return new EndSessionRequestValidationError( ErrorCodes.ConfirmationRequired, diff --git a/Abblix.Oidc.Server/Endpoints/EndSession/Validation/PostLogoutRedirectUrisValidator.cs b/Abblix.Oidc.Server/Endpoints/EndSession/Validation/PostLogoutRedirectUrisValidator.cs index 017b9d83..99c33e20 100644 --- a/Abblix.Oidc.Server/Endpoints/EndSession/Validation/PostLogoutRedirectUrisValidator.cs +++ b/Abblix.Oidc.Server/Endpoints/EndSession/Validation/PostLogoutRedirectUrisValidator.cs @@ -23,6 +23,7 @@ using Abblix.Oidc.Server.Common.Constants; using Abblix.Oidc.Server.Endpoints.EndSession.Interfaces; using Abblix.Oidc.Server.Features.UriValidation; +using Abblix.Utils; using Microsoft.Extensions.Logging; using static Abblix.Oidc.Server.Model.EndSessionRequest; @@ -75,7 +76,7 @@ public PostLogoutRedirectUrisValidator(ILogger return null; _logger.LogWarning("The post-logout redirect URI {RedirectUri} is invalid for client with id {ClientId}", - redirectUri, + new Sanitized(redirectUri), context.ClientInfo.ClientId); return new EndSessionRequestValidationError( 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/AuthorizationCodeReusePreventingDecorator.cs b/Abblix.Oidc.Server/Endpoints/Token/AuthorizationCodeReusePreventingDecorator.cs index a33f6de7..0c4662b8 100644 --- a/Abblix.Oidc.Server/Endpoints/Token/AuthorizationCodeReusePreventingDecorator.cs +++ b/Abblix.Oidc.Server/Endpoints/Token/AuthorizationCodeReusePreventingDecorator.cs @@ -99,13 +99,10 @@ public async Task ProcessAsync(ValidTokenRequest request) var response = await _processor.ProcessAsync(request); // Register issued tokens as part of the authorization code grant - if (response is TokenIssuedResponse - { - AccessToken: var accessToken, - RefreshToken: var refreshToken - }) + if (response is TokenIssuedResponse { AccessToken: var accessToken, RefreshToken: var refreshToken }) { var issuedTokensList = new List(); + void TryRegisterToken(JsonWebToken? token) { if (token is { Payload: { JwtId: { } jwtId, ExpiresAt: { } expiresAt }}) diff --git a/Abblix.Oidc.Server/Endpoints/Token/Grants/RefreshTokenGrantHandler.cs b/Abblix.Oidc.Server/Endpoints/Token/Grants/RefreshTokenGrantHandler.cs index 281f16d6..3fa12fa8 100644 --- a/Abblix.Oidc.Server/Endpoints/Token/Grants/RefreshTokenGrantHandler.cs +++ b/Abblix.Oidc.Server/Endpoints/Token/Grants/RefreshTokenGrantHandler.cs @@ -28,7 +28,6 @@ using Abblix.Oidc.Server.Features.ClientInformation; using Abblix.Oidc.Server.Features.Storages; using Abblix.Oidc.Server.Features.Tokens; -using Abblix.Oidc.Server.Features.Tokens.Revocation; using Abblix.Oidc.Server.Features.Tokens.Validation; using Abblix.Oidc.Server.Model; 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 cc929639..0ce7d8b6 100644 --- a/Abblix.Oidc.Server/Endpoints/Token/TokenRequestProcessor.cs +++ b/Abblix.Oidc.Server/Endpoints/Token/TokenRequestProcessor.cs @@ -20,7 +20,6 @@ // CONTACT: For license inquiries or permissions, contact Abblix LLP at // info@abblix.com -using Abblix.Jwt; using Abblix.Oidc.Server.Common; using Abblix.Oidc.Server.Common.Constants; using Abblix.Oidc.Server.Endpoints.Token.Interfaces; @@ -31,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/ClientInformation/ClientInfo.cs b/Abblix.Oidc.Server/Features/ClientInformation/ClientInfo.cs index fc54f297..525f84a9 100644 --- a/Abblix.Oidc.Server/Features/ClientInformation/ClientInfo.cs +++ b/Abblix.Oidc.Server/Features/ClientInformation/ClientInfo.cs @@ -73,7 +73,7 @@ public record ClientInfo(string ClientId) /// Indicates whether the client is to use Proof Key for Code Exchange (PKCE) in the authorization code flow, /// enhancing security for public clients. /// - public bool PkceRequired { get; set; } = false; + public bool? PkceRequired { get; set; } = true; /// /// Indicates if the client is allowed to use the "plain" method for PKCE. @@ -85,13 +85,13 @@ public record ClientInfo(string ClientId) /// The validity period of an authorization code issued to this client. /// Shorter durations are recommended for higher security. /// - public TimeSpan AuthorizationCodeExpiresIn { get; set; } = TimeSpan.FromMinutes(5); + public TimeSpan AuthorizationCodeExpiresIn { get; set; } = TimeSpan.FromMinutes(1); /// /// Specifies the lifetime of access tokens issued to this client. /// Shorter access token lifetimes reduce the risk of token leakage. /// - public TimeSpan AccessTokenExpiresIn { get; set; } = TimeSpan.FromHours(1); + public TimeSpan AccessTokenExpiresIn { get; set; } = TimeSpan.FromMinutes(10); /// /// Configures the behavior and properties of refresh tokens issued to this client, @@ -131,7 +131,7 @@ public record ClientInfo(string ClientId) /// /// Allows the client to request tokens that enable access to the user's resources while they are offline. /// - public bool OfflineAccessAllowed { get; set; } = false; + public bool? OfflineAccessAllowed { get; set; } = false; /// /// The set of JSON Web Keys used by the client, typically for signing request objects and decrypting diff --git a/Abblix.Oidc.Server/Features/ClientInformation/ClientKeysProvider.cs b/Abblix.Oidc.Server/Features/ClientInformation/ClientKeysProvider.cs index ef3385af..dd8374cc 100644 --- a/Abblix.Oidc.Server/Features/ClientInformation/ClientKeysProvider.cs +++ b/Abblix.Oidc.Server/Features/ClientInformation/ClientKeysProvider.cs @@ -22,7 +22,6 @@ using System.Net.Http.Json; using Abblix.Jwt; -using Abblix.Utils; using Microsoft.Extensions.Logging; namespace Abblix.Oidc.Server.Features.ClientInformation; @@ -56,7 +55,7 @@ public ClientKeysProvider( /// A collection of encryption keys as an asynchronous enumerable. public IAsyncEnumerable GetEncryptionKeys(ClientInfo clientInfo) { - return GetKeys(clientInfo).WhereAsync(key => key.Usage == PublicKeyUsages.Encryption); + return GetKeys(clientInfo).Where(key => key.Usage == PublicKeyUsages.Encryption); } /// @@ -66,7 +65,7 @@ public IAsyncEnumerable GetEncryptionKeys(ClientInfo clientInfo) /// A collection of signing keys as an asynchronous enumerable. public IAsyncEnumerable GetSigningKeys(ClientInfo clientInfo) { - return GetKeys(clientInfo).WhereAsync(key => key.Usage == PublicKeyUsages.Signature); + return GetKeys(clientInfo).Where(key => key.Usage == PublicKeyUsages.Signature); } /// 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/Licensing/OptionsLicenseJwtProvider.cs b/Abblix.Oidc.Server/Features/Licensing/OptionsLicenseJwtProvider.cs index 0152bb41..e706097d 100644 --- a/Abblix.Oidc.Server/Features/Licensing/OptionsLicenseJwtProvider.cs +++ b/Abblix.Oidc.Server/Features/Licensing/OptionsLicenseJwtProvider.cs @@ -21,7 +21,6 @@ // info@abblix.com using Abblix.Oidc.Server.Common.Configuration; -using Abblix.Utils; using Microsoft.Extensions.Options; namespace Abblix.Oidc.Server.Features.Licensing; @@ -47,6 +46,6 @@ public OptionsLicenseJwtProvider(IOptions options) public IAsyncEnumerable? GetLicenseJwtAsync() { var licenseJwt = _options.Value.LicenseJwt; - return licenseJwt != null ? new[] { licenseJwt }.AsAsync() : null; + return licenseJwt != null ? new[] { licenseJwt }.ToAsyncEnumerable() : null; } } diff --git a/Abblix.Oidc.Server/Features/Licensing/StaticLicenseJwtProvider.cs b/Abblix.Oidc.Server/Features/Licensing/StaticLicenseJwtProvider.cs index 3f2311b7..abf2ec1a 100644 --- a/Abblix.Oidc.Server/Features/Licensing/StaticLicenseJwtProvider.cs +++ b/Abblix.Oidc.Server/Features/Licensing/StaticLicenseJwtProvider.cs @@ -20,8 +20,6 @@ // CONTACT: For license inquiries or permissions, contact Abblix LLP at // info@abblix.com -using Abblix.Utils; - namespace Abblix.Oidc.Server.Features.Licensing; /// @@ -53,6 +51,6 @@ public StaticLicenseJwtProvider(string licenseJwt) /// A task that represents the asynchronous operation, resulting in the license JWT string. public IAsyncEnumerable? GetLicenseJwtAsync() { - return new[] { _licenseJwt }.AsAsync(); + return new[] { _licenseJwt }.ToAsyncEnumerable(); } } 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/SessionManagement/Resources/checkSession.html b/Abblix.Oidc.Server/Features/SessionManagement/Resources/checkSession.html index 84f323c6..119583a1 100644 --- a/Abblix.Oidc.Server/Features/SessionManagement/Resources/checkSession.html +++ b/Abblix.Oidc.Server/Features/SessionManagement/Resources/checkSession.html @@ -1,3 +1,4 @@ +