Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource Indicators support #12

Merged
merged 15 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ public async Task<ActionResult> 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
{
Expand All @@ -127,7 +127,7 @@ public async Task<ActionResult> 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() &&
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 record AuthorizationRequest
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 @@
// info@abblix.com

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
// info@abblix.com

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 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;

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,20 @@ 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 = Array.ConvertAll(grantedConsents.Scopes, scope => scope.Scope);
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 +184,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