Skip to content

Commit

Permalink
Resource Indicators support (#12)
Browse files Browse the repository at this point in the history
* Added parameter iss return from Authorization endpoint
* Added Resource Indicators support
* Bump-up nuget refs
* Fix some suggestions from Sonar
  • Loading branch information
kirill-abblix committed Jul 9, 2024
1 parent 006e08d commit fade652
Show file tree
Hide file tree
Showing 58 changed files with 1,806 additions and 218 deletions.
2 changes: 1 addition & 1 deletion Abblix.Jwt/Abblix.Jwt.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.5.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.2" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion Abblix.Oidc.Server.Mvc/Abblix.Oidc.Server.Mvc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using Microsoft.AspNetCore.Http;

namespace Abblix.Oidc.Server.Mvc.ActionResults;

public static class CookieOptionsExtensions
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ internal class AuthorizationResponseFormatter : AuthorizationErrorFormatter, IAu
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
{
Expand All @@ -127,7 +127,7 @@ internal class AuthorizationResponseFormatter : AuthorizationErrorFormatter, IAu
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() &&
Expand Down
4 changes: 2 additions & 2 deletions Abblix.Oidc.Server.Mvc/Model/AuthorizationRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ public record AuthorizationRequest
/// resource.
/// </summary>
[BindProperty(SupportsGet = true, Name = Parameters.Resource)]
public Uri[]? Resource { get; set; }
public Uri[]? Resources { get; set; }

public Core.AuthorizationRequest Map() => new()
{
Expand All @@ -218,6 +218,6 @@ public Core.AuthorizationRequest Map() => new()
CodeChallengeMethod = CodeChallengeMethod,
IdTokenHint = IdTokenHint,
ClaimsLocales = ClaimsLocales,
Resource = Resource,
Resources = Resources,
};
}
6 changes: 3 additions & 3 deletions Abblix.Oidc.Server.Mvc/Model/ClientRegistrationRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ public record ClientRegistrationRequest
/// PKCE enhances the security of the OAuth authorization code flow, particularly for public clients.
/// </summary>
[JsonPropertyName(Parameters.PkceRequired)]
public bool PkceRequired { get; set; }
public bool? PkceRequired { get; set; } = false;

/// <summary>
/// Indicates whether this client is allowed to request offline access.
Expand All @@ -303,7 +303,7 @@ public record ClientRegistrationRequest
/// This is relevant for scenarios where the client needs to be notified when the user logs out.
/// </summary>
[JsonPropertyName(Parameters.BackChannelLogoutSessionRequired)]
public bool BackChannelLogoutSessionRequired { get; set; }
public bool? BackChannelLogoutSessionRequired { get; set; } = false;

/// <summary>
/// URI used for back-channel logout. This URI is called by the OpenID Provider to initiate a logout for the client.
Expand All @@ -324,7 +324,7 @@ public record ClientRegistrationRequest
/// This is used to manage user sessions in scenarios involving multiple clients.
/// </summary>
[JsonPropertyName(Parameters.FrontChannelLogoutSessionRequired)]
public bool FrontChannelLogoutSessionRequired { get; set; }
public bool? FrontChannelLogoutSessionRequired { get; set; } = false;

/// <summary>
/// Array of URIs to which the OP will redirect the user's user agent after logging out.
Expand Down
6 changes: 3 additions & 3 deletions Abblix.Oidc.Server.Mvc/Model/TokenRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
[BindProperty(SupportsGet = true, Name = Parameters.Username)]
[BindProperty(Name = Parameters.Username)]
public string? UserName { get; set; }

/// <summary>
/// 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.
/// </summary>
[BindProperty(SupportsGet = true, Name = Parameters.Password)]
[BindProperty(Name = Parameters.Password)]
public string? Password { get; set; }

/// <summary>
Expand All @@ -125,7 +125,7 @@ public Core.TokenRequest Map()
GrantType = GrantType,
Code = Code,
Password = Password,
Resource = Resource,
Resources = Resource,
Scope = Scope,
RefreshToken = RefreshToken,
RedirectUri = RedirectUri,
Expand Down
7 changes: 7 additions & 0 deletions Abblix.Oidc.Server/Common/AuthorizationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public string? CodeChallengeMethod { get; init; }

/// <summary>
/// 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.
/// </summary>
public Uri[]? Resources { get; init; }
}
17 changes: 16 additions & 1 deletion Abblix.Oidc.Server/Common/AuthorizationContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -70,9 +73,21 @@ public static void ApplyTo(this AuthorizationContext context, JsonWebTokenPayloa
/// </remarks>
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<Uri>()
.ToArray();

return new AuthorizationContext(
payload.ClientId.NotNull(nameof(payload.ClientId)),
payload.Scope.NotNull(nameof(payload.Scope)).ToArray(),
payload[JwtClaimTypes.RequestedClaims].Deserialize<RequestedClaims>(JsonSerializerOptions));
payload[JwtClaimTypes.RequestedClaims].Deserialize<RequestedClaims>(JsonSerializerOptions))
{
Nonce = payload.Nonce,
Resources = resources,
};
}
}
36 changes: 29 additions & 7 deletions Abblix.Oidc.Server/Common/Configuration/OidcOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
// [email protected]

using Abblix.Jwt;
using Abblix.Oidc.Server.Common.Constants;
using Abblix.Oidc.Server.Features.ClientInformation;
using Abblix.Oidc.Server.Model;

Expand All @@ -41,7 +42,7 @@ public record OidcOptions

/// <summary>
/// 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.
/// </summary>
public string? Issuer { get; set; }
Expand Down Expand Up @@ -100,7 +101,7 @@ public record OidcOptions
/// <summary>
/// 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.
/// </summary>
Expand Down Expand Up @@ -129,9 +130,9 @@ public record OidcOptions
/// <summary>
/// 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.
/// </summary>
public IReadOnlyCollection<JsonWebKey> EncryptionKeys { get; set; } = Array.Empty<JsonWebKey>();

Expand All @@ -146,12 +147,33 @@ public record OidcOptions
/// <summary>
/// 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.
/// </summary>
public string? LicenseJwt { get; set; }

/// <summary>
/// The standard length of the authorization code generated by the server.
/// </summary>
public int AuthorizationCodeLength { get; set; } = 64;

/// <summary>
/// The standard length of the request URI generated by the server for Pushed Authorization Requests (PAR).
/// </summary>
public int RequestUriLength { get; set; } = 64;

/// <summary>
/// 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.
/// </summary>
public ScopeDefinition[]? Scopes { get; set; }

/// <summary>
/// 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.
/// </summary>
public ResourceDefinition[]? Resources { get; set; }
}
33 changes: 33 additions & 0 deletions Abblix.Oidc.Server/Common/Constants/ResourceDefinition.cs
Original file line number Diff line number Diff line change
@@ -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
// [email protected]

namespace Abblix.Oidc.Server.Common.Constants;

/// <summary>
/// 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.
/// </summary>
/// <param name="Resource">The identifier for the resource, often a unique name or URL representing the resource.</param>
/// <param name="Scopes">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.</param>
public record ResourceDefinition(Uri Resource, params ScopeDefinition[] Scopes);
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,21 @@ public class AuthorizationRequestProcessor : IAuthorizationRequestProcessor
/// and identity token services, and time-related functionality.
/// </summary>
/// <param name="authSessionService">Service for handling user authentication.</param>
/// <param name="consentService">Service for managing user consent.</param>
/// <param name="consentsProvider">Service for managing user consent.</param>
/// <param name="authorizationCodeService">Service for generating and managing authorization codes.</param>
/// <param name="accessTokenService">Service for creating access tokens.</param>
/// <param name="identityTokenService">Service for generating identity tokens.</param>
/// <param name="clock">Service for managing time-related operations.</param>
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;
Expand All @@ -75,7 +75,7 @@ public class AuthorizationRequestProcessor : IAuthorizationRequestProcessor
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;

Expand Down Expand Up @@ -131,7 +131,8 @@ public async Task<AuthorizationResponse> 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)
{
Expand All @@ -143,16 +144,24 @@ public async Task<AuthorizationResponse> 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))
Expand All @@ -179,27 +188,22 @@ public async Task<AuthorizationResponse> 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;
Expand Down
Loading

0 comments on commit fade652

Please sign in to comment.