Безопасность OWIN - Как реализовать токены обновления OAuth2

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

В OAuthAuthorizationServerOptions я заметил, что сервер 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

Я много погуглил и не нашел ничего полезного.

Вот как далеко мне удалось зайти. Теперь я дошел до точки "Чего я сейчас делаю".

Я написал 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
    };

Так что теперь, когда кто-то запрашивает bearer_token, я отправляю refresh_token, и это здорово.

Итак, как мне использовать этот refresh_token для получения нового bearer_token, предположительно, мне нужно отправить запрос в конечную точку моего токена с некоторыми определенными установленными заголовками HTTP?

Просто думаю вслух, пока печатаю ... Должен ли я обрабатывать истечение срока действия refresh_token в моем SimpleRefreshTokenProvider? Как клиент получит новый refresh_token?

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


person SimonGates    schedule 17.12.2013    source источник
comment
Существует отличный учебник по реализации токенов обновления с использованием Owin и OAuth: bitoftech.net/2014/07/16/   -  person Philip Bergström    schedule 01.10.2015


Ответы (4)


Только что реализовал мою службу OWIN с носителем (далее называется access_token) и токенами обновления. Насколько я понимаю, вы можете использовать разные потоки. Таким образом, это зависит от потока, который вы хотите использовать, как вы устанавливаете время истечения срока действия 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 минут, чтобы токены не истекли.

B) в этом потоке вы хотите иметь краткосрочное истечение срока действия вашего 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, но и в своей функции Create. Поэтому вам следует подумать об использовании вашей собственной функции (например, CreateTokenInternal) для приведенного выше кода. Здесь вы можете найти реализации различных потоков, включая поток refresh_token (но без установки времени истечения срока действия refresh_token)

Пример реализации greitrovider. > (с установкой срока действия refresh_token)

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

Я надеюсь, что это поможет некоторым другим сэкономить время при попытке реализовать OAuth2.0 со временем истечения срока действия refresh_token, отличным от времени истечения срока действия access_token. Я не смог найти в сети пример реализации (кроме примера thinktecture, ссылка на который приведена выше), и мне потребовалось несколько часов исследования, прежде чем он сработал для меня.

Новая информация: в моем случае у меня есть две разные возможности для получения токенов. Один из них - получить действующий access_token. Там мне нужно отправить вызов POST с телом String в формате application / x-www-form-urlencoded со следующими данными

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

Во-вторых, если access_token больше не действителен, мы можем попробовать refresh_token, отправив вызов POST с телом String в формате application/x-www-form-urlencoded со следующими данными grant_type=refresh_token&client_id=YOURCLIENTID&refresh_token=YOURREFRESHTOKENGUID

person Freddy    schedule 08.03.2014
comment
в одном из ваших комментариев говорится о сохранении только хеша дескриптора, разве этот комментарий не должен применяться к строке выше? Билет содержит исходный идентификатор guid, но мы храним только хэш этого идентификатора в RefreshTokens, поэтому в случае утечки RefreshTokens злоумышленник не сможет использовать эту информацию !? - person esskar; 06.10.2014
comment
похоже на это; - спросил О.А. - person esskar; 06.10.2014
comment
В потоке A каждый раз, когда вы обновляете страницу, вы можете сбросить таймер JavaScript для нового отсчета до 19 минут. Но значит ли это, что вам нужно получать новый токен доступа и обновлять токен при каждом обновлении страницы? Это звучит расточительно из-за токенов, если это правда :) Нельзя ли продлить срок действия существующего токена? - person user20358; 10.04.2015
comment
Как описано в потоке 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. - person Freddy; 12.04.2015
comment
Вы всегда можете установить время истечения срока действия обоих типов токенов - это не зависит от используемого вами потока. - person Freddy; 12.04.2015
comment
См. мой ответ ниже для реализации хеширования токенов. - person Knelis; 06.01.2016
comment
что такое client_id, должен ли клиент это отправлять? если да, то откуда это вообще взялось? - person raklos; 16.06.2016
comment
Не используйте Guids для ваших токенов или их хэшей, это небезопасно. Используйте пространство имен System.Cryptography, чтобы сгенерировать случайный массив байтов и преобразовать его в строку. В противном случае ваши жетоны обновления могут быть угаданы с помощью атак грубой силы. - person Bon; 20.06.2016
comment
Хороший ответ ! Я хочу реализовать Digits для аутентификации, которая предоставит мне идентификатор клиента, а затем как я могу передать его для создания токена доступа и токена обновления с использованием OAuth 2.0? - person Neo; 16.05.2017
comment
@Bon Ты собираешься угадать Гида методом перебора? Ваш ограничитель скорости должен сработать, прежде чем злоумышленник сможет отправить хотя бы несколько запросов. А если нет, то это все равно Guid. - person lonix; 22.05.2019
comment
@linux нет, я не такой. Не чисто то есть. Но я мог бы предположить и существенно ограничить проблемное пространство. stackoverflow.com/questions/3652944/ - person Bon; 23.05.2019
comment
Генератор GUID @lonix не удовлетворяет тесту следующего бита, поэтому это не криптографически безопасный генератор псевдослучайных чисел - person Vladi Pavelka; 26.08.2019

Вам необходимо реализовать 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()
};
person Mauricio    schedule 17.05.2014
comment
Это будет создавать и возвращать новый токен обновления каждый раз, даже если вы можете быть вовлечены в возврат только нового токена доступа, а не нового токена обновления. Например, wen вызывает токен доступа, но с токеном обновления, а не с учетными данными (имя пользователя / пароль). Есть ли способ избежать этого? - person Mattias; 03.03.2016
comment
Можно, но это некрасиво. context.OwinContext.Environment содержит Microsoft.Owin.Form#collection ключ, который дает вам FormCollection, где вы можете найти тип предоставления и соответственно добавить токен. Утечка реализации, она может сломаться в любой момент с будущими обновлениями, и я не уверен, переносится ли она между хостами OWIN. - person hvidgaard; 10.03.2016
comment
вы можете избежать выдачи нового токена обновления каждый раз, прочитав значение grant_type из объекта OwinRequest, например: var form = await context.Request.ReadFormAsync(); var grantType = form.GetValue("grant_type"); затем выдать токен обновления, если тип предоставления не является refresh_token - person Duy; 16.03.2016
comment
@mattias В этом сценарии вы все равно захотите вернуть новый токен обновления. В противном случае клиент останется в беде после обновления в первый раз, потому что срок действия второго токена доступа истекает, и у него нет возможности обновить его без повторного запроса учетных данных. - person Eric Eskildsen; 20.09.2017

Я не думаю, что вам следует использовать массив для хранения токенов. Также вам не нужен гид в качестве токена.

Вы можете легко использовать 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);
    }
}
person peeyush rahariya    schedule 19.04.2016

Ответ Фредди очень помог мне заставить это работать. Для полноты картины вот как можно реализовать хеширование токена:

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);
        }
    }
}
person Knelis    schedule 06.01.2016
comment
Как в этом случае помогает хеширование? - person Ajaxe; 23.02.2016
comment
@Ajaxe: исходное решение хранило Guid. При хешировании мы сохраняем не токен обычного текста, а его хэш. Если вы храните токены, например, в базе данных, лучше хранить хеш. Если база данных скомпрометирована, токены нельзя будет использовать, пока они зашифрованы. - person Knelis; 23.02.2016
comment
Не только для защиты от внешних угроз, но и для предотвращения кражи токенов сотрудниками (имеющими доступ к базе данных). - person lonix; 22.05.2019