DuendeSoftware / Support

Support for Duende Software products
21 stars 0 forks source link

If the interval between /authorize and /connect/token is too short, invalid_grant occurs #635

Closed beyondnagatani closed 1 year ago

beyondnagatani commented 1 year ago

Which version of Duende IdentityServer are you using? 6

Which version of .NET are you using? 6

Describe the bug If the interval between /authorize and /connect/token is too short, invalid_grant occurs

To Reproduce

  1. Authenticate with /authorize
  2. Request /connect/token using code for query parameter from redirect_uri
  3. The following error occurs
Category: Duende.IdentityServer.Validation.TokenRequestValidator
EventId: 0
SpanId: 7f4a43a4900c69c6
TraceId: d29e28d6ab620f43a0d5d50bac9e2753
ParentId: 3b32a73cc271ea1a
RequestId: 800008b3-0001-f800-b63f-84710c7967bb
RequestPath: /connect/token

Invalid authorization code{ code = CB80CCC6E59F43613F896EA2056A8C9B69B05597AED085826662EAED67910EE6-1 }
  1. Send a request to /connect/token again with the same request parameters, and the response is returned normally

Log output/exception with stacktrace

Category: Duende.IdentityServer.Validation.TokenRequestValidator
EventId: 0
SpanId: 7f4a43a4900c69c6
TraceId: d29e28d6ab620f43a0d5d50bac9e2753
ParentId: 3b32a73cc271ea1a
RequestId: 800008b3-0001-f800-b63f-84710c7967bb
RequestPath: /connect/token

Invalid authorization code{ code = CB80CCC6E59F43613F896EA2056A8C9B69B05597AED085826662EAED67910EE6-1 }

Additional context

I can solve this problem by doing a retry process for /connect/token here, but is there any other way?

josephdecock commented 1 year ago

This sounds very strange, as though your grant is not fully committed to the store before the authorize endpoint redirects back to the client. What implementation of the operational data stores are you using?

josephdecock commented 1 year ago

Any update here? Should we close this issue?

beyondnagatani commented 1 year ago

Sorry for the late reply. I am not sure what you mean by operational data stores, but I will share the actual Startup.cs configuration.

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentityServer(options =>
    {
        options.Caching.ClientStoreExpiration = TimeSpan.FromMinutes(60);
        options.Caching.ResourceStoreExpiration = TimeSpan.FromMinutes(60);
        options.Caching.CorsExpiration = TimeSpan.FromMinutes(60);
        options.LicenseKey = Configuration.GetConnectionString("identityLicenseKey");

        options.KeyManagement.RotationInterval = TimeSpan.FromDays(30);

        options.KeyManagement.PropagationTime = TimeSpan.FromDays(2);

        options.KeyManagement.RetentionDuration = TimeSpan.FromDays(7);

        options.KeyManagement.DeleteRetiredKeys = false;

        options.KeyManagement.KeyPath = "/home/shared/keys";
    })
        .AddRedirectUriValidator<RedirectUriValidator>()
        .AddInMemoryCaching()
        .AddInMemoryIdentityResources(new IdentityResource[]
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
        })
        .AddInMemoryApiScopes(new ApiScope[]
        {
            new ApiScope(IdentityServerConstants.LocalApi.ScopeName),
        })
        .AddInMemoryPersistedGrants()
        .AddInMemoryCaching()
        .AddInMemoryClients(Configuration.GetSection("Clients"))
        .AddAspNetIdentity<User>()
        .AddProfileService<ProfileService>();

    services.ConfigureApplicationCookie(config =>
    {
        config.LoginPath = "/Web/User/Login";
        config.LogoutPath = "/Web/User/Logout";
        config.ExpireTimeSpan = TimeSpan.FromSeconds(Common.LoginTimeoutSeconds);
    });

    services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = Configuration.GetValue<string>("DomainName");
        options.SaveToken = true;
        options.RequireHttpsMetadata = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            ValidateAudience = false,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Common.TokenCreateKey)),
            ClockSkew = TimeSpan.Zero,
        };
    }).AddGoogle(options =>
    {
        options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
        options.ClientId = Configuration.GetValue<string>("GoogleClient:ClientId");
        options.ClientSecret = Configuration.GetValue<string>("GoogleClient:ClientSecret");
    }).AddMicrosoftAccount(options =>
    {
        options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
        options.ClientId = Configuration.GetValue<string>("MicrosoftClient:ClientId");
        options.ClientSecret = Configuration.GetValue<string>("MicrosoftClient:ClientSecret");
    });

    services.AddLocalApiAuthentication();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseForwardedHeaders();
    app.UseSession();
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseStatusCodePagesWithRedirects("/Home/Error?errorCode={0}");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseRouting();
    app.UseIdentityServer();
    app.UseCors();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });

}

Can you find the cause here?

josephdecock commented 1 year ago

What I mean by the operational data stores is basically where the persisted grants are stored. This is normally a database table that contains authorization codes, refresh tokens, etc.

You're doing AddInMemoryPersistedGrants, so you're storing the auth codes in memory on the IdentityServer host. What this means is that, if you are in a load balanced environment, every load balanced instance only sees the authorization codes that it sent.

The error is happening because one instance is sending the code, while another receives the token request to exchange the code for tokens. You need to create a data store for auth codes that is shared between the instances and then tell identity server how to use that shared data store. The abstraction for doing this is the IPersistedGrantsStore. We have an entity framework based implementation ready to go that you can read about here, or you can implement the store yourself.

beyondnagatani commented 1 year ago

Indeed, the App Service we are using has the number of instances set to 3, so memory may be the cause. I will try using the Entity Framework as described in the document you gave us, as it may solve the problem. Thank you very much.

josephdecock commented 1 year ago

Is anything further needed on this issue, or should we close it?

josephdecock commented 1 year ago

Closing, but feel free to reopen if necessary.