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