Безопасность 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

Я сделал много поисковых запросов и не нашел ничего полезного.

Так вот, как далеко я успел добраться. Теперь я достиг точки "WTF do я 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
    };

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

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

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

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

Ответ 1

Просто реализована моя служба 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)

Ниже приведена одна примерная реализация IAuthenticationTokenProvider на github (с установкой времени истечения срока действия 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

Ответ 2

Вам нужно реализовать 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()
};

Ответ 3

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

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

Ответ 4

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

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