Bezpieczeństwo OWIN - jak zaimplementować tokeny odświeżania OAuth2

80

Używam szablonu Web Api 2, który jest dostarczany z programem Visual Studio 2013, zawiera oprogramowanie pośredniczące OWIN do uwierzytelniania użytkowników i tym podobne.

W OAuthAuthorizationServerOptionszauważyłem, że serwer OAuth2 jest skonfigurowany do wydawania tokenów, które wygasają za 14 dni

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

To nie jest odpowiednie dla mojego najnowszego projektu. Chciałbym wręczyć krótkotrwałe bearer_tokens, które można odświeżyć za pomocą plikurefresh_token

Dużo szukałem w Google i nie mogę znaleźć nic pomocnego.

A więc tak daleko udało mi się dotrzeć. Dotarłem teraz do punktu „WTF do I teraz”.

Napisałem, RefreshTokenProviderże implementuje się IAuthenticationTokenProviderzgodnie z RefreshTokenProviderwłaściwością w OAuthAuthorizationServerOptionsklasie:

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

Więc teraz, kiedy ktoś prosi o a bearer_token, teraz wysyłam refresh_token, co jest świetne.

Jak więc teraz użyć tego refresh_token, aby uzyskać nowy bearer_token, prawdopodobnie muszę wysłać żądanie do mojego punktu końcowego tokenu z określonymi określonymi nagłówkami HTTP?

Po prostu głośno myślę, kiedy piszę ... Czy powinienem obsłużyć wygaśnięcie tokena refresh_token w moim SimpleRefreshTokenProvider? W jaki sposób klient uzyskałby nowego refresh_token?

Naprawdę przydałby mi się materiał do czytania / dokumentacja, ponieważ nie chcę tego pomylić i chciałbym przestrzegać jakiegoś standardu.

SimonGates
źródło
7
Istnieje świetny samouczek dotyczący wdrażania tokenów odświeżania przy użyciu Owin i OAuth: bitoftech.net/2014/07/16/ ...
Philip Bergström

Odpowiedzi:

76

Właśnie zaimplementowałem moją usługę OWIN z elementem Bearer (nazywanym dalej access_token) i odświeżaniem tokenów. Mój wgląd w to jest taki, że możesz używać różnych przepływów. Zależy to więc od przepływu, którego chcesz użyć, jak ustawić czas wygaśnięcia access_token i refresh_token.

W dalszej części opiszę dwa przepływy A i B (proponuję, aby mieć przepływ B):

A) Czas wygaśnięcia access_token i refresh_token jest taki sam, jak domyślnie 1200 sekund lub 20 minut. Ten przepływ wymaga, aby Twój klient najpierw wysłał client_id i client_secret z danymi logowania, aby uzyskać access_token, refresh_token i expiration_time. Dzięki refresh_token można teraz uzyskać nowy access_token na 20 minut (lub cokolwiek ustawisz AccessTokenExpireTimeSpan w OAuthAuthorizationServerOptions na). Z tego powodu, że czas wygaśnięcia access_token i refresh_token jest taki sam, twój klient jest odpowiedzialny za uzyskanie nowego access_token przed upływem czasu! Np. Twój klient może wysłać wywołanie odświeżającego POST do punktu końcowego tokena z treścią (uwaga: w produkcji należy używać https)

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

otrzymać nowy token po np. 19 minutach, aby zapobiec wygaśnięciu tokenów.

B) w tym przepływie chcesz mieć krótkoterminowe wygaśnięcie access_token i długoterminowe wygaśnięcie dla refresh_token. Załóżmy, że dla celów testowych ustawiłeś access_token na wygaśnięcie za 10 sekund ( AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10)), a refresh_token na 5 minut. Teraz chodzi o interesującą część ustawiania czasu wygaśnięcia refresh_token: Robisz to w swojej funkcji createAsync w klasie SimpleRefreshTokenProvider w następujący sposób:

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

Teraz Twój klient może wysłać wywołanie POST z refresh_tokenem do punktu końcowego tokenu, gdy access_tokenwygasł. Część treści wywołania może wyglądać następująco:grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xx

Ważną rzeczą jest to, że możesz chcieć użyć tego kodu nie tylko w funkcji CreateAsync, ale także w funkcji Create. Dlatego powinieneś rozważyć użycie własnej funkcji (np. O nazwie CreateTokenInternal) dla powyższego kodu. Tutaj możesz znaleźć implementacje różnych przepływów, w tym przepływ refresh_token (ale bez ustawiania czasu wygaśnięcia refresh_token)

Oto jedna przykładowa implementacja IAuthenticationTokenProvider na github (z ustawieniem czasu wygaśnięcia refresh_token)

Przykro mi, że nie mogę pomóc z innymi materiałami niż specyfikacje OAuth i dokumentacja Microsoft API. Zamieszczałbym tutaj linki, ale moja reputacja nie pozwala mi publikować więcej niż 2 linków ....

Mam nadzieję, że może to pomóc innym w zaoszczędzeniu czasu podczas próby zaimplementowania OAuth2.0 z czasem wygaśnięcia refresh_token innym niż czas wygaśnięcia access_token. Nie mogłem znaleźć przykładowej implementacji w sieci (poza tą z Thinktecture, do której link znajduje się powyżej) i zajęło mi to kilka godzin, zanim zadziałało.

Nowe informacje: W moim przypadku mam dwie różne możliwości otrzymania tokenów. Jednym z nich jest otrzymanie ważnego access_token. Tam muszę wysłać wywołanie POST z treścią String w formacie application / x-www-form-urlencoded z następującymi danymi

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

Po drugie, jeśli access_token nie jest już ważny, możemy wypróbować refresh_token, wysyłając wywołanie POST z treścią String w formacie application/x-www-form-urlencodedz następującymi danymigrant_type=refresh_token&client_id=YOURCLIENTID&refresh_token=YOURREFRESHTOKENGUID

Freddy
źródło
1
jeden z twoich komentarzy mówi „rozważ przechowywanie tylko skrótu uchwytu”, czy nie powinien on odnosić się do powyższej linii? Bilet zawiera oryginalny guid, ale przechowujemy tylko hash guid RefreshTokens, więc jeśli RefreshTokenszostanie ujawniony, atakujący nie może użyć tych informacji !?
esskar
1
Jak opisano w przepływie B, możesz ustawić czas wygaśnięcia access_token za pomocą AccessTokenExpireTimeSpan = TimeSpan.FromMinutes (60) przez jedną godzinę lub FromWHATEVER na czas, w którym access_token ma wygaśnąć. Ale pamiętaj, że jeśli używasz refresh_token w swoim przepływie, czas wygaśnięcia twojego refresh_token powinien być wyższy niż twój access_token. Na przykład 24 godziny na access_token i 2 miesiące na refresh_token. Możesz ustawić czas wygaśnięcia access_token w konfiguracji OAuth.
Freddy
12
Nie używaj Guidów do swoich tokenów ani ich skrótów, nie jest to bezpieczne. Użyj przestrzeni nazw System.Cryptography, aby wygenerować losową tablicę bajtów i przekonwertować ją na ciąg. W przeciwnym razie twoje żetony odświeżenia mogą zostać odgadnięte przez ataki brutalnej siły.
Bon
1
@Bon Będziesz brutalnie-siłą-odgadnąć Guid? Twój ogranicznik szybkości powinien zadziałać, zanim napastnik będzie mógł opublikować nawet kilka żądań. A jeśli nie, nadal jest to Guid.
lonix
46

Musisz zaimplementować RefreshTokenProvider . Najpierw utwórz klasę dla RefreshTokenProvider tj.

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

Następnie dodaj wystąpienie do OAuthOptions .

OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/authenticate"),
    Provider = new ApplicationOAuthProvider(),
    AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(expire),
    RefreshTokenProvider = new ApplicationRefreshTokenProvider()
};
Mauricio
źródło
Spowoduje to utworzenie i zwrócenie nowego tokenu odświeżania za każdym razem, nawet jeśli możesz być zainteresowany tylko zwróceniem nowego tokenu dostępu, a nie nowego tokenu odświeżania. Na przykład wen wywołuje token dostępu, ale z tokenem odświeżania, a nie poświadczeniami (nazwa użytkownika / hasło). Czy w ogóle można tego uniknąć?
Mattias
Możesz, ale to nie jest ładne. context.OwinContext.EnvironmentZawiera Microsoft.Owin.Form#collectionklucz, który daje FormCollection, gdzie można odnaleźć typ dotacji i dodać token odpowiednio. Wycieka implementacja, może się zepsuć w dowolnym momencie przy przyszłych aktualizacjach i nie jestem pewien, czy jest przenoszona między hostami OWIN.
hvidgaard
3
możesz uniknąć wydawania nowego tokena odświeżania za każdym razem, odczytując wartość „grant_type” z obiektu OwinRequest, na przykład: var form = await context.Request.ReadFormAsync(); var grantType = form.GetValue("grant_type"); następnie wystaw token odświeżania, jeśli typem grantu nie jest „refresh_token”
Duy
1
@mattias W tym scenariuszu nadal chcesz zwrócić nowy token odświeżania. W przeciwnym razie klient po pierwszym odświeżeniu zostanie pozostawiony na lodzie, ponieważ drugi token dostępu wygasa i nie ma możliwości odświeżenia bez ponownego wyświetlenia monitu o poświadczenia.
Eric Eskildsen
9

Nie sądzę, że powinieneś używać tablicy do obsługi tokenów. Nie potrzebujesz też przewodnika jako tokena.

Możesz łatwo użyć context.SerializeTicket ().

Zobacz mój poniższy kod.

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);
    }
}
peeyush rahariya
źródło
2

Odpowiedź Freddy'ego bardzo mi pomogła. Aby uzyskać kompletność, oto jak zaimplementować haszowanie tokena:

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

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

    return Convert.ToBase64String(encoded);
}

W 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);
        }
    }
}
Knelis
źródło
Jak haszowanie pomaga w tym przypadku?
Ajaxe
3
@ Ajaxe: Oryginalne rozwiązanie przechowywało plik Guid. W przypadku mieszania nie przechowujemy tokena zwykłego tekstu, ale jego skrót. Jeśli na przykład przechowujesz tokeny w bazie danych, lepiej jest przechowywać skrót. Jeśli baza danych zostanie naruszona, tokeny są bezużyteczne, o ile są zaszyfrowane.
Knelis
Nie tylko w celu ochrony przed zagrożeniami zewnętrznymi, ale także w celu zapobieżenia kradzieży tokenów pracownikom (którzy mają dostęp do bazy danych).
lonix