OWIN-Sicherheit - Implementieren von OAuth2-Aktualisierungstoken


80

Ich verwende die Web Visual API 2-Vorlage, die mit Visual Studio 2013 geliefert wird. Sie verfügt über eine OWIN-Middleware für die Benutzerauthentifizierung und dergleichen.

In der habe OAuthAuthorizationServerOptionsich festgestellt, dass der OAuth2-Server so eingerichtet ist, dass Token ausgegeben werden, die in 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 a aktualisiert werden könnenrefresh_token

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 eine geschrieben RefreshTokenProvider, die IAuthenticationTokenProvidergemäß der RefreshTokenProviderEigenschaft auf OAuthAuthorizationServerOptionsKlasse 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 eine anfordert, bearer_tokensende ich jetzt eine refresh_token, was großartig ist.

Wie verwende ich nun dieses refresh_token, um ein neues zu erhalten bearer_token? Vermutlich muss ich eine Anfrage an meinen Token-Endpunkt senden, wobei bestimmte HTTP-Header festgelegt sind.

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

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


7
Es gibt ein großartiges Tutorial zum Implementieren von Aktualisierungstoken mit Owin und OAuth: bitoftech.net/2014/07/16/…
Philip Bergström

Antworten:


75

Ich habe gerade meinen OWIN-Dienst mit Bearer (im Folgenden access_token genannt) implementiert und Tokens aktualisiert. Mein Einblick in diese ist, dass Sie verschiedene Flows verwenden können. Es hängt also von dem Ablauf ab, den Sie verwenden möchten, wie Sie die Ablaufzeiten für access_token und refresh_token festlegen.

Ich werde im Folgenden zwei Flüsse A und B beschreiben (ich schlage vor, Sie möchten Fluss B haben):

A) Die Ablaufzeit von access_token und refresh_token ist dieselbe wie standardmäßig 1200 Sekunden oder 20 Minuten. Für diesen Ablauf muss Ihr Client zuerst client_id und client_secret mit Anmeldedaten senden, um ein access_token, ein refresh_token und eine expiration_time zu erhalten. Mit dem refresh_token ist es jetzt möglich, ein neues access_token für 20 Minuten abzurufen (oder was auch immer Sie das AccessTokenExpireTimeSpan in den OAuthAuthorizationServerOptions auf setzen). Aus dem Grund, dass die Ablaufzeit von access_token und refresh_token gleich ist, ist Ihr Client dafür verantwortlich, vor Ablauf der Ablaufzeit ein neues access_token zu erhalten! Beispielsweise könnte Ihr Client einen Aktualisierungs-POST-Aufruf mit dem Body an Ihren Token-Endpunkt senden (Anmerkung: Sie sollten https in der Produktion verwenden).

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

um nach zB 19 Minuten einen neuen Token zu erhalten, um zu verhindern, dass die Token ablaufen.

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. Nun kommt es zu dem interessanten Teil, der die Ablaufzeit von refresh_token festlegt: 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 POST-Aufruf mit einem refresh_token an Ihren Token-Endpunkt senden, wenn der access_tokenabgelaufen ist. Der Hauptteil des Anrufs 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. Sie sollten daher in Betracht ziehen, Ihre eigene Funktion (z. B. CreateTokenInternal) für den obigen Code zu verwenden. Hier finden Sie Implementierungen verschiedener Flows, einschließlich refresh_token-Flow (ohne jedoch die Ablaufzeit des refresh_token festzulegen).

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

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

Ich hoffe, dies kann einigen anderen helfen, Zeit zu sparen, wenn sie versuchen, OAuth2.0 mit einer Ablaufzeit von refresh_token zu implementieren, die sich von der Ablaufzeit von access_token unterscheidet. Ich konnte keine Beispielimplementierung im Web finden (außer der oben verlinkten von thinktecture) und es dauerte einige Stunden, bis ich für sie funktionierte.

Neue Info: In meinem Fall habe ich zwei verschiedene Möglichkeiten, Token zu erhalten. Eine besteht darin, 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 den 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 versuchen, indem wir einen POST-Aufruf mit einem String-Body im Format application/x-www-form-urlencodedmit den folgenden Daten sendengrant_type=refresh_token&client_id=YOURCLIENTID&refresh_token=YOURREFRESHTOKENGUID


1
In einem Ihrer Kommentare heißt es: "Erwägen Sie, nur den Hash des Handles zu speichern." Sollte dieser Kommentar nicht für die obige Zeile gelten? Das Ticket enthält die Original-Guid, aber wir speichern nur den Hash der Guid in RefreshTokens. Wenn RefreshTokensalso ein Leck durchgesickert ist, kann ein Angreifer diese Informationen nicht verwenden!?
Esskar


1
Wie in Ablauf B beschrieben, können Sie die Ablaufzeit für access_token mithilfe von AccessTokenExpireTimeSpan = TimeSpan.FromMinutes (60) für eine Stunde oder FromWHATEVER für die Zeit festlegen, für die das access_token ablaufen soll. Beachten Sie jedoch, dass bei Verwendung von refresh_token in Ihrem Flow die Ablaufzeit Ihres refresh_token höher sein sollte als die Ihres access_token. So zum Beispiel 24 Stunden für access_token und 2 Monate für refresh_token. Sie können die Ablaufzeit von access_token in der OAuth-Konfiguration festlegen.
Freddy

12
Verwenden Sie keine Guids für Ihre Token oder Hashes von ihnen, es ist nicht sicher. Verwenden Sie den System.Cryptography-Namespace, um ein zufälliges Byte-Array zu generieren und dieses in eine Zeichenfolge zu konvertieren. Andernfalls können Ihre Aktualisierungstoken durch Brute-Force-Angriffe erraten werden.
Bon

1
@Bon Du wirst einen Guid brutal erraten? Ihr Ratenbegrenzer sollte einspringen, bevor der Angreifer auch nur eine Handvoll Anfragen stellen kann. Und wenn nicht, ist es immer noch ein Guid.
Lonix

46

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

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

Dadurch wird jedes Mal ein neues Aktualisierungstoken erstellt und zurückgegeben, auch wenn Sie möglicherweise nur daran interessiert sind, ein neues Zugriffstoken und nicht auch ein neues Aktualisierungstoken zurückzugeben. Zum Beispiel wen, der ein Zugriffstoken anfordert, aber ein Aktualisierungstoken und keine Anmeldeinformationen (Benutzername / Passwort). Gibt es sowieso etwas zu vermeiden?
Mattias

Sie können, aber es ist nicht schön. Der context.OwinContext.Environmententhält einen Microsoft.Owin.Form#collectionSchlüssel, mit dem Sie FormCollectionden Grant-Typ finden und einen entsprechenden Token hinzufügen können. Die Implementierung ist undicht, kann bei zukünftigen Updates jederzeit beschädigt werden, und ich bin mir nicht sicher, ob sie zwischen OWIN-Hosts portierbar ist.
Hvidgaard

3
Sie können die Ausgabe eines neuen Aktualisierungstokens jedes Mal vermeiden, indem Sie den Wert "grant_type" aus dem OwinRequest-Objekt wie folgt lesen: Geben Sie var form = await context.Request.ReadFormAsync(); var grantType = form.GetValue("grant_type"); dann das Aktualisierungstoken aus, wenn der Grant-Typ nicht "refresh_token" ist
Duy

1
@mattias In diesem Szenario möchten Sie immer noch ein neues Aktualisierungstoken zurückgeben. Andernfalls bleibt der Client nach dem ersten Aktualisieren im Stich, da das zweite Zugriffstoken abläuft und er keine Aktualisierung durchführen kann, ohne erneut nach Anmeldeinformationen zu fragen.
Eric Eskildsen

9

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

Sie können context.SerializeTicket () problemlos 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);
    }
}

2

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

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

Wie hilft Hashing in diesem Fall?
Ajaxe

3
@ Ajaxe: Die ursprüngliche Lösung hat die Guid gespeichert. Beim Hashing behalten wir nicht das Klartext-Token, sondern dessen Hash. Wenn Sie die Token beispielsweise in einer Datenbank speichern, ist es besser, den Hash zu speichern. Wenn die Datenbank kompromittiert wird, sind die Token unbrauchbar, solange sie verschlüsselt sind.
Knelis

Nicht nur zum Schutz vor externen Bedrohungen, sondern auch um zu verhindern, dass Mitarbeiter (die Zugriff auf die Datenbank haben) die Token stehlen.
Lonix
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.