Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

OWIN ASP.NET - Cant generate Access Token using Refresh Token if Access Token is expired

When I try to generate an Access Token using the Refresh Token before Access Token expires, the system generate a new one and everything is okey. But if the Access Token is expired, the request return invalid_grant.

Isn't the Validated() method from GrantRefreshToken generate the Access Token using the identity I stored in the dictionary from the previously Access Token?

And how can I block clients from asking for new Access Token using the same Refresh Token if the last one isn't expired yet?

Here is my code:

Startup.cs:

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true,

            TokenEndpointPath = new PathString("/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(5),

            Provider = new OAuthProvider(),
            RefreshTokenProvider = new RefreshTokenProvider()
        });
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
        app.UseWebApi(config);
    }
}

OAuthProvider.cs:

public class OAuthProvider : OAuthAuthorizationServerProvider
{
    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        context.Validated();
        return Task.FromResult<object>(null);
    }

    public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        if (context.UserName == "admin" && context.Password == "123456")
        {
            var claimsIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));

            var ticket = new AuthenticationTicket(claimsIdentity, null);
            context.Validated(ticket);
        }
        return Task.FromResult<object>(null);
    }

    public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
    {
        context.Validated(context.Ticket);
        return Task.FromResult<object>(null);
    }
}

RefreshTokenProvider.cs:

public class RefreshTokenProvider : AuthenticationTokenProvider
{
    private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>();

    public override Task CreateAsync(AuthenticationTokenCreateContext context)
    {
        var guid = Guid.NewGuid().ToString();
        _refreshTokens.TryAdd(guid, context.Ticket);

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

    public override Task ReceiveAsync(AuthenticationTokenReceiveContext context)
    {
        if (_refreshTokens.TryRemove(context.Token, out AuthenticationTicket ticket))
        {
            context.SetTicket(ticket);
        }
        return Task.FromResult<object>(null);
    }
}

Sorry for bad english, I hope you understand!

EDIT:

Well, I modified the code to implement Database support and set the Refresh Token expiration to 5 minutes. The expiration is small for testing purposes.

Here is the result:

OAuthProvider.cs:

public class OAuthProvider : OAuthAuthorizationServerProvider
{
    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        context.Validated();
        return Task.FromResult<object>(null);
    }

    public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        try
        {
            var account = AccountRepository.Instance.GetByUsername(context.UserName);
            if (account != null && Global.VerifyHash(context.Password, account.Password))
            {
                var claimsIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
                claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, account.Username));
                claimsIdentity.AddClaim(new Claim("DriverId", account.DriverId.ToString()));

                var newTicket = new AuthenticationTicket(claimsIdentity, null);
                context.Validated(newTicket);
            }
        }
        catch { }

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

    public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
    {
        context.Validated();
        return Task.FromResult<object>(null);
    }
}

RefreshTokenProvider.cs:

public class RefreshTokenProvider : AuthenticationTokenProvider
{
    public override Task CreateAsync(AuthenticationTokenCreateContext context)
    {
        var refreshToken = new TokenModel()
        {
            Subject = context.Ticket.Identity.Name,
            Token = GenerateToken(),
            IssuedUtc = DateTime.UtcNow,
            ExpiresUtc = DateTime.UtcNow.AddMinutes(5)
        };

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

        refreshToken.Ticket = context.SerializeTicket();

        try
        {
            TokenRepository.Instance.Insert(refreshToken);
            context.SetToken(refreshToken.Token);
        }
        catch { }

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

    public override Task ReceiveAsync(AuthenticationTokenReceiveContext context)
    {
        try
        {
            var refreshToken = TokenRepository.Instance.Get(context.Token);
            if (refreshToken != null)
            {
                if (TokenRepository.Instance.Delete(refreshToken))
                {
                    context.DeserializeTicket(refreshToken.Ticket);
                }
            }
        }
        catch { }

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

    private string GenerateToken()
    {
        HashAlgorithm hashAlgorithm = new SHA256CryptoServiceProvider();

        byte[] byteValue = Encoding.UTF8.GetBytes(Guid.NewGuid().ToString("N"));
        byte[] byteHash = hashAlgorithm.ComputeHash(byteValue);

        return Convert.ToBase64String(byteHash);
    }
}

Thanks for the support!

like image 908
Julián Pera Avatar asked Nov 24 '25 23:11

Julián Pera


1 Answers

It seems that the refresh token has the same expiration time as the access token. So you'll need to extend the expiration of the refresh token:

public override Task CreateAsync(AuthenticationTokenCreateContext context)
{
    var form = context.Request.ReadFormAsync().Result;
    var grantType = form.GetValues("grant_type");

    if (grantType[0] != "refresh_token")
    {
        ...

        // One day
        int expire = 24 * 60 * 60;
        context.Ticket.Properties.ExpiresUtc = new DateTimeOffset(DateTime.Now.AddSeconds(expire));
    }
    base.Create(context);
}

-- Update --

I've updated the code to answer the question in your comment. It's more than you've asked for. But I think it will help to explain.

It all depends on the requirement and chosen strategy. To explain the code:

Each time an access token is issued you will also hit this piece of code which will add a refresh token. In this strategy I will issue a new refresh token only when grant_type is not 'refresh_token'. This means that at some point the refresh token expires and the user has to login again.

In the example the user has to login every day again. But if the user logs in before the refresh_token expires, a new refresh token will be issued. That way the refresh token has an absolute expiration time, forcing the user to login at least once a day.

If you want 'sliding expiration', you can add a refresh token every time you issue an access token. Please note that the user may never have to login again, unless the refresh token expires before refreshing.

In any case I wouldn't block users from creating access tokens when grant_type is not refresh_token. Since that is user interaction. The access token has a very limited window (think of hours or minutes, not days), so it will expire soon enough. You do not want to validate an access token each time you receive it in a header, since this means you'll need to check the database for each call.

Instead think of a strategy to invalidate 'old' refresh tokens. You could save the hashed version of the last refresh token in the database. If it doesn't match then respond with an 'invalid_grant' error.


Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!