Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I update my cookie, having got a new access_token?

Tags:

Having used a refresh token to get a new access token, I want to update my client side cookie with that access token.

My client is able to sign in and call my REST API using ajax, however when that first authorization expires, naturally the API calls no longer work.

I have a .NET web application which consumes its own REST API. The API is a part of the same project. It does not have its own startup configuration.

As the cookie is being sent in the header of each request it needs to have the new unexpired access token so that I don't get 'User unauthorized' for the request.

Right now I am able to get a new token using my refresh token but the value of the cookie has not changed, so I believe I need to update my cookie to reflect the new access token before the client sends any requests.

Here's a look at my hybrid client:

using IdentityModel.Client;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace Cts.HomeService.Web.App_Start
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var identityServerSection = (IdentityServerSectionHandler)System.Configuration.ConfigurationManager.GetSection("identityserversection");

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "Cookies",
                CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
            });


            app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                ClientId = "localTestClient",
                Authority = "http://localhost:5000",
                RedirectUri = identityServerSection.Identity.RedirectUri,
                Scope = "openid profile offline_access",
                ResponseType = "code id_token",
                RequireHttpsMetadata = false,
                PostLogoutRedirectUri = identityServerSection.Identity.RedirectUri,

                TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "role",
                },
                SignInAsAuthenticationType = "Cookies",
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthorizationCodeReceived = async n =>
                    {
                        var tokenClient = new TokenClient(
                            "http://localhost:5000/connect/token",
                            "localTestClient",
                            "");

                        var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
                            n.Code, n.RedirectUri);

                        if (tokenResponse.IsError)
                        {
                            throw new Exception(tokenResponse.Error);
                        }

                        // use the access token to retrieve claims from userinfo
                        var userInfoClient = new UserInfoClient(
                            "http://localhost:5000/connect/userinfo");

                        var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);

                        // create new identity
                        var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
                        id.AddClaims(userInfoResponse.Claims);

                        id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
                        id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
                        id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
                        id.AddClaim(new Claim("id_token", tokenResponse.IdentityToken));
                        id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));

                        n.AuthenticationTicket = new AuthenticationTicket(
                            new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
                            n.AuthenticationTicket.Properties);
                    },

                    RedirectToIdentityProvider = n =>
                    {
                        {
                            // so here I'll grab the access token
                            if (isAccessTokenExpired()) {
                                var cancellationToken = new CancellationToken();
                                var newAccessToken = context.GetNewAccessTokenAsync(refresh_token, null, cancellationToken);
                               // now what?
                            }

                            // if signing out, add the id_token_hint
                            if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                            {
                                var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");

                                if (idTokenHint != null)
                                {
                                    n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                                }
                            }
                            return Task.FromResult(0);
                        }
                    }
                }
            });
        }
    }
}

I've looked into a lot of things but the value of my cookie always stays the same. I've considered deleting the old cookie and just building the new cookie manually, but that requires encrypting it the right way and it smells funny, surely not the idiomatic way to do it.

I feel there must be something simple I am missing. I would expect a simple "UpdateCookie(newToken)" kind of method and I have tried SignIn() and SignOut() but these have not worked out for me, seemingly not interacting with the cookie at all in fact.

like image 841
Pointo Senshi Avatar asked Apr 14 '19 13:04

Pointo Senshi


People also ask

How do you refresh access tokens?

To refresh the access token, select the Refresh access token API call within the Authorization folder of the Postman collection. Next, click the Send button to request a new access_token .

How do I refresh my MFA token?

Go to Services > Azure Partner (NCE) > Manage Refresh Token. In the Automatic Update group, click Update Refresh Token. The login page of the Microsoft Partner Center will open in a new browser window.

How do I refresh my Dropbox token?

It's not possible to get a refresh token from an access token. A refresh token can only be retrieved by authorizing the app via the OAuth app authorization flow. (The "Generate" button on an app's page on the App Console does not offer the ability to get a refresh token; that only returns an access token.)


2 Answers

This is how I got mine to work, add the following lines:

SecurityTokenValidated = context =>
                        {
                            context.AuthenticationTicket.Properties.AllowRefresh = true;
                            context.AuthenticationTicket.Properties.IsPersistent = true;
                        }

Then in AuthorizationCodeReceived add this to the end:

HttpContext.Current.GetOwinContext().Authentication.SignIn(new AuthenticationProperties
                                    {
                                        ExpiresUtc = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn),
                                        AllowRefresh = true,
                                        IssuedUtc = DateTime.UtcNow,
                                        IsPersistent = true
                                    }, newIdentity);

Where newIdentity is your claims identity, hope this helps.

like image 173
dagiakatsikas Avatar answered Oct 11 '22 21:10

dagiakatsikas


I recently stuck with the same question and the solution is:

  1. Set UseTokenLifetime = false in OpenIdConnectAuthenticationOptions used to configure OAuth middleware (otherwise session cookie lifetime will be set to access token lifetime, which is usually one hour)
  2. Create your own CookieAuthenticationProvider that validates access token expiration
  3. When token is expired (or close to be expired):
    1. Get new access token using refresh token (if MSAL is used for OAuth - this is a simple IConfidentialClientApplication.AcquireTokenSilent() method call)
    2. Build a fresh IIdentity object with the acquired access token using ISecurityTokenValidator.ValidateToken() method
    3. Replace request context identity by the newly built identity
    4. Call IAuthenticationManager.SignIn(properties, freshIdentity) to update the session cookie

Here is the full solution to make refresh tokens work with OWIN cookie middleware:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using EPiServer.Logging;
using Microsoft.Identity.Client;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Host.SystemWeb;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;

namespace MyApp
{
    public class OwinStartup
    {
        public void Configuration(IAppBuilder app)
        {
            var openIdConnectOptions = new OpenIdConnectAuthenticationOptions
            {
                UseTokenLifetime = false,
                // ...
            };

            var msalAppBuilder = new MsalAppBuilder();
            var refreshTokenHandler = new RefreshTokenHandler(msalAppBuilder, openIdConnectOptions);

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                CookieManager = new SystemWebChunkingCookieManager(),
                Provider = new RefreshTokenCookieAuthenticationProvider(refreshTokenHandler)
            });
        }
    }

    public class RefreshTokenCookieAuthenticationProvider : CookieAuthenticationProvider
    {
        private readonly RefreshTokenHandler _refreshTokenHandler;

        private static readonly ILogger _log = LogManager.GetLogger();

        public RefreshTokenCookieAuthenticationProvider(RefreshTokenHandler refreshTokenHandler)
        {
            _refreshTokenHandler = refreshTokenHandler;
        }

        public override async Task ValidateIdentity(CookieValidateIdentityContext context)
        {
            var exp = context.Identity?.FindFirst("exp")?.Value;

            if (string.IsNullOrEmpty(exp))
            {
                return;
            }

            var utcNow = DateTimeOffset.UtcNow;
            var expiresUtc = DateTimeOffset.FromUnixTimeSeconds(long.Parse(exp));
            var maxMinsBeforeExpires = TimeSpan.FromMinutes(2);

            if (expiresUtc - utcNow >= maxMinsBeforeExpires)
            {
                return;
            }

            try
            {
                var freshIdentity = await _refreshTokenHandler.TryRefreshAccessTokenAsync(context.Identity);

                if (freshIdentity != null)
                {
                    context.ReplaceIdentity(freshIdentity);
                    context.OwinContext.Authentication.SignIn(context.Properties, (ClaimsIdentity) freshIdentity);
                }
                else
                {
                    context.RejectIdentity();
                }
            }
            catch (Exception ex)
            {
                _log.Error("Can't refresh user token", ex);
                context.RejectIdentity();
            }
        }
    }

    public class RefreshTokenHandler
    {
        private readonly MsalAppBuilder _msalAppBuilder;
        private readonly OpenIdConnectAuthenticationOptions _openIdConnectOptions;

        public RefreshTokenHandler(
            MsalAppBuilder msalAppBuilder,
            OpenIdConnectAuthenticationOptions openIdConnectOptions)
        {
            _msalAppBuilder = msalAppBuilder;
            _openIdConnectOptions = openIdConnectOptions;
        }

        public async Task<IIdentity> TryRefreshAccessTokenAsync(IIdentity identity, CancellationToken ct = default)
        {
            try
            {
                var idToken = await GetFreshIdTokenAsync(identity, ct);
                var freshIdentity = await GetFreshIdentityAsync(idToken, ct);

                return freshIdentity;
            }
            catch (MsalUiRequiredException)
            {
                return null;
            }
        }

        private async Task<string> GetFreshIdTokenAsync(IIdentity identity, CancellationToken ct)
        {
            var principal = new ClaimsPrincipal(identity);
            var app = _msalAppBuilder.BuildConfidentialClientApplication(principal);

            var accounts = await app.GetAccountsAsync();
            var result = await app.AcquireTokenSilent(new[] {"openid"}, accounts.FirstOrDefault()).ExecuteAsync(ct);

            return result.IdToken;
        }

        private async Task<IIdentity> GetFreshIdentityAsync(string idToken, CancellationToken ct)
        {
            var validationParameters = await CreateTokenValidationParametersAsync(ct);
            var principal = _openIdConnectOptions.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out _);
            var identity = (ClaimsIdentity) principal.Identity;

            return identity;
        }

        // This is additional code for cases with multiple issuers - can be skipped if this configuration is static
        private async Task<TokenValidationParameters> CreateTokenValidationParametersAsync(CancellationToken ct)
        {
            var validationParameters = _openIdConnectOptions.TokenValidationParameters.Clone();
            var configuration = await _openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(ct);

            validationParameters.ValidIssuers = (validationParameters.ValidIssuers ?? new string[0])
                .Union(new[] {configuration.Issuer})
                .ToList();
            validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys ?? new SecurityKey[0])
                .Union(configuration.SigningKeys)
                .ToList();

            return validationParameters;
        }
    }

    // From official samples: https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi/blob/master/TaskWebApp/Utils/MsalAppBuilder.cs
    public class MsalAppBuilder
    {
        public IConfidentialClientApplication BuildConfidentialClientApplication(ClaimsPrincipal currentUser)
        {
            // ...
        }
    }
}
like image 41
whyleee Avatar answered Oct 11 '22 20:10

whyleee