简体   繁体   中英

OWIN ASP.NET - Avoid multiple logins for the same account without Identity in Web Api

I want to know how can I block users to have multiple Refresh Tokens working at the same time. Let me explain:

  • Some user ask for a new Access Token using Credentials.
  • Credentials are okey, so the Authentication servers returns an Access Token and a Refresh Token.
  • When Access Token expires, user have to use the new Refresh Token instead of Credentials to get a new Access Token.

The problem is that if another user log in using the same credentials, another Refresh Token will be generated for the same identity. So, what i want to do is: if somebody log in again using some credentials that have an active refresh token, instead of generating a new one, replace the existing one, or delete it and insert the new one. So the previous user will be disconnected when Access Token expires since Refresh Token wont exist anymore.

Also, how can i implement some service to destroy a Refresh Token in the Authentication Server? So the user can call it to disconnect his account, not just deleting the cookie and wait till it expires.

Here is my code:

Startup.cs:

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        HttpConfiguration config = new HttpConfiguration();

        config.MapHttpAttributeRoutes();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}"
        );

        app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true,

            TokenEndpointPath = new PathString("/auth"),
            AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(1),

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

One more question: how can i throw an Internal Server Error in the catch? Because it's actually returning invalid_grant , but the catch means a Database error, not invalid Credentials or Token.

Thanks for the help, and sorry for the bad english. I hope you understand!

Consider the following:

  1. set access token expiration to small value (minutes / hours)
  2. only issue a refresh token when user logs in using credentials (grant_type=password). This is a new refresh token.
  3. store the 'new' refresh token encrypted in the database, replacing the 'current' refresh token.

You do not have to block users. The access token expires soon enough, just make sure you do not issue a new access token when refreshing the token. You can do this by checking the refresh token. If the encrypted refresh token doesn't match the encrypted refresh token in the database 'invalid_grant' will be returned. The user has only one option: log in again.

If the user logs in using credentials the refresh token is updated (also in the database). This will automatically invalidate the 'old' refresh tokens.

You can implement points 2 and 3 in RefreshTokenProvider.CreateAsync. Some pseudo code:

// using Microsoft.AspNet.Identity;

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

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

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

        // Store the encrypted token in the database
        var currentUser = context.Ticket.Identity.GetUserId();
        TokenRepository.Instance.EncryptAndSaveTokenInDatabase(context.Token, currentUser);
    }
    base.Create(context);
}

About the error, just return invalid_grant . how often do you expect the database to fail? The client will expect to login or either receive an 'invalid_grant'. It knows how to handle that (redirect to login page). The client doesn't have to know there was a database error. If you want additional information, you can log it in the backend.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM