web-dev-qa-db-fra.com

OWIN Security - Comment implémenter des jetons d'actualisation OAuth2

J'utilise le modèle Web Api 2 fourni avec Visual Studio 2013 qui dispose d'un middleware OWIN pour l'authentification d'utilisateur, etc.

Dans le OAuthAuthorizationServerOptions, j'ai remarqué que le serveur OAuth2 est configuré pour distribuer des jetons expirant dans 14 jours.

 OAuthOptions = new OAuthAuthorizationServerOptions
 {
      TokenEndpointPath = new PathString("/api/token"),
      Provider = new ApplicationOAuthProvider(PublicClientId,UserManagerFactory) ,
      AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
      AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
      AllowInsecureHttp = true
 };

Cela ne convient pas à mon dernier projet. Je voudrais distribuer des mots bearer_tokens de courte durée qui peuvent être rafraîchis à l'aide d'un refresh_token

J'ai fait beaucoup de recherches sur Google et je ne trouve rien qui soit utile.

Donc, c'est à quel point j'ai réussi à obtenir. J'ai maintenant atteint le point de "WTF do I now".

J'ai écrit un RefreshTokenProvider qui implémente IAuthenticationTokenProvider conformément à la propriété RefreshTokenProvider sur OAuthAuthorizationServerOptions classe:

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

        public async Task CreateAsync(AuthenticationTokenCreateContext context)
        {
            var guid = Guid.NewGuid().ToString();


            _refreshTokens.TryAdd(guid, context.Ticket);

            // hash??
            context.SetToken(guid);
        }

        public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        {
            AuthenticationTicket ticket;

            if (_refreshTokens.TryRemove(context.Token, out ticket))
            {
                context.SetTicket(ticket);
            }
        }

        public void Create(AuthenticationTokenCreateContext context)
        {
            throw new NotImplementedException();
        }

        public void Receive(AuthenticationTokenReceiveContext context)
        {
            throw new NotImplementedException();
        }
    }

    // Now in my Startup.Auth.cs
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
        TokenEndpointPath = new PathString("/api/token"),
        Provider = new ApplicationOAuthProvider(PublicClientId,UserManagerFactory) ,
        AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
        AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(2),
        AllowInsecureHttp = true,
        RefreshTokenProvider = new RefreshTokenProvider() // This is my test
    };

Alors maintenant, quand quelqu'un demande un bearer_token J'envoie maintenant un refresh_token, qui est super.

Alors maintenant, comment puis-je utiliser ce refresh_token pour obtenir un nouveau bearer_token, je suppose que je dois envoyer une demande à mon noeud final avec un ensemble d'en-têtes HTTP spécifique?

Penser à voix haute pendant que je tape ... Devrais-je gérer l'expiration refresh_token dans mon SimpleRefreshTokenProvider? Comment un client obtiendrait-il un nouveau refresh_token?

Je pourrais vraiment faire un peu de lecture/documentation parce que je ne veux pas me tromper et que j'aimerais suivre une sorte de norme.

75
SimonGates

Je viens juste de mettre en œuvre mon service OWIN avec Bearer (appelé access_token dans la suite) et les jetons d'actualisation. Mon idée à ce sujet est que vous pouvez utiliser différents flux. Cela dépend donc du flux que vous souhaitez utiliser pour définir les délais d'expiration access_token et refresh_token.

Je décrirai deux flux [~ # ~] a [~ # ~] et [~ # ~] b [~ # ~] dans la suite (je suggère ce que vous voulez avoir comme flux B):

A) l'heure d'expiration de access_token et de refresh_token est identique à celle par défaut de 1200 secondes ou 20 minutes. Ce flux nécessite que votre client envoie d’abord client_id et client_secret avec les données de connexion pour obtenir un code access_token, refresh_token et expiration_time. Avec refresh_token, il est maintenant possible d'obtenir un nouveau access_token pendant 20 minutes (ou ce que vous définissiez comme suit: AccessTokenExpireTimeSpan dans les options OAuthAuthorizationServerOptions). Étant donné que les dates d'expiration de access_token et de refresh_token sont identiques, votre client est responsable de l'obtention d'un nouveau access_token avant l'heure d'expiration! Par exemple. votre client peut envoyer un rafraîchissement POST vers votre noeud final avec le corps (remarque: vous devez utiliser https en production)

grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xxxxx

pour obtenir un nouveau jeton après par exemple 19 minutes pour empêcher l'expiration des jetons.

B) dans ce flux, vous voulez avoir une expiration à court terme pour votre access_token et une expiration à long terme pour votre refresh_token. Supposons, à des fins de test, que le paramètre access_token expire dans 10 secondes (AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10)) et le paramètre refresh_token sur 5 minutes. Maintenant, il s’agit de la partie intéressante qui définit le délai d’expiration de refresh_token: vous le faites dans la fonction createAsync de la classe SimpleRefreshTokenProvider comme suit:

var guid = Guid.NewGuid().ToString();


        //copy properties and set the desired lifetime of refresh token
        var refreshTokenProperties = new AuthenticationProperties(context.Ticket.Properties.Dictionary)
        {
            IssuedUtc = context.Ticket.Properties.IssuedUtc,
            ExpiresUtc = DateTime.UtcNow.AddMinutes(5) //SET DATETIME to 5 Minutes
            //ExpiresUtc = DateTime.UtcNow.AddMonths(3) 
        };
        /*CREATE A NEW TICKET WITH EXPIRATION TIME OF 5 MINUTES 
         *INCLUDING THE VALUES OF THE CONTEXT TICKET: SO ALL WE 
         *DO HERE IS TO ADD THE PROPERTIES IssuedUtc and 
         *ExpiredUtc to the TICKET*/
        var refreshTokenTicket = new AuthenticationTicket(context.Ticket.Identity, refreshTokenProperties);

        //saving the new refreshTokenTicket to a local var of Type ConcurrentDictionary<string,AuthenticationTicket>
        // consider storing only the hash of the handle
        RefreshTokens.TryAdd(guid, refreshTokenTicket);            
        context.SetToken(guid);

Votre client peut maintenant envoyer un appel POST avec un refresh_token au noeud final de votre jeton lorsque le access_token Est expiré. Le corps de l'appel peut ressembler à ceci: grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xx

Une chose importante est que vous souhaiterez peut-être utiliser ce code non seulement dans votre fonction CreateAsync, mais également dans votre fonction Create. Par conséquent, vous devriez envisager d’utiliser votre propre fonction (appelée par exemple CreateTokenInternal) pour le code ci-dessus. Ici, vous pouvez trouver des implémentations de différents flux, y compris le flux refresh_token (mais sans définir le délai d'expiration de refresh_token)

Voici un exemple d'implémentation de IAuthenticationTokenProvider sur github (avec la définition de la date d'expiration de l'élément refresh_token)

Je suis désolé de ne pouvoir vous aider avec d'autres documents que le OAuth Specs et la documentation de l'API Microsoft. J'enverrais les liens ici, mais ma réputation ne me laisse pas en poster plus de 2 liens....

J'espère que cela aidera d'autres à gagner du temps lorsque vous essayez d'implémenter OAuth2.0 avec un délai d'expiration refresh_token différent du délai d'expiration access_token. Je n'ai pas trouvé d'exemple d'implémentation sur le Web (à l'exception de celui de thinktecture lié ci-dessus) et il m'a fallu quelques heures d'enquête pour que cela fonctionne pour moi.

Nouvelle information: Dans mon cas, j'ai deux possibilités différentes pour recevoir des jetons. L'une consiste à recevoir un code d'accès valide. Là, je dois envoyer un appel POST avec un corps de chaîne au format application/x-www-form-urlencoded avec les données suivantes

client_id=YOURCLIENTID&grant_type=password&username=YOURUSERNAME&password=YOURPASSWORD

Deuxièmement, si access_token n'est plus valide, nous pouvons essayer refresh_token en envoyant un appel POST avec un corps de chaîne au format application/x-www-form-urlencoded Avec les données suivantes grant_type=refresh_token&client_id=YOURCLIENTID&refresh_token=YOURREFRESHTOKENGUID

74
Freddy

Vous devez implémenter RefreshTokenProvider. Commencez par créer une classe pour RefreshTokenProvider ie.

public class ApplicationRefreshTokenProvider : AuthenticationTokenProvider
{
    public override void Create(AuthenticationTokenCreateContext context)
    {
        // Expiration time in seconds
        int expire = 5*60;
        context.Ticket.Properties.ExpiresUtc = new DateTimeOffset(DateTime.Now.AddSeconds(expire));
        context.SetToken(context.SerializeTicket());
    }

    public override void Receive(AuthenticationTokenReceiveContext context)
    {
        context.DeserializeTicket(context.Token);
    }
}

Ajoutez ensuite une instance à OAuthOptions.

OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/authenticate"),
    Provider = new ApplicationOAuthProvider(),
    AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(expire),
    RefreshTokenProvider = new ApplicationRefreshTokenProvider()
};
41
Mauricio

Je ne pense pas que vous devriez utiliser un tableau pour maintenir les jetons. Ni vous avez besoin d'un guid en guise de jeton.

Vous pouvez facilement utiliser context.SerializeTicket ().

Voir mon code ci-dessous.

public class RefreshTokenProvider : IAuthenticationTokenProvider
{
    public async Task CreateAsync(AuthenticationTokenCreateContext context)
    {
        Create(context);
    }

    public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
    {
        Receive(context);
    }

    public void Create(AuthenticationTokenCreateContext context)
    {
        object inputs;
        context.OwinContext.Environment.TryGetValue("Microsoft.Owin.Form#collection", out inputs);

        var grantType = ((FormCollection)inputs)?.GetValues("grant_type");

        var grant = grantType.FirstOrDefault();

        if (grant == null || grant.Equals("refresh_token")) return;

        context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(Constants.RefreshTokenExpiryInDays);

        context.SetToken(context.SerializeTicket());
    }

    public void Receive(AuthenticationTokenReceiveContext context)
    {
        context.DeserializeTicket(context.Token);

        if (context.Ticket == null)
        {
            context.Response.StatusCode = 400;
            context.Response.ContentType = "application/json";
            context.Response.ReasonPhrase = "invalid token";
            return;
        }

        if (context.Ticket.Properties.ExpiresUtc <= DateTime.UtcNow)
        {
            context.Response.StatusCode = 401;
            context.Response.ContentType = "application/json";
            context.Response.ReasonPhrase = "unauthorized";
            return;
        }

        context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(Constants.RefreshTokenExpiryInDays);
        context.SetTicket(context.Ticket);
    }
}
8
peeyush rahariya

réponse de Freddy m'a beaucoup aidé à faire en sorte que cela fonctionne. Par souci d'exhaustivité, voici comment vous pouvez implémenter le hachage du jeton:

private string ComputeHash(Guid input)
{
    byte[] source = input.ToByteArray();

    var encoder = new SHA256Managed();
    byte[] encoded = encoder.ComputeHash(source);

    return Convert.ToBase64String(encoded);
}

Dans CreateAsync:

var guid = Guid.NewGuid();
...
_refreshTokens.TryAdd(ComputeHash(guid), refreshTokenTicket);
context.SetToken(guid.ToString());

ReceiveAsync:

public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
    Guid token;

    if (Guid.TryParse(context.Token, out token))
    {
        AuthenticationTicket ticket;

        if (_refreshTokens.TryRemove(ComputeHash(token), out ticket))
        {
            context.SetTicket(ticket);
        }
    }
}
2
Knelis