openiddict-core icon indicating copy to clipboard operation
openiddict-core copied to clipboard

How to override refresh_token logic?

Open kaputsyn opened this issue 6 months ago • 6 comments

Confirm you've already contributed to this project or that you sponsor it

  • [x] I confirm I'm a sponsor or a contributor

Version

6.2.0

Question

Hi! I want to override refresh_token logic for migration purposes. I need to apply custom logic for refreshing token (validating and constructing principal) if default implementation rejected. How to properly implement this?

I am using IOpenIddictServerHandler<OpenIddictServerEvents.HandleTokenRequestContext> but it is not firing if default validation rejects the request.

kaputsyn avatar Jun 16 '25 11:06 kaputsyn

Hi,

How to properly implement this?

It's a bit unclear what your requirements. Care to elaborate what you're trying to implement exactly?

kevinchalet avatar Jun 16 '25 11:06 kevinchalet

I have identityServer4 identity provider with the database and a lot of refresh_tokens. I need to migrate from old identityprovider(is4) to new one (openiddict). I want clients to migrate smoothly (simply to change authority url).

Then if client make refresh_token request I want to validate it either on NEW indetity or if failed on OLD one. I want client to fail on refresh only if refresh token is invalid on BOTH identities.

kaputsyn avatar Jun 16 '25 12:06 kaputsyn

Ah, in this case you'll want to implement that at the ValidateToken(Context) level, which is the event responsible for extracting token payloads and validating them.

Here's an example that produces an arbitrary "refresh token principal" for user [email protected] when the token is the hardcoded magic_token string (in your case, you'll want to query IdSrv's database and populate the principal based on that, obviously 😄)

/// <summary>
/// Contains the logic responsible for processing custom refresh tokens.
/// </summary>
public sealed class ValidateCustomRefreshToken : IOpenIddictServerHandler<OpenIddictServerEvents.ValidateTokenContext>
{
    /// <summary>
    /// Gets the default descriptor definition assigned to this handler.
    /// </summary>
    public static OpenIddictServerHandlerDescriptor Descriptor { get; }
        = OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.ValidateTokenContext>()
            .UseSingletonHandler<ValidateCustomRefreshToken>()
            .SetOrder(OpenIddictServerHandlers.Protection.ValidatePrincipal.Descriptor.Order - 500)
            .SetType(OpenIddictServerHandlerType.Custom)
            .Build();

    /// <inheritdoc/>
    public ValueTask HandleAsync(OpenIddictServerEvents.ValidateTokenContext context)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        // A null principal at this point indicates that the OpenIddict server was unable
        // to handle the token (e.g because the format wasn't recognized/isn't supported or
        // because it's malformed). In that case, you still have a chance to populate the
        // principal yourself, for instance to implement and support custom token formats.

        if (context.Principal is not null)
        {
            return default;
        }

        if (context.Token is "magic_token")
        {
            var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType);
            identity.SetClaim(Claims.Subject, "[email protected]");

            // Note: you MUST be 100% sure the processed token is a refresh token before
            // calling this method, as it's used by OpenIddict to validate the token type.
            identity.SetTokenType(TokenTypeHints.RefreshToken);

            // Note: if you're using the OpenIddict 7.0 previews, the new URI-style token types
            // MUST be used instead of the token_type_hint constants used in previous versions.
            //
            // See https://github.com/openiddict/openiddict-core/issues/2296 for more information.
            // identity.SetTokenType(TokenTypeIdentifiers.RefreshToken);

            context.Principal = new ClaimsPrincipal(identity);
        }

        return default;
    }
}
services.AddOpenIddict()
    .AddServer(options =>
    {
        options.AddEventHandler(ValidateCustomRefreshToken.Descriptor);
    });

Hope that'll help.

kevinchalet avatar Jun 16 '25 13:06 kevinchalet

Thanks. It returns access_token but not refresh_token. How to fix this? I need new refresh_token to be created and returned.

kaputsyn avatar Jun 16 '25 14:06 kaputsyn

How to fix this? I need new refresh_token to be created and returned.

Make sure the offline_access scope is granted (e.g identity.SetScopes(Scopes.OfflineAccess)).

kevinchalet avatar Jun 16 '25 17:06 kevinchalet

Did that work, @kaputsyn? 😃

kevinchalet avatar Jun 17 '25 19:06 kevinchalet