web-dev-qa-db-de.com

OWIN-Sicherheit - Implementieren von OAuth2-Aktualisierungstoken

Ich verwende die Web-API-2-Vorlage, die in Visual Studio 2013 enthalten ist. Sie enthält eine OWIN-Middleware für die Benutzerauthentifizierung und dergleichen.

Im OAuthAuthorizationServerOptions ist mir aufgefallen, dass der OAuth2-Server so eingerichtet ist, dass er Token austeilt, die nach 14 Tagen ablaufen

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

Dies ist nicht für mein aktuelles Projekt geeignet. Ich möchte kurzlebige bearer_tokens verteilen, die mit einem refresh_token Aktualisiert werden können.

Ich habe viel gegoogelt und kann nichts hilfreiches finden.

So weit bin ich also gekommen. Ich habe jetzt den Punkt "WTF mache ich jetzt" erreicht.

Ich habe ein RefreshTokenProvider geschrieben, das IAuthenticationTokenProvider gemäß der Eigenschaft RefreshTokenProvider für die Klasse OAuthAuthorizationServerOptions implementiert:

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

Wenn also jemand einen bearer_token Anfordert, sende ich jetzt einen refresh_token, Was großartig ist.

Wie verwende ich nun dieses refresh_token, um einen neuen bearer_token Zu erhalten? Vermutlich muss ich eine Anfrage an meinen Token-Endpunkt mit bestimmten HTTP-Headern senden.

Ich muss nur laut nachdenken, während ich tippe ... Soll ich den Ablauf von refresh_token in meinem SimpleRefreshTokenProvider behandeln? Wie würde ein Kunde einen neuen refresh_token Erhalten?

Ich könnte wirklich etwas Lesematerial/Dokumentation gebrauchen, weil ich das nicht falsch verstehen möchte und eine Art Standard befolgen möchte.

75
SimonGates

Habe gerade meinen OWIN Service mit Bearer (im Folgenden access_token genannt) und Refresh Tokens implementiert. Meine Erkenntnis ist, dass Sie verschiedene Flows verwenden können. Es hängt also von dem Ablauf ab, den Sie verwenden möchten, um die Ablaufzeiten für access_token und refresh_token festzulegen.

Ich werde zwei Flüsse beschreiben [~ # ~] ein [~ # ~] und [~ # ~] b [~ # ~] im Folgenden (ich schlage vor, Sie möchten Flow B haben):

A) Die Ablaufzeit von access_token und refresh_token entspricht der Standardeinstellung von 1200 Sekunden oder 20 Minuten. In diesem Ablauf muss Ihr Client zuerst client_id und client_secret mit Anmeldedaten senden, um ein access_token, refresh_token und expiration_time zu erhalten. Mit dem refresh_token ist es jetzt möglich, ein neues access_token für 20 Minuten abzurufen (oder wie auch immer Sie das AccessTokenExpireTimeSpan in den OAuthAuthorizationServerOptions festlegen). Aus dem Grund, dass die Ablaufzeit von access_token und refresh_token gleich ist, ist Ihr Client dafür verantwortlich, vor Ablaufzeitpunkt ein neues access_token zu erhalten! Z.B. Ihr Client könnte einen Refresh POST an Ihren Token-Endpunkt mit dem Body senden (Anmerkung: Sie sollten https in der Produktion verwenden)

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

um ein neues Token zu erhalten, nachdem z.B. 19 Minuten, um das Ablaufen der Token zu verhindern.

B) In diesem Ablauf möchten Sie einen kurzfristigen Ablauf für Ihr access_token und einen langfristigen Ablauf für Ihr refresh_token haben. Nehmen wir zu Testzwecken an, Sie setzen das access_token so, dass es in 10 Sekunden abläuft (AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10)) und das refresh_token auf 5 Minuten. Jetzt kommt der interessante Teil zum Festlegen der Ablaufzeit von refresh_token: Sie tun dies in Ihrer createAsync-Funktion in der SimpleRefreshTokenProvider-Klasse wie folgt:

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

Jetzt kann Ihr Client einen Aufruf POST mit einem Refresh_Token an Ihren Token-Endpunkt senden, wenn der access_token Abgelaufen ist. Der Hauptteil des Aufrufs kann folgendermaßen aussehen: grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xx

Eine wichtige Sache ist, dass Sie diesen Code möglicherweise nicht nur in Ihrer CreateAsync-Funktion, sondern auch in Ihrer Create-Funktion verwenden möchten. Daher sollten Sie in Betracht ziehen, für den obigen Code eine eigene Funktion (z. B. CreateTokenInternal) zu verwenden. Hier finden Sie Implementierungen verschiedener Flows, einschließlich des Flows "refresh_token" (jedoch ohne die Ablaufzeit des Flows "refresh_token" festzulegen)

Hier ist eine Beispielimplementierung von IAuthenticationTokenProvider auf github (mit dem Festlegen der Ablaufzeit des refresh_token)

Es tut mir leid, dass ich nicht weiterhelfen kann als mit den OAuth Specs und der Microsoft API Documentation. Ich würde die Links hier posten, aber mein Ruf lässt mich nicht mehr als 2 posten links ....

Ich hoffe, dies kann einigen anderen helfen, Zeit zu sparen, wenn sie versuchen, OAuth2.0 mit einer anderen Ablaufzeit für refresh_token als die Ablaufzeit für access_token zu implementieren. Ich konnte im Web keine Beispielimplementierung finden (mit Ausnahme der oben verlinkten von thinktecture), und ich habe einige Stunden lang nachgeforscht, bis es für mich funktioniert hat.

Neue Info: In meinem Fall habe ich zwei verschiedene Möglichkeiten, Token zu erhalten. Eine ist, ein gültiges access_token zu erhalten. Dort muss ich einen POST Aufruf mit einem String-Body im Format application/x-www-form-urlencoded mit folgenden Daten senden

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

Zweitens: Wenn access_token nicht mehr gültig ist, können wir das refresh_token testen, indem wir einen POST mit einem String-Body im Format application/x-www-form-urlencoded Mit den folgenden Daten grant_type=refresh_token&client_id=YOURCLIENTID&refresh_token=YOURREFRESHTOKENGUID Senden.

74
Freddy

Sie müssen RefreshTokenProvider implementieren. Erstellen Sie zuerst eine Klasse für RefreshTokenProvider, z.

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

Fügen Sie dann eine Instanz zu OAuthOptions hinzu.

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

Ich denke nicht, dass Sie ein Array verwenden sollten, um Token zu verwalten. Sie brauchen auch keine Anleitung als Token.

Sie können einfach context.SerializeTicket () verwenden.

Siehe meinen folgenden Code.

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

Freddys Antwort hat mir sehr geholfen, das zum Laufen zu bringen. Der Vollständigkeit halber sehen Sie hier, wie Sie das Hashing des Tokens implementieren können:

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

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

    return Convert.ToBase64String(encoded);
}

In 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