IdentityServer4 - Использование токенов обновления после выполнения краткого руководства для гибридного MVC

Я следил за кратким руководством на странице документации и имею рабочую конфигурацию трех служб (IdentityServer, одну службу Api, одно приложение ASPNET MVC), использующих IdentityServer для аутентификации.

Все работает отлично (логин, логин, авторизация и т. Д.), Пока не истечет срок действия access_token через 1 час. На этом этапе приложение MVC начинает получать (правильно) 401 от службы API (поскольку срок действия токена истек). В этот момент я знаю, что должен использовать refresh_token, чтобы получить новый access_token.

Я искал механизм, который автоматически обновлял access_token, и наткнулся на это: https://github.com/mderriey/TokenRenewal/blob/master/src/MvcClient/Startup.cs (из этот ответ). Я попытался использовать это, но это не сработало (TokenEndpointResponse был пустым, хотя аутентификация прошла успешно).

Я понимаю, как использовать refresh_token для получения нового access_token, но после того, как я его получу, как мне вставить его обратно в файл cookie, чтобы будущий запрос имел доступ к новым токенам?


person Carlos G.    schedule 19.01.2017    source источник


Ответы (6)


В примере McvHybrid есть хороший пример возврата новых access_token и refresh_token в принципал. Вот ссылка на github с кодом, который находится в RenewTokens(), как показано ниже.

    public async Task<IActionResult> RenewTokens()
    {
        var disco = await DiscoveryClient.GetAsync(Constants.Authority);
        if (disco.IsError) throw new Exception(disco.Error);

        var tokenClient = new TokenClient(disco.TokenEndpoint, "mvc.hybrid", "secret");
        var rt = await     HttpContext.Authentication.GetTokenAsync("refresh_token");
        var tokenResult = await tokenClient.RequestRefreshTokenAsync(rt);

        if (!tokenResult.IsError)
        {
            var old_id_token = await HttpContext.Authentication.GetTokenAsync("id_token");
            var new_access_token = tokenResult.AccessToken;
            var new_refresh_token = tokenResult.RefreshToken;

            var tokens = new List<AuthenticationToken>();
            tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = old_id_token });
            tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = new_access_token });
            tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = new_refresh_token });

            var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
            tokens.Add(new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) });

            var info = await HttpContext.Authentication.GetAuthenticateInfoAsync("Cookies");
            info.Properties.StoreTokens(tokens);
            await HttpContext.Authentication.SignInAsync("Cookies", info.Principal, info.Properties);

            return Redirect("~/Home/Secure");
        }

        ViewData["Error"] = tokenResult.Error;
        return View("Error");
    }
person LugTread    schedule 19.01.2017
comment
В примере McvHybrid RenewTokens вызываются только вручную (при нажатии на ссылку). Вы знаете пример, в котором токен обновления вызывается, когда срок действия токена доступа истекает? - person Michael Freidgeim; 24.07.2017
comment
Вы можете легко добавить приведенный выше код в какое-то промежуточное программное обеспечение, которое затем будет зарегистрировано в вашем конвейере до MVC. - person LugTread; 26.07.2017
comment
@LugTread Я не думаю, что у вас есть такой пример, не так ли? Я новичок в MVC в .NET Core, и я никогда не писал промежуточного программного обеспечения, я сейчас изучаю его, но если бы вы сделали что-то вроде и у вас есть образец кода, это было бы потрясающе. Спасибо. - person bgs264; 27.08.2017
comment
Пример кода обновлен до последней версии: удалены устаревшие операторы, добавлен httpClientFactory, новые расширения. Вы также можете обновить код в своем ответе. - person Ruard van Elburg; 20.09.2018
comment
Почему мы берем старый идентификатор identity_token в отличие от токенов доступа и обновления? - person Simple Code; 27.11.2018

Как вариант метода RenewTokens из клиента MVC пример, я сделал один фильтр, который выполняет задание автоматически, когда срок действия токена составляет около 10 минут или меньше.

public class TokenFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var expat = filterContext.HttpContext.Authentication.GetTokenAsync("expires_at").Result;

        var dataExp = DateTime.Parse(expat, null, DateTimeStyles.RoundtripKind);

        if ((dataExp - DateTime.Now).TotalMinutes < 10)
        {
            var disco = DiscoveryClient.GetAsync("http://localhost:5000/").Result;
            if (disco.IsError) throw new Exception(disco.Error);

            var tokenClient = new TokenClient(disco.TokenEndpoint, "clientId",
                "clientSecret");

            var rt = filterContext.HttpContext.Authentication.GetTokenAsync("refresh_token").Result;
            var tokenResult = tokenClient.RequestRefreshTokenAsync(rt).Result;

            if (!tokenResult.IsError)
            {
                var oldIdToken = filterContext.HttpContext.Authentication.GetTokenAsync("id_token").Result;
                var newAccessToken = tokenResult.AccessToken;
                var newRefreshToken = tokenResult.RefreshToken;

                var tokens = new List<AuthenticationToken>
                {
                    new AuthenticationToken {Name = OpenIdConnectParameterNames.IdToken, Value = oldIdToken},
                    new AuthenticationToken
                    {
                        Name = OpenIdConnectParameterNames.AccessToken,
                        Value = newAccessToken
                    },
                    new AuthenticationToken
                    {
                        Name = OpenIdConnectParameterNames.RefreshToken,
                        Value = newRefreshToken
                    }
                };

                var expiresAt = DateTime.Now + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
                tokens.Add(new AuthenticationToken
                {
                    Name = "expires_at",
                    Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                });

                var info = filterContext.HttpContext.Authentication.GetAuthenticateInfoAsync("Cookies").Result;
                info.Properties.StoreTokens(tokens);
                filterContext.HttpContext.Authentication.SignInAsync("Cookies", info.Principal, info.Properties);
            }
        }
    }
}

Использование:

[Authorize]
[TokenFilter]
public class HomeController : Controller
{}
person Rafael Trojack    schedule 24.07.2017
comment
Работает ли это, если срок действия самого access_token истек, но refresh_token все еще действителен? Или промежуточное ПО перенаправит вас на страницу входа ID4, прежде чем это сможет обновить токен доступа в этом сценарии? - person Watson; 05.07.2018

Во-первых, обязательно используйте библиотеку IdentityModel (добавьте ее). Во-вторых, поскольку Auth 2.0 вышел, есть некоторые критические изменения, и HttpContext.Authentication, используемый в решении Rafaels, теперь устарел. Вот изменения, которые необходимо внести, чтобы снова запустить его как фильтр.

var expat = filterContext.HttpContext.Authentication.GetTokenAsync("expires_at").Result;

должно стать:

var expat = filterContext.HttpContext.GetTokenAsync("expires_at").Result;

var rt = filterContext.HttpContext.Authentication.GetTokenAsync("refresh_token").Result;

должно стать:

var rt = filterContext.HttpContext.GetTokenAsync("refresh_token").Result;

var oldIdToken = filterContext.HttpContext.Authentication.GetTokenAsync("id_token").Result;

должен стать

var oldIdToken = filterContext.HttpContext.GetTokenAsync("id_token").Result;

var info = filterContext.HttpContext.Authentication.GetAuthenticateInfoAsync("Cookies").Result;

должен стать

var info = filterContext.HttpContext.AuthenticateAsync("Cookies").Result;

filterContext.HttpContext.Authentication.SignInAsync("Cookies", info.Principal, info.Properties);

должен стать

filterContext.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);

А это весь код:

public class TokenFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var expat = filterContext.HttpContext.GetTokenAsync("expires_at").Result;

        var dataExp = DateTime.Parse(expat, null, DateTimeStyles.RoundtripKind);

        if ((dataExp - DateTime.Now).TotalMinutes < 10)
        {
            var disco = DiscoveryClient.GetAsync("http://localhost:5000/").Result;
            if (disco.IsError) throw new Exception(disco.Error);

            var tokenClient = new TokenClient(disco.TokenEndpoint, "clientId",
            "clientSecret");

            var rt = filterContext.HttpContext.GetTokenAsync("refresh_token").Result;
            var tokenResult = tokenClient.RequestRefreshTokenAsync(rt).Result;

            if (!tokenResult.IsError)
            {
                var oldIdToken = filterContext.HttpContext.GetTokenAsync("id_token").Result;
                var newAccessToken = tokenResult.AccessToken;
                var newRefreshToken = tokenResult.RefreshToken;

                var tokens = new List<AuthenticationToken>
                {
                    new AuthenticationToken {Name = OpenIdConnectParameterNames.IdToken, Value = oldIdToken},
                    new AuthenticationToken
                    {
                        Name = OpenIdConnectParameterNames.AccessToken,
                        Value = newAccessToken
                    },
                    new AuthenticationToken
                    { 
                        Name = OpenIdConnectParameterNames.RefreshToken,
                        Value = newRefreshToken
                    }
                };

                var expiresAt = DateTime.Now + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
                tokens.Add(new AuthenticationToken
                {
                    Name = "expires_at",
                    Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                });

                var info = filterContext.HttpContext.AuthenticateAsync("Cookies").Result;
                info.Properties.StoreTokens(tokens);  
                filterContext.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
            }
        }
    }
}

Использование такое же, как показал Рафаэль.

person Ivaylo Dimitrov    schedule 12.06.2018
comment
Обратите внимание, что и DiscoveryClient, и TokenClient теперь устарели. Эта статья хорошо объясняет детали. - person McGuireV10; 16.12.2019

Ссылка, которую вы предоставили на https://github.com/mderriey/TokenRenewal/blob/master/src/MvcClient/Startup.cs мне очень помог!

Проблема была в разделе AddOpenIdConnect. Требуемое событие не является событием OnTokenValidated. Вам следует использовать событие OnTokenResponseReceived. На этом этапе у вас будет правильный access_token и refresh_token для добавления в файл cookie.

person kduenke    schedule 20.08.2018
comment
Этот маленький кусочек информации наконец-то помог мне после нескольких часов выдергивания волос! Все примеры, которые я нашел, ссылались на OnTokenValidated, но TokenEndpointResponse всегда был нулевым, либо я делаю что-то не так, либо что-то изменилось, в любом случае OnTokenResponseReceived мое решение заработало. - person XN16; 19.06.2020

IdentityServer4-Github имеет еще один (новый?) пример MvcAutagementTokenManagement. а>.

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

Самая актуальная (?) Часть:

    public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
    {
        // [removed about 20 lines of code to get and check tokens here...]
        if (dtRefresh < _clock.UtcNow)
        {
            var shouldRefresh = _pendingRefreshTokenRequests.TryAdd(refreshToken.Value, true);
            if (shouldRefresh)
            {
                try
                {
                    var response = await _service.RefreshTokenAsync(refreshToken.Value);

                    if (response.IsError)
                    {
                        _logger.LogWarning("Error refreshing token: {error}", response.Error);
                        return;
                    }

                    context.Properties.UpdateTokenValue("access_token", response.AccessToken);
                    context.Properties.UpdateTokenValue("refresh_token", response.RefreshToken);

                    var newExpiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn);
                    context.Properties.UpdateTokenValue("expires_at", newExpiresAt.ToString("o", CultureInfo.InvariantCulture));

                    await context.HttpContext.SignInAsync(context.Principal, context.Properties);
                }
                finally
                {
                    _pendingRefreshTokenRequests.TryRemove(refreshToken.Value, out _);
                }
            }
        }
    }

    public override async Task SigningOut(CookieSigningOutContext context)
    {
        // [removed about 15 lines of code to get and check tokens here...]
        var response = await _service.RevokeTokenAsync(refreshToken.Value);
        if (response.IsError)
        {
            _logger.LogWarning("Error revoking token: {error}", response.Error);
            return;
        }
    }
person Yahoo Serious    schedule 17.05.2019
comment
Реализация, представленная Домиником Байером, действительно чиста и работает безупречно - есть еще одна зависимость - IdentityModel. Спасибо @YahooSerious за указание на этот ресурс. - person Ivaylo Dimitrov; 08.08.2019
comment
У меня проблема в том, что context.Properties.GetTokens() устарел - person DaleyKD; 23.01.2020
comment
Если я добавил context.ShouldRenew = true прямо перед SignInAsync, это, похоже, гарантирует, что мои токены не устареют в следующий раз. - person DaleyKD; 23.01.2020

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

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // Other code here

    app.UseAutomaticSilentRenew("http://localhost:5000/", "clientId", "clientSecret")
    app.UseAccessTokenLifetime();

    // And here
}

UseAutomaticSilentRenew - возобновляет доступ и обновляет токены
UseAccessTokenLifetime - выдает пользователя из системы, если срок действия токена доступа истек. Поместите это после UseAutomaticSilentRenew, чтобы он работал только в том случае, если UseAutomaticSilentRenew не удалось получить новый токен доступа ранее.

Реализация:

public static class OidcExtensions
{
    public static IApplicationBuilder UseAutomaticSilentRenew(this IApplicationBuilder builder, string authority, string clientId, string clientSecret, string cookieSchemeName = CookieAuthenticationDefaults.AuthenticationScheme)
    {
        return builder.UseMiddleware<AutomaticSilentRenewMiddleware>(authority, clientId, clientSecret, cookieSchemeName);
    }

    public static IApplicationBuilder UseAccessTokenLifetime(this IApplicationBuilder builder, string cookieSchemeName = CookieAuthenticationDefaults.AuthenticationScheme)
    {
        return builder.UseMiddleware<TokenLifetimeMiddleware>(OpenIdConnectParameterNames.AccessToken, cookieSchemeName);
    }

    public static IApplicationBuilder UseIdTokenLifetime(this IApplicationBuilder builder, string cookieSchemeName = CookieAuthenticationDefaults.AuthenticationScheme)
    {
        return builder.UseMiddleware<TokenLifetimeMiddleware>(OpenIdConnectParameterNames.IdToken, cookieSchemeName);
    }
}

public class AutomaticSilentRenewMiddleware
{
    private readonly RequestDelegate next;
    private readonly string authority;
    private readonly string clientId;
    private readonly string clientSecret;
    private readonly string cookieSchemeName;

    public AutomaticSilentRenewMiddleware(RequestDelegate next, string authority, string clientId, string clientSecret, string cookieSchemeName)
    {
        this.next = next;
        this.authority = authority;
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.cookieSchemeName = cookieSchemeName;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        string oldAccessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
        if (!string.IsNullOrEmpty(oldAccessToken))
        {
            JwtSecurityToken tokenInfo = new JwtSecurityToken(oldAccessToken);

            // Renew access token if pass halfway of its lifetime
            if (tokenInfo.ValidFrom + (tokenInfo.ValidTo - tokenInfo.ValidFrom) / 2 < DateTime.UtcNow)
            {
                string tokenEndpoint;
                var disco = await DiscoveryClient.GetAsync(authority);
                if (!disco.IsError)
                {
                    tokenEndpoint = disco.TokenEndpoint;
                }
                else
                {
                    // If failed to get discovery document use default URI
                    tokenEndpoint = authority + "/connect/token";
                }
                TokenClient tokenClient = new TokenClient(tokenEndpoint, clientId, clientSecret);
                string oldRefreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
                TokenResponse tokenResult = await tokenClient.RequestRefreshTokenAsync(oldRefreshToken);

                if (!tokenResult.IsError)
                {
                    string idToken = await context.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
                    string newAccessToken = tokenResult.AccessToken;
                    string newRefreshToken = tokenResult.RefreshToken;

                    var tokens = new List<AuthenticationToken>
                    {
                        new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = idToken },
                        new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = newAccessToken },
                        new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = newRefreshToken }
                    };

                    AuthenticateResult info = await context.AuthenticateAsync(cookieSchemeName);
                    info.Properties.StoreTokens(tokens);
                    await context.SignInAsync(cookieSchemeName, info.Principal, info.Properties);
                }
            }
        }

        await next.Invoke(context);
    }
}

public class TokenLifetimeMiddleware
{
    private readonly RequestDelegate next;
    private readonly string tokenName;
    private readonly string cookieSchemeName;

    public TokenLifetimeMiddleware(RequestDelegate next, string tokenName, string cookieSchemeName)
    {
        this.next = next;
        this.tokenName = tokenName;
        this.cookieSchemeName = cookieSchemeName;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        string token = await context.GetTokenAsync(tokenName);
        if (!string.IsNullOrEmpty(token))
        {
            DateTime validTo = new JwtSecurityToken(token).ValidTo;
            if (validTo < DateTime.UtcNow)
            {
                // Sign out if token is no longer valid
                await context.SignOutAsync(cookieSchemeName);
            }
        }

        await next.Invoke(context);
    }
}

Примечание. Я не устанавливал срок действия cookie, потому что в нашем случае он зависит от времени жизни токена обновления, который не предоставляется сервером идентификации. Если бы я согласовал истечение срока действия файла cookie с истечением срока действия токена доступа, я бы не смог обновить токен доступа после его истечения.

Да, и еще кое-что. UseAccessTokenLifetime удаляет файл cookie, но не выполняет выход пользователя. Выход происходит после перезагрузки страницы. Не нашел способа исправить это.

person Максим Кошевой    schedule 21.09.2018
comment
Будет ли этот код обрабатывать токен обновления, срок действия которого истек или он был удален? - person DaImTo; 06.09.2019
comment
Если срок действия токена обновления истек (или он не выходит по какой-либо причине), мы ничего не можем сделать, чтобы обновить токен доступа. Таким образом, UseAutomaticSilentRenew ничего не сделает, а затем UseAccessTokenLifetime выйдет из системы, если токен доступа истек. - person Максим Кошевой; 07.09.2019