OWIN 보안-OAuth2 새로 고침 토큰 구현 방법


80

Visual Studio 2013과 함께 제공되는 Web Api 2 템플릿을 사용하고 있으며 사용자 인증 등을 수행하는 일부 OWIN 미들웨어가 있습니다.

에서 OAuthAuthorizationServerOptions발견 i를 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_tokenrefresh_token

인터넷 검색을 많이했는데 도움이되는 것을 찾을 수 없습니다.

그래서 이것은 내가 얼마나 멀리 얻을 수 있었는지입니다. 나는 이제 "WTF do I now"의 지점에 도달했습니다.

클래스 의 속성에 따라 RefreshTokenProvider구현 하는를 작성했습니다 .IAuthenticationTokenProviderRefreshTokenProviderOAuthAuthorizationServerOptions

    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있습니까?

나는 이것을 잘못 이해하고 싶지 않고 일종의 표준을 따르고 싶기 때문에 독서 자료 / 문서로 정말 할 수 있습니다.


7
Owin 및 OAuth를 사용하여 새로 고침 토큰을 구현하는 방법에 대한 훌륭한 자습서가 있습니다 : bitoftech.net/2014/07/16/…
Philip Bergström

답변:


76

Bearer (다음에서 access_token이라고 함) 및 새로 고침 토큰으로 OWIN 서비스를 구현했습니다. 이에 대한 나의 통찰력은 다른 흐름을 사용할 수 있다는 것입니다. 따라서 access_token 및 refresh_token 만료 시간을 설정하는 방법을 사용하려는 흐름에 따라 다릅니다.

다음에서 두 개의 흐름 AB 를 설명하겠습니다 (원하는 것이 흐름 B라고 제안합니다).

A) access_token 및 refresh_token의 만료 시간은 기본 1200 초 또는 20 분당과 동일합니다. 이 흐름에서는 클라이언트가 먼저 access_token, refresh_token 및 expire_time을 가져 오기 위해 로그인 데이터와 함께 client_id 및 client_secret을 보내야합니다. refresh_token을 사용하면 이제 20 분 동안 새 access_token을 얻을 수 있습니다 (또는 OAuthAuthorizationServerOptions에서 AccessTokenExpireTimeSpan을 설정 한대로). 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의 만료 시간을 설정하는 흥미로운 부분에 대해 설명합니다. SimpleRefreshTokenProvider 클래스의 createAsync 함수에서 다음과 같이 수행합니다.

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

이제 클라이언트 access_token는 만료 될 때 토큰 엔드 포인트에 refresh_token이 포함 된 POST 호출을 보낼 수 있습니다. 호출의 본문 부분은 다음과 같습니다.grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xx

한 가지 중요한 점은 CreateAsync 함수뿐만 아니라 Create 함수에서도이 코드를 사용할 수 있다는 것입니다. 따라서 위 코드에 대해 자체 함수 (예 : CreateTokenInternal)를 사용하는 것을 고려해야합니다. 여기에서 refresh_token 흐름을 포함한 다양한 흐름의 구현을 찾을 수 있습니다 (그러나 refresh_token 의 만료 시간을 설정하지 않음).

다음은 github의 IAuthenticationTokenProvider 구현 샘플입니다 (refresh_token의 만료 시간 설정 포함).

OAuth 사양 및 Microsoft API 문서 이외의 자료로 도움을 드릴 수 없어서 죄송합니다. 나는 여기에 링크를 게시 할 것이지만 내 평판은 내가 2 개 이상의 링크를 게시하도록 허용하지 않는다 ....

나는 이것이 access_token 만료 시간과 다른 refresh_token 만료 시간으로 OAuth2.0을 구현하려고 할 때 다른 사람들이 시간을 할애하는 데 도움이되기를 바랍니다. 웹에서 예제 구현을 찾을 수 없었고 (위에 링크 된 thinktecture 중 하나를 제외하고) 저에게 효과가있을 때까지 조사하는 데 몇 시간이 걸렸습니다.

새로운 정보 : 제 경우에는 토큰을받을 수있는 두 가지 다른 가능성이 있습니다. 하나는 유효한 access_token을받는 것입니다. 거기에 다음 데이터로 application / x-www-form-urlencoded 형식의 문자열 본문이있는 POST 호출을 보내야합니다.

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

두 번째는 access_token이 더 이상 유효하지 않은 경우 application/x-www-form-urlencoded다음 데이터가 포함 된 형식의 문자열 본문과 함께 POST 호출을 전송하여 refresh_token을 시도 할 수 있습니다.grant_type=refresh_token&client_id=YOURCLIENTID&refresh_token=YOURREFRESHTOKENGUID


1
귀하의 의견 중 하나는 "손잡이의 해시를 저장하는 것을 고려하십시오"라고 말합니다. 그 주석이 위의 줄에 적용되지 않습니까? 티켓은 원래 GUID를 보유하고 있지만 우리는 GUID의 해시를에 저장하기 RefreshTokens때문에 RefreshTokens유출되면 공격자가 해당 정보를 사용할 수 없습니다!?
esskar


1
흐름 B에 설명 된대로 AccessTokenExpireTimeSpan = TimeSpan.FromMinutes (60)를 1 시간 동안 사용하거나 FromWHATEVER를 사용하여 access_token이 만료되기를 원하는 시간을 사용하여 access_token의 만료 시간을 설정할 수 있습니다. 그러나 흐름에서 refresh_token을 사용하는 경우 refresh_token의 만료 시간이 access_token의 만료 시간보다 높아야합니다. 예를 들어 access_token은 24 시간, refresh_token은 2 개월입니다. OAuth 구성에서 access_token의 만료 시간을 설정할 수 있습니다.
프레디

12
토큰이나 해시에 Guid를 사용하지 마십시오. 안전하지 않습니다. System.Cryptography 네임 스페이스를 사용하여 임의의 바이트 배열을 생성하고이를 문자열로 변환합니다. 그렇지 않으면 무차별 대입 공격으로 새로 고침 토큰을 추측 할 수 있습니다.
Bon

1
@Bon 당신은 Guid를 무차별 대입 할거야? 공격자가 소수의 요청을 게시하기 전에 속도 제한 기가 작동해야합니다. 그렇지 않다면 여전히 Guid입니다.
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은 액세스 토큰을 요청하지만 자격 증명 (사용자 이름 / 암호)이 아닌 새로 고침 토큰을 사용합니다. 어쨌든 이것을 피할 수 있습니까?
Mattias

할 수 있지만 예쁘지 않습니다. 는 context.OwinContext.Environment포함 Microsoft.Owin.Form#collection당신에게 제공 키 FormCollection당신이 부여 유형을 발견하고 그에 따라 토큰을 추가 할 수 있습니다. 구현이 유출되고 있으며 향후 업데이트로 인해 언제든지 중단 될 수 있으며 OWIN 호스트간에 이식 가능한지 확실하지 않습니다.
hvidgaard

3
var form = await context.Request.ReadFormAsync(); var grantType = form.GetValue("grant_type"); 다음 과 같이 OwinRequest 개체에서 "grant_type"값을 읽어 매번 새 새로 고침 토큰을 발행하는 것을 피할 수 있습니다. 그런 다음 허가 유형이 "refresh_token"이 아닌 경우 새로 고침 토큰을 발행하십시오.
Duy

1
@mattias 해당 시나리오에서 새 새로 고침 토큰을 반환하고 싶을 것입니다. 그렇지 않으면 두 번째 액세스 토큰이 만료되고 자격 증명을 다시 요청하지 않고는 새로 고칠 방법이 없기 때문에 클라이언트는 처음으로 새로 고친 후 불안정한 상태로 남아 있습니다.
Eric Eskildsen

9

토큰을 유지하기 위해 배열을 사용해야한다고 생각하지 않습니다. 토큰으로 GUID가 필요하지 않습니다.

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

Freddy의 답변 은이 작업을 수행하는 데 많은 도움이되었습니다. 완전성을 위해 토큰 해싱을 구현하는 방법은 다음과 같습니다.

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 : 원래 솔루션은 Guid를 저장했습니다. 해싱을 사용하면 일반 텍스트 토큰이 아니라 해시를 유지합니다. 예를 들어 토큰을 데이터베이스에 저장하는 경우 해시를 저장하는 것이 좋습니다. 데이터베이스가 손상된 경우 토큰은 암호화 된 한 사용할 수 없습니다.
Knelis

외부 위협으로부터 보호 할뿐만 아니라 직원 (데이터베이스에 액세스 할 수있는)이 토큰을 도용하지 못하도록 방지합니다.
lonix
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.