Refresh tokens using owin middleware and IdentityServer v3
Asked Answered
O

1

6

I've recently setup IdentityServer v3 and its running like a dream, however I'm having troubles with the OWIN middleware.

I would like to use the hybrid flow so I can refresh tokens in the backend without the user having to redirect back to the IdentityServer to get a new access token every 5 minutes (which is also odd as its set to have a lifetime of 1 hour on the server).

I'm using the following config in startup and I'm getting the tokens fine, but it never seems to try and refresh the access token once it's expired. Do I need some custom logic somewhere to refresh my tokens?

        app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
        {
            ClientId = clientId,
            ClientSecret = clientSecret, //Not sure what this does?

            Authority = "https://auth.example.com",

            RedirectUri = "http://website.example.com",
            PostLogoutRedirectUri = "http://website.example.com",

            ResponseType = "code id_token token",
            Scope = "openid profile email write read offline_access",

            SignInAsAuthenticationType = "Cookies",

            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                AuthorizationCodeReceived = async n =>
                {
                    // filter "protocol" claims
                    var claims = new List<Claim>(from c in n.AuthenticationTicket.Identity.Claims
                                                 where c.Type != "iss" &&
                                                       c.Type != "aud" &&
                                                       c.Type != "nbf" &&
                                                       c.Type != "exp" &&
                                                       c.Type != "iat" &&
                                                       c.Type != "nonce" &&
                                                       c.Type != "c_hash" &&
                                                       c.Type != "at_hash"
                                                 select c);

                    // get userinfo data
                    var userInfoClient = new UserInfoClient(
                        new Uri(n.Options.Authority + "/connect/userinfo"),
                        n.ProtocolMessage.AccessToken);

                    var userInfo = await userInfoClient.GetAsync();
                    userInfo.Claims.ToList().ForEach(ui => claims.Add(new Claim(ui.Item1, ui.Item2)));

                    // get access and refresh token
                    var tokenClient = new OAuth2Client(
                        new Uri(n.Options.Authority + "/connect/token"),
                        clientId,
                        clientSecret);

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

                    claims.Add(new Claim("access_token", response.AccessToken));
                    claims.Add(new Claim("expires_at", DateTime.UtcNow.AddSeconds(response.ExpiresIn).ToLocalTime().ToString(CultureInfo.InvariantCulture)));
                    claims.Add(new Claim("refresh_token", response.RefreshToken));
                    claims.Add(new Claim("id_token", n.ProtocolMessage.IdToken));

                    //Does this help?
                    n.AuthenticationTicket.Properties.AllowRefresh = true;

                    n.AuthenticationTicket = new AuthenticationTicket(
                        new ClaimsIdentity(
                            claims.Distinct(new ClaimComparer()),
                            n.AuthenticationTicket.Identity.AuthenticationType),
                        n.AuthenticationTicket.Properties);
                },

                RedirectToIdentityProvider = async n =>
                {
                    // if signing out, add the id_token_hint
                    if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
                    {
                        var id = n.OwinContext.Authentication.User.FindFirst("id_token");

                        if (id != null)
                        {
                            var idTokenHint = id.Value;
                            n.ProtocolMessage.IdTokenHint = idTokenHint;
                        }
                    }
                }
            }
        });

I'm also using the following in my ApiClient (RestSharp) which talks to my resource api

public class MyTokenAuthenticator : IAuthenticator
{
    public void Authenticate(IRestClient client, IRestRequest request)
    {
        var tokenClaim = ClaimsPrincipal.Current.Claims.FirstOrDefault(c => c.Type.Equals("access_token"));

        if (tokenClaim != null && !String.IsNullOrWhiteSpace(tokenClaim.Value))
            request.AddHeader("Authorization", String.Format("Bearer {0}", tokenClaim.Value));
    }
}
Ombre answered 28/7, 2015 at 6:21 Comment(0)
S
3

I was able to get a refresh token and then use it to get a new access token: I followed similar logic as yours to get a token. I created the following method which I called every time I needed a token:

private static async Task CheckAndPossiblyRefreshToken(ClaimsIdentity id)
    {
        var clientName = "Myhybridclient";
        // check if the access token hasn't expired.
        if (DateTime.Now.ToLocalTime() >=
             (DateTime.Parse(id.FindFirst("expires_at").Value)))
        {
            // expired.  Get a new one.
            var tokenEndpointClient = new OAuth2Client(
                new Uri(Constants.TokenEndpoint),
                clientName,
                "secret");

            var tokenEndpointResponse =
                await tokenEndpointClient
                .RequestRefreshTokenAsync(id.FindFirst("refresh_token").Value);

            if (!tokenEndpointResponse.IsError)
            {
                // replace the claims with the new values - this means creating a 
                // new identity!                              
                var result = from claim in id.Claims
                             where claim.Type != "access_token" && claim.Type != "refresh_token" &&
                                   claim.Type != "expires_at"
                             select claim;

                var claims = result.ToList();

                claims.Add(new Claim("access_token", tokenEndpointResponse.AccessToken));
                claims.Add(new Claim("expires_at",
                             DateTime.Now.AddSeconds(tokenEndpointResponse.ExpiresIn)
                             .ToLocalTime().ToString()));
                claims.Add(new Claim("refresh_token", tokenEndpointResponse.RefreshToken));

                var newIdentity = new ClaimsIdentity(claims, "Cookies");
                var wrapper = new HttpRequestWrapper(HttpContext.Current.Request);
                wrapper.GetOwinContext().Authentication.SignIn(newIdentity);
            }
            else
            {
                // log, ...
                throw new Exception("An error has occurred");
            }
        }
    } 
Systemic answered 2/5, 2016 at 22:59 Comment(2)
In my opinion waiting till local time is bigger or equal access token expiration time is not the best idea. Imagine a situation when this condition will consider AccessToken as valid only few milliseconds before expiration. If you will send such AccessToken to some other service, then it could be rejected because it has expired in the meantime. When you are adding claim "expires_at" you could also add something like "refresh_at" claim as half timespan between now and "expires_at" value. Such solution would allow to make sure that Access Token won't expire before you will be able to use it.Aggression
I usually allow, thru configuration, I minute for checking if token is expired: for example if the token expires in less than 1 minute I will go ahead and refresh it.Systemic

© 2022 - 2024 — McMap. All rights reserved.