github openiddict/openiddict-core 7.0.0-preview.4

latest release: 7.0.0
pre-release2 months ago

This release introduces the following changes:

var result = await _service.AuthenticateWithTokenExchangeAsync(new()
{
    ActorToken = actorToken,
    ActorTokenType = actorTokenType,
    CancellationToken = stoppingToken,
    ProviderName = "Local",
    RequestedTokenType = TokenTypeIdentifiers.AccessToken,
    SubjectToken = subjectToken,
    SubjectTokenType = subjectTokenType
});

var token = result.IssuedToken;
var type = result.IssuedTokenType;
[HttpPost("~/connect/token"), IgnoreAntiforgeryToken, Produces("application/json")]
public async Task<IActionResult> Exchange()
{
    var request = HttpContext.GetOpenIddictServerRequest() ??
        throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

    if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
    {
        // ...
    }

    else if (request.IsTokenExchangeGrantType())
    {
        // Retrieve the claims principal stored in the subject token.
        //
        // Note: the principal may not represent a user (e.g if the token was issued during a client credentials token
        // request and represents a client application): developers are strongly encouraged to ensure that the user
        // and client identifiers are randomly generated so that a malicious client cannot impersonate a legit user.
        //
        // See https://datatracker.ietf.org/doc/html/rfc9068#SecurityConsiderations for more information.
        var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

        // If available, retrieve the claims principal stored in the actor token.
        var actor = result.Properties?.GetParameter<ClaimsPrincipal>(OpenIddictServerAspNetCoreConstants.Properties.ActorTokenPrincipal);

        // Retrieve the user profile corresponding to the subject token.
        var user = await _userManager.FindByIdAsync(result.Principal!.GetClaim(Claims.Subject)!);
        if (user is null)
        {
            return Forbid(
                authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                properties: new AuthenticationProperties(new Dictionary<string, string?>
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
                }));
        }

        // Ensure the user is still allowed to sign in.
        if (!await _signInManager.CanSignInAsync(user))
        {
            return Forbid(
                authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                properties: new AuthenticationProperties(new Dictionary<string, string?>
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
                }));
        }

        // Note: whether the identity represents a delegated or impersonated access (or any other
        // model) is entirely up to the implementer: to support all scenarios, OpenIddict doesn't
        // enforce any specific constraint on the identity used for the sign-in operation and only
        // requires that the standard "act" and "may_act" claims be valid JSON objects if present.

        var identity = new ClaimsIdentity(
            authenticationType: TokenValidationParameters.DefaultAuthenticationType,
            nameType: Claims.Name,
            roleType: Claims.Role);

        // Add the claims that will be persisted in the issued token.
        identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user))
                .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user))
                .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user))
                .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user))
                .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]);

        // Note: IdentityModel doesn't support serializing ClaimsIdentity.Actor to the
        // standard "act" claim yet, which requires adding the "act" claim manually.
        //
        // For more information, see
        // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3219.
        if (!string.IsNullOrEmpty(actor?.GetClaim(Claims.Subject)) &&
            !string.Equals(identity.GetClaim(Claims.Subject), actor.GetClaim(Claims.Subject), StringComparison.Ordinal))
        {
            identity.SetClaim(Claims.Actor, new JsonObject
            {
                [Claims.Subject] = actor.GetClaim(Claims.Subject)
            });
        }

        // Note: in this sample, the granted scopes match the requested scope
        // but you may want to allow the user to uncheck specific scopes.
        // For that, simply restrict the list of scopes before calling SetScopes.
        identity.SetScopes(request.GetScopes());
        identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
        identity.SetDestinations(GetDestinations);

        // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
        return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    }

    throw new InvalidOperationException("The specified grant type is not supported.");
}

Tip

See #1249 (comment) and #2335 (comment) for more information on the key aspects of the specification and its implementation in OpenIddict.

  • OpenIddict 7.0 preview 4 implements the Updates to Audience Values for OAuth 2.0 Authorization Servers draft: while it hasn't been officially adopted yet, it fixes a vulnerability affecting the standard private_jwt_key client authentication method supported. Since this draft introduces important breaking changes in multiple OAuth 2.0 and OpenID Connect specifications to mitigate the vulnerability, implementing it proactively in OpenIddict 7.0 is a better option than having to the next major version to address this issue for good.

Warning

Due to this change, the following cases won't be supported by OpenIddict 7.0:

  • The application authenticates using the OpenIddict client with a third-party server that requires the use of the token_endpoint as the audience of client assertions, even for endpoints other than the token endpoint (e.g., the "pushed authorization endpoint" or the "introspection endpoint").

  • The application authenticates using the OpenIddict client with a third-party server that does not support the new client-authentication+jwt JSON Web Token type defined by the specification for client assertions.

  • The application uses the OpenIddict server and allows clients to authenticate with client assertions that use token_endpoint instead of issuer as the audience.

  • The application uses the OpenIddict server and allows clients to authenticate with client assertions that do not use the new client-authentication+jwt JSON Web Token type.

The OpenIddict client, server and validation stacks have all been updated to support the new requirements introduced by this specification and it is expected that other implementations will make similar changes in the future. OpenIddict 7.0 preview 4 doesn't currently allow reverting to the unsafe behavior, but I'll monitor the situation to determine whether opt-in compatibility quirks should be introduced in a future version to support unsafe client assertions.

  • As part of the OAuth 2.0 Token Exchange support, the OpenIddict server now automatically validates the audience and resource parameters used in token exchange token requests and the resource parameters present in the authorization and pushed authorization requests. To ensure OpenIddict doesn't reject requests that use these parameters, the allowed audiences and resources must be registered in the server options:
services.AddOpenIddict()
    .AddServer(options =>
    {
        options.RegisterAudiences("financial_api");
        options.RegisterResources("https://fabrikam.com/financial_api");
    });

Tip

Alternatively, audiences and resources validation can be disabled, which can be useful when implementing dynamic audiences/resources:

services.AddOpenIddict()
    .AddServer(options =>
    {
        options.DisableAudienceValidation();
        options.DisableResourceValidation();
    });
  • In the same vein, the OpenIddict server stack now supports audience and resource application permissions, that work exactly like scopes:
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
    // ...

    Permissions =
    {
        Permissions.Prefixes.Audience + "financial_api",
        Permissions.Prefixes.Resource + "https://fabrikam.com/financial_api"
    }
});

Tip

If necessary, both audience and resource permissions can be ignored, which can be useful if custom validation - e.g implemented directly in the authorization controller, to support dynamic values - is preferred.

services.AddOpenIddict()
    .AddServer(options =>
    {
        options.IgnoreAudiencePermissions();
        options.IgnoreResourcePermissions();
    });
  • The maximum length of the OpenIddictEntityFrameworkToken.Type/OpenIddictEntityFrameworkCoreToken.Type column was changed from 50 to 150 to ensure the new URI-style token types can be safely stored without hitting length limits or triggering model validation errors.

Tip

Note: this preview also includes all the changes introduced in the OpenIddict 6.4.0 release and is likely the last preview before 7.0 RTM ships next months. As such, all users are encouraged to start testing OpenIddict 7.0 preview 4 as soon as possible, specially if the private_key_jwt client authentication is used with third-party clients and servers.

Don't miss a new openiddict-core release

NewReleases is sending notifications on new releases.