Подтвердить что ты не робот

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

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

Все работает отлично (логин, логин, авторизация и т.д.) до истечения 1 часа после истечения срока действия access_token. На этом этапе приложение 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, чтобы будущий запрос имел доступ к новым токенам?

4b9b3361

Ответ 1

Образец 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");
    }

Ответ 2

В качестве опции для метода 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
{}

Ответ 3

Во-первых, обязательно используйте библиотеку 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);
            }
        }
    }
}

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

Ответ 4

Я сделал промежуточное программное обеспечение, которое выполняет работу автоматически, когда прошло более половины срока жизни токена. Таким образом, вам не нужно вызывать какой-либо метод или применять какой-либо фильтр. Просто вставьте это в 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 не смог получить новый токен доступа ранее.

Реализация:

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);
    }
}

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

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

Ответ 5

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

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

Ответ 6

У IdentityServer4-Github есть еще один (новый?) Пример MvcHybridAutomaticRefresh.

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;
        }
    }