Asp.NET 核心 OpenIddict invalid_grant

Asp.NET Core OpenIddict invalid_grant

第一件事:我知道这个问题很大post,但是我跟踪这个问题已经好几个星期了,我收集了很多信息可能是什么问题的根源。

我正在使用带有 OpenIddict 身份验证的 Angular2 应用程序。我在客户端应用程序上得到 access_token、refresh_token。我可以使用 refresh_token 获取新的 access_token,一切正常。差不多。

有时我从服务器收到错误响应:

POST https://mydomain:2000/api/authorization/token 400 (Bad Request)

和响应:

error:"invalid_grant"
error_description:"Invalid ticket"

我检查了所有内容,refresh_token 我发送的是正确的。

关于设计:
在我向服务器发出请求之前,我检查 access_token 是否过期。如果过期,我会发送请求以使用 refresh_token 获取新的 access_token。

它适用于随机时间,但在某些随机时间(重复)refresh_token 变得无效。
我虽然和AddEphemeralSigningKey有关,但我把它改成了AddSigningCertificate(详情在 线程中。)

我认为,IIS 在输入一段时间后会杀死 Kestrel-activity。我的应用程序池配置是:

StartMode: OnDemand
Idle Time-out (minutes): 20
Idle Time-out (action): Terminate

我怀疑在发出新请求后,OpenIddict 错误地解密了 refresh_token,因为 Kestrel 已重新启动?还是我错了?

我还检查了 OpenIddict 表和 OpenIddictApplications、OpenIddictAuthorizations 和 OpenIddictScopes 都是空的。只有 OpenIddictTokens 包含一些数据(并且都是类型 refresh_token):

我希望 refresh_token 保存在某个地方。 Where? 也许这是源问题,为什么我的 refresh_tokens 在随机时间后无效(可能是当 Kestrel 重新启动时)。

IIS 日志:

Hosting environment: Production
Content root path: D:\Podatki\OpPISWeb\WWWProduction
Now listening on: http://localhost:1408
Application started. Press Ctrl+C to shut down.
fail: AspNet.Security.OpenIdConnect.Server.OpenIdConnectServerMiddleware[0]
      The token request was rejected because the authorization code or the refresh token was invalid.
fail: AspNet.Security.OpenIdConnect.Server.OpenIdConnectServerMiddleware[0]
      The token request was rejected because the authorization code or the refresh token was invalid.

这是我的 Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    try
    {
        services.Configure<IISOptions>(options =>
        {
        });

        services.AddMvc();
        services.AddMvcCore().AddDataAnnotations();

        services.AddEntityFrameworkSqlServer();

        services.AddScoped<UserStore<AppUser, AppRole, AppDbContext, int, AppUserClaim, AppUserRole, AppUserLogin, AppUserToken, AppRoleClaim>, AppUserStore>();
        services.AddScoped<UserManager<AppUser>, AppUserManager>();
        services.AddScoped<RoleManager<AppRole>, AppRoleManager>();
        services.AddScoped<SignInManager<AppUser>, AppSignInManager>();
        services.AddScoped<RoleStore<AppRole, AppDbContext, int, AppUserRole, AppRoleClaim>, AppRoleStore>();

        var connection = Configuration["ConnectionStrings:Web"];
        services.AddDbContext<AppDbContext>(options =>
        {
            options.UseSqlServer(connection);
            options.UseOpenIddict<int>();
            if (this.env.IsDevelopment())
                options.EnableSensitiveDataLogging();
        });


        services
            .AddIdentity<AppUser, AppRole>()
            .AddUserStore<AppUserStore>()
            .AddUserManager<AppUserManager>()
            .AddRoleStore<AppRoleStore>()
            .AddRoleManager<AppRoleManager>()
            .AddSignInManager<AppSignInManager>()
            .AddDefaultTokenProviders();

        services.Configure<IdentityOptions>(options =>
            {
                options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
                options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
                options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
            });

        services.AddOpenIddict<int>(options =>
        {
            options.AddEntityFrameworkCoreStores<AppDbContext>();
            options.AddMvcBinders();
            options.EnableTokenEndpoint("/API/authorization/token");
            options.AllowPasswordFlow();
            options.AllowRefreshTokenFlow();
            options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:google_identity_token");
            options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:logedin");
            options.UseJsonWebTokens();
            if (this.env.IsDevelopment())
                options.AddEphemeralSigningKey();  
            else
                options.AddSigningCertificate(new FileStream(
                    Directory.GetCurrentDirectory() + "/Resources/cert.pfx", FileMode.Open), "password");
            options.SetAccessTokenLifetime(TimeSpan.FromMinutes(30));
            options.SetRefreshTokenLifetime(TimeSpan.FromDays(14));
            if (this.env.IsDevelopment())
                options.DisableHttpsRequirement();
        });

        services.AddSingleton<DbSeeder>();
        services.AddSingleton<IConfiguration>(c => { return Configuration; });

    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
        throw;
    }
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, DbSeeder dbSeeder)
{
    loggerFactory.AddConsole(this.Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
        {
            HotModuleReplacement = true
        });
    }
    app.UseStaticFiles();

    app.UseStaticFiles(new StaticFileOptions()
    {
        FileProvider = new PhysicalFileProvider(this.Configuration["Directories:Upload"]),
        RequestPath = new PathString("/Files")
    });

    app.UseOpenIddict();

    var JwtOptions = new JwtBearerOptions()
    {
        Authority = this.Configuration["Authentication:OpenIddict:Authority"],
        Audience = "OpPISWeb",
        AutomaticAuthenticate = true,
        AutomaticChallenge = true,

        RequireHttpsMetadata = false
    };
    JwtOptions.RequireHttpsMetadata = !env.IsDevelopment();
    app.UseJwtBearerAuthentication(JwtOptions);

    app.UseMvc();

    using (var context = new AppDbContext(this.Configuration))
    {
        context.Database.Migrate();
    }
    try
    {
        dbSeeder.SeedAsync();
    }
    catch (AggregateException e)
    {
        throw new Exception(e.ToString());
    }
}

控制台截图:

更新:
最后我所要做的就是:

services.AddDataProtection()
                    .SetApplicationName(this.Configuration["Authentication:ApplicationId"])
                    .PersistKeysToFileSystem(new DirectoryInfo(this.Configuration["Directories:Keys"]));

不要忘记为 Directories:Keys 文件夹添加 IIS 权限。

I would expect, that refresh_tokens are saved somewhere. Where?

无处可去。 OpenIddict 颁发的授权代码、刷新令牌和访问令牌(使用默认格式时)是 独立的 并且出于安全原因从不存储(仅元数据,如主题或授权标识符与令牌关联的是)。

您看到的问题可能是由于您没有配置您的环境以正确保存 ASP.NET 核心数据保护堆栈 OpenIddict 所依赖的加密其令牌所使用的加密密钥。您可以阅读 以了解有关如何解决该问题的更多信息。