Безпека OWIN - Як застосувати оновлені маркери OAuth2


80

Я використовую шаблон Web Api 2, який постачається разом з Visual Studio 2013, має проміжне програмне забезпечення OWIN для автентифікації користувачів тощо.

У OAuthAuthorizationServerOptionsI зауважив , що OAuth2 сервер налаштований , щоб роздати лексем, закінчується через 14 днів

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

Це не підходить для мого останнього проекту. Я хотів би роздати короткочасні файли bearer_tokens, які можна оновити за допомогоюrefresh_token

Я багато разів гуглив і не можу знайти нічого корисного.

Тож ось як далеко мені вдалося пройти. Зараз я дійшов до точки "WTF do I now".

Я написав a, RefreshTokenProviderякий реалізує IAuthenticationTokenProviderвідповідно до RefreshTokenProviderвластивості OAuthAuthorizationServerOptionsкласу:

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

Отже, коли хтось просить a, bearer_tokenя зараз надсилаю a refresh_token, що чудово.

Тож як тепер я використовую цей refresh_token для отримання нового bearer_token, імовірно, мені потрібно надіслати запит до своєї кінцевої точки маркера з набором певних заголовків HTTP?

Просто думаючи вголос під час набору тексту ... Чи повинен я обробляти термін дії refresh_token у своєму SimpleRefreshTokenProvider? Як клієнт отримає новий refresh_token?

Я дійсно міг би попрацювати з деякими матеріалами для читання / документацією, тому що я не хочу це неправильно сприймати і хотів би дотримуватися якогось стандарту.


7
Існує чудовий підручник з реалізації токенів оновлення за допомогою Owin та OAuth: bitoftech.net/2014/07/16/…
Філіп Бергстрем

Відповіді:


76

Щойно реалізував мою службу OWIN за допомогою Bearer (далі - access_token) та Refresh Tokens. Моє розуміння цього полягає в тому, що ви можете використовувати різні потоки. Тож це залежить від потоку, який ви хочете використовувати, як ви встановлюєте час закінчення терміну дії access_token та refresh_token.

Я опишу два потоки A і B в подальшому (я пропоную те, що ви хочете мати, це потік B):

A) час закінчення доступу access_token та refresh_token такий же, як і за замовчуванням 1200 секунд або 20 хвилин. Цьому потоку спочатку потрібно, щоб ваш клієнт надіслав client_id і client_secret з даними для входу, щоб отримати access_token, refresh_token і expiration_time. Завдяки refresh_token тепер можна отримати новий access_token протягом 20 хвилин (або що б ви не встановили AccessTokenExpireTimeSpan в OAuthAuthorizationServerOptions). З тієї причини, що час закінчення доступу access_token та refresh_token однаковий, ваш клієнт несе відповідальність за отримання нового access_token до закінчення терміну дії! Наприклад, ваш клієнт може надіслати оновлений виклик POST до кінцевої точки вашого маркера разом із тілом (зауваження: ви повинні використовувати https у виробництві)

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

отримати новий маркер приблизно, наприклад, через 19 хвилин, щоб запобігти закінченню терміну дії токенів.

Б) у цьому потоці ви хочете мати короткочасний термін дії для вашого access_token і довгостроковий термін дії для вашого refresh_token. Припустимо, для цілей тесту ви встановили, що access_token закінчується через 10 секунд ( AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10)), а refresh_token - 5 хвилин. Тепер мова заходить про цікаву частину, яка встановлює час закінчення періоду refresh_token: Ви робите це у своїй функції createAsync у класі SimpleRefreshTokenProvider так:

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

Тепер ваш клієнт може надіслати POST-дзвінок з refresh_token на кінцеву точку вашого маркера, коли access_tokenтермін дії закінчився. Основна частина дзвінка може виглядати так:grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xx

Одна важлива річ полягає в тому, що ви можете захотіти використовувати цей код не лише у своїй функції CreateAsync, але й у своїй функції Створення. Тому вам слід розглянути можливість використання власної функції (наприклад, з назвою CreateTokenInternal) для вищевказаного коду. Тут ви можете знайти реалізації різних потоків, включаючи потік refresh_token (але без встановлення часу закінчення періоду refresh_token)

Ось одна зразкова реалізація IAuthenticationTokenProvider на github (із встановленням часу закінчення періоду refresh_token)

Мені шкода, що я не можу допомогти з іншими матеріалами, окрім специфікацій OAuth та документації Microsoft API. Я б розмістив тут посилання, але моя репутація не дозволяє розміщувати більше 2 посилань ....

Я сподіваюся, це може допомогти деяким іншим привільнити час при спробі впровадити OAuth2.0 із часом закінчення refresh_token, різним до часу закінчення access_token. Я не зміг знайти приклад реалізації в Інтернеті (за винятком тієї, що міститься вище, зв’язаної), і мені зайняли кілька годин розслідування, поки це не спрацювало для мене.

Нова інформація: У моєму випадку у мене є дві різні можливості отримувати жетони. Один з них - отримати дійсний маркер доступу. Там я повинен відправити виклик POST із текстом рядка у форматі application / x-www-form-urlencoded із такими даними

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

По-друге, якщо access_token вже не дійсний, ми можемо спробувати refresh_token, відправивши виклик POST із тілом рядка у форматі application/x-www-form-urlencodedз наступними данимиgrant_type=refresh_token&client_id=YOURCLIENTID&refresh_token=YOURREFRESHTOKENGUID


1
один із ваших коментарів говорить "розгляньте можливість зберігання оні хешу дескриптора", чи не повинен цей коментар стосуватися рядка вище? У квитку міститься оригінальний гід, але ми зберігаємо лише хеш гіда RefreshTokens, тому, якщо RefreshTokensвитік, зловмисник не може використовувати цю інформацію !?
esskar

здається так; запитав OA: github.com/thinktecture/Thinktecture.IdentityModel/commit/…
esskar

1
Як описано в потоці B, ви можете встановити час закінчення для access_token, використовуючи AccessTokenExpireTimeSpan = TimeSpan.FromMinutes (60) на одну годину або FromWHATEVER на час, коли ви хочете, щоб закінчився access_token. Але майте на увазі, що якщо ви використовуєте refresh_token у своєму потоці, час закінчення вашого refresh_token повинен бути вищим, ніж час вашого access_token. Так, наприклад, ми використовуємо 24 години для access_token і 2 місяці для refresh_token. Ви можете встановити термін дії access_token у конфігурації OAuth.
Фредді

12
Не використовуйте посібники для своїх токенів і їх хеші, це не безпечно. Використовуйте простір імен System.Cryptography, щоб сформувати випадковий байтовий масив і перетворити його на рядок. В іншому випадку ваші токени оновлення можна вгадати за допомогою атак грубої сили.
Бон

1
@Bon Ти збираєшся грубою силою вгадати Порадника? Ваш обмежувач тарифу повинен перешкодити, перш ніж зловмисник зможе опублікувати навіть кілька запитів. А якщо ні, то це все-таки Посібник.
lonix

46

Вам потрібно застосувати RefreshTokenProvider . Спочатку створіть клас для RefreshTokenProvider, тобто.

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

Потім додайте екземпляр до OAuthOptions .

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

Це буде створювати та повертати новий маркер оновлення кожного разу, навіть якщо вам може бути спонукано повернути новий маркер доступу, а не новий маркер оновлення. Наприклад, wen закликає маркер доступу, але з маркером оновлення, а не з обліковими даними (ім'я користувача / пароль). Чи є спосіб уникнути цього?
Маттіас

Можна, але це не красиво. context.OwinContext.EnvironmentМістить Microsoft.Owin.Form#collectionключ , який дає вам , FormCollectionде ви можете знайти тип гранту і додати маркер відповідно. Це витікає з реалізації, вона може зламатися в будь-який момент із майбутніми оновленнями, і я не впевнений, чи є вона портативною між хостами OWIN.
hvidgaard

3
ви можете уникнути видачі нового поновлення маркера кожен раз, читаючи «grant_type» значення з об'єкта OwinRequest, наприклад , так: var form = await context.Request.ReadFormAsync(); var grantType = form.GetValue("grant_type"); потім видавати поновлення маркера , якщо тип гранту не "refresh_token»
Дуй

1
@mattias Ви все одно хочете повернути новий маркер оновлення в цьому сценарії. В іншому випадку клієнт залишається в недоліку після першого оновлення, оскільки термін дії другого маркера доступу закінчується, і вони не мають можливості оновити без повторного запиту на введення облікових даних.
Ерік Ескільдсен,

9

Я не думаю, що вам слід використовувати масив для підтримки токенів. Вам також не потрібен гід в якості символу.

Ви можете легко використовувати context.SerializeTicket ().

Дивіться мій код нижче.

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

Відповідь Фредді мені дуже допомогла отримати цю роботу. Для повноти ось як можна реалізувати хешування маркера:

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

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

    return Convert.ToBase64String(encoded);
}

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

Як хешування допомагає в цьому випадку?
Ajaxe

3
@Ajaxe: оригінальне рішення зберігало посібник. За допомогою хешування ми не зберігаємо маркер простого тексту, а його хеш. Якщо ви зберігаєте маркери, наприклад, у базі даних, краще зберігати хеш. Якщо база даних скомпрометована, токени непридатні, доки вони зашифровані.
Кнеліс

Не тільки для захисту від зовнішніх загроз, але й для того, щоб не дати співробітникам (які мають доступ до бази даних) красти токени.
lonix,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.