Asp.NET Core OpenIddict invalid_grant

Перво-наперво: я знаю, что это большой пост, но я отслеживаю эту проблему в течение нескольких недель и собираю много информации, которая может быть источником проблемы.

Я использую приложение Angular2 с аутентификацией OpenIddict. Я получаю access_token, refresh_token в клиентском приложении. Я могу использовать refresh_token для получения нового access_token, все работает. Почти.

В какой-то момент я получаю ответ с ошибкой от сервера:

POST https://mydomain:2000/api/authorization/token 400 (неверный запрос)

и ответ:

error:"invalid_grant"
error_description:"Invalid ticket"

Я трижды проверил все, и отправленный мной refresh_token правильный.

О дизайне:
Перед тем, как сделать запрос к серверу, я проверяю, истекает ли срок действия access_token. По истечении срока я отправляю запрос на получение нового access_token с помощью refresh_token.

И это работает в случайное время, но в какое-то случайное время (повтор) refresh_token становится недействительным.
Я, хотя это как-то связано с AddEphemeralSigningKey, меняю его на AddSigningCertificate. (Подробности в этой ветке.)

Я думаю, что IIS убивает Kestrel после некоторого времени бездействия. Конфигурация моего пула приложений:

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

Я подозреваю, что после того, как был сделан новый запрос, OpenIddict неправильно расшифровал refresh_token из-за перезапуска Kestrel? Или я не прав?

Я также проверяю, что таблицы OpenIddict и OpenIddictApplications, OpenIddictAuthorizations и OpenIddictScopes пусты. Только OpenIddictTokens содержат некоторые данные (и все они имеют тип refresh_token):

OpenIddictTokens

Я ожидал, что refresh_tokens где-нибудь сохранятся. Где? Может быть, это проблема источника, почему мои 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());
    }
}

Скриншоты консоли:  Получение refresh_token  Отправка refresh_token

Обновление:
В конце концов, все, что мне нужно было сделать, это:

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

Не забудьте добавить права на IIS для каталогов: папка ключей.


person Makla    schedule 20.04.2017    source источник


Ответы (1)


Я ожидал, что refresh_tokens где-нибудь сохранятся. Где?

Нигде. Коды авторизации, токены обновления и токены доступа (при использовании формата по умолчанию), выдаваемые OpenIddict, автономны и никогда не сохраняются по соображениям безопасности (только метаданные, такие как тема или идентификатор авторизации, связанный с токен есть).

Проблема, которую вы видите, вероятно, вызвана тем, что вы не настроили свою среду для правильного сохранения криптографических ключей, используемых стеком защиты данных ASP.NET Core, на который OpenIddict использует для шифрования своих токенов. Вы можете прочитать OpenIddict: ошибка 401 при двух или более службах количество экземпляров, чтобы узнать, как это исправить.

person Kévin Chalet    schedule 20.04.2017
comment
@Pinpint: Думаю, это правда. Проблема появляется, когда другой пользователь входит в систему. Есть ли какой-либо рекомендуемый способ, как это сделать. Проверяю ваши примеры, читаю связанные темы. Может быть, несколько строк кода, что мне делать. Спасибо. - person Makla; 21.04.2017