Search code examples
angularjsoauth-2.0owinasp.net-authorizationrefresh-token

OWIN Security - OAuth2 Refresh Token - How to include Refresh Token's expiration


After following this guide, I have a functional authorization server. My app receives the following response:

{
  "access_token": "wNl5VT4UuMwMpFOkoMTUscO7XgS96ktzeE_FoAcKpugLD4VrZGZ0HgGvgfgbY1axOPsdxQ5bzB2hA5jKtWNZdq21OvKU4LLnRXXhSHbOWLnbVSAVfkrX1n_Vv_TgWncOheK3WJ7OkELoLUkwYYQCzX712BVmblLkSjsjpvX94VywnUv16z_cnIPsUAHjHlLsEB5cFvyItGQCU2KRq-A3j70l2zBNnu9N9s3dUoidP8eiW6QpDWOnSOXKX9DG0vd6lQnUtgt7mHeP8Det55QvptRTPKABnKxB9_0QvSGa6I8",
  "token_type": "bearer",
  "expires_in": 1799,
  "refresh_token": "a39bca5a33ef4c9fb77c3652f17152db",
  "as:client_id": "myClient",
  "userName": "myUser",
  ".issued": "Wed, 22 Feb 2017 17:05:24 GMT",
  ".expires": "Wed, 22 Feb 2017 17:35:24 GMT"
}

My problem is the refresh token's expiration isn't included. I know I could catch this as an exception, but it would be better to avoid that if possible first. How can I include the expiration date/time when the refresh token is created in the IAuthenticationTokenProvider.CreateAsync(...) method which occurs after the OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials(...) and OAuthAuthorizationServerProvider.TokenEndpoint(...) methods.

Authorization Provider

public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        ...
    }

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");
        var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin") ?? "*";

        context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });

        using (AuthRepository repo = new AuthRepository())
        {
            IdentityUser user = await repo.FindUser(context.UserName, context.Password);

            if (user == null)
            {
                context.SetError("invalid_grant", "The user name or password is incorrect.");
                return;
            }
        }

        var identity = new ClaimsIdentity(context.Options.AuthenticationType);
        identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
        identity.AddClaim(new Claim("sub", context.UserName));
        identity.AddClaim(new Claim("role", "user"));

        var props = new AuthenticationProperties(new Dictionary<string, string>
        {
            {
                "as:client_id", context.ClientId ?? string.Empty
            },
            {
                "userName", context.UserName
            }
        });

        var ticket = new AuthenticationTicket(identity, props);
        context.Validated(ticket);
    }

    public override Task TokenEndpoint(OAuthTokenEndpointContext context)
    {
        foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
        {
            context.AdditionalResponseParameters.Add(property.Key, property.Value);
        }

        return Task.FromResult<object>(null);
    }
}

Refresh Token Provider

public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
{
    public async Task CreateAsync(AuthenticationTokenCreateContext context)
    {
        var clientid = context.Ticket.Properties.Dictionary["as:client_id"];

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

        var refreshTokenId = Guid.NewGuid().ToString("n");

        using (AuthRepository repo = new AuthRepository())
        {
            var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");

            var token = new RefreshToken
            {
                Id = Helper.GetHash(refreshTokenId),
                ClientId = clientid,
                Subject = context.Ticket.Identity.Name,
                IssuedUtc = DateTime.UtcNow,
                ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
            };

            context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
            context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc;

            token.ProtectedTicket = context.SerializeTicket();

            var result = await repo.AddRefreshToken(token);

            if (result)
            {
                context.SetToken(refreshTokenId);
            }
        }
    }

    public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
    {
        ...
    }
}

Solution

  • I created a DTO class for the refresh token to be serialized and returned in place of the refreshTokenId variable.

    public class RefreshTokenDTO
    {
        [JsonProperty("token")]
        public string Token { get; set; }
    
        [JsonProperty("issued")]
        public DateTime Issued { get; set; }
    
        [JsonProperty("expires")]
        public DateTime Expires { get; set; }
    }
    

    and changed the SimpleRefreshTokenProvider.CreateAsync method

    public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
    {
        public async Task CreateAsync(AuthenticationTokenCreateContext context)
        {
            ...
    
            using (AuthRepository repo = new AuthRepository())
            {
                ...
    
                if (result)
                {
                    //context.SetToken(refreshTokenId);
    
                    var rToken = new RefreshTokenDTO
                    {
                        Token = refreshTokenId,
                        Issued = token.IssuedUtc,
                        Expires = token.ExpiresUtc
                    };
    
                    var json = JsonConvert.SerializeObject(rToken);
    
                    context.SetToken(json);
                }
            }
        }
    

    The new response is

    {
      "access_token": "dRJiWTT03KlapjqENhDeIa-f35rE4eRDn6DL60laVeKysUQkOHE2Zu6ySYYFmq53jN5KQL3A7Aj6obM3oe5iYtHbfeueODcMitzGEBXMph1-791v86VWgdvW4EtvIbhQnLq8Acr6K_Nt5qDQWTCD5DETjr6h0OenbZtDIQak3ycUUPEU5m1Ws3b2qZbw62-DSUzaOZ2TYhvdkRdFg_zWhLIDd9vIWiGXcWxTr415P5a7d1s_K-8vmq5q-I5nEUCmJshmCWIU_4oPKz7sQLHhy79JE9Z00BfdidzFbYaA9yo",
      "token_type": "bearer",
      "expires_in": 1799,
      "refresh_token": "{\"Token\":\"f94f5ba1494644e388f0ec4862a81909\",\"Issued\":\"2017-02-22T17:48:03.771661Z\",\"Expires\":\"2017-03-04T17:48:03.771661Z\"}",
      "as:client_id": "epAndroid",
      "as:issued": "",
      "userName": "pm00115905",
      ".issued": "Wed, 22 Feb 2017 17:48:02 GMT",
      ".expires": "Wed, 22 Feb 2017 18:18:02 GMT"
    }
    

    and on the client side I created a JsonConverter

    public class RefreshTokenJsonConverter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            return JsonConvert.DeserializeObject<RefreshToken>(reader.Value.ToString());
        }
    
        public override bool CanConvert(Type objectType)
        {
            return true;
        }
    }