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