webdevqa.jp.net

OWINセキュリティ-OAuth2更新トークンを実装する方法

Visual Studio 2013に付属しているWeb Api 2テンプレートを使用しています。ユーザー認証などを行うための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
 };

これは私の最新のプロジェクトには適していません。 refresh_tokenを使用して更新できる短命のbearer_tokensを配布したい

私は多くのグーグル検索を行ってきましたが、役に立つものは見つかりません。

だから、これは私が何とかやってきたことです。 「WTF do I now」のポイントに到達しました。

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ヘッダーセットを使用してトークンエンドポイントにリクエストを送信する必要があります。

入力中に大声で考えてください... SimpleRefreshTokenProviderでrefresh_tokenの有効期限を処理する必要がありますか?クライアントはどのようにして新しいrefresh_tokenを取得しますか?

私はこれを間違えたくはなく、ある種の標準に従うことを望んでいるので、読み物/ドキュメンテーションで本当にできました。

75
SimonGates

Bearer(以下ではaccess_tokenと呼ばれます)およびRefresh TokensでOWINサービスを実装しました。これに対する私の洞察は、異なるフローを使用できるということです。したがって、access_tokenおよびrefresh_tokenの有効期限を設定する方法を使用するフローによって異なります。

2つのflowsAB次のようにします(フローBにしたいことをお勧めします)。

A)access_tokenおよびrefresh_tokenの有効期限は、デフォルトの1200秒または20分ごとと同じです。このフローでは、クライアントが最初にclient_idとclient_secretをログインデータとともに送信して、access_token、refresh_token、expiration_timeを取得する必要があります。 refresh_tokenを使用すると、20分間(またはOAuthAuthorizationServerOptionsでAccessTokenExpireTimeSpanを設定したもの)新しいaccess_tokenを取得できるようになりました。 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

重要なことの1つは、CreateAsync関数だけでなく、Create関数でもこのコードを使用することです。したがって、上記のコードには独自の関数(CreateTokenInternalなど)を使用することを検討する必要があります。 ここでは、refresh_tokenフローを含むさまざまなフローの実装を見つけることができます (ただし、refresh_tokenの有効期限を設定せずに)

githubのIAuthenticationTokenProviderの1つのサンプル実装 (refresh_tokenの有効期限を設定して)

OAuth仕様およびMicrosoft APIドキュメント以外の資料を提供できないのは残念です。ここにリンクを投稿しますが、私の評判では2つ以上のリンクを投稿できません。

Access_tokenの有効期限とは異なるrefresh_tokenの有効期限でOAuth2.0を実装しようとするときに、他の人が時間を節約するのに役立つことを願っています。ウェブ上に実装例を見つけることができませんでした(上記のリンクのthinktectureを除く)。それがうまくいくまで、調査に数時間かかりました。

新しい情報:私の場合、トークンを受け取る方法は2つあります。 1つは、有効なaccess_tokenを受け取ることです。そこで、次のデータを使用して、application/x-www-form-urlencoded形式のStringボディでPOST呼び出しを送信する必要があります

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

2つ目は、access_tokenが無効になった場合、次のデータapplication/x-www-form-urlencodedを含むgrant_type=refresh_token&client_id=YOURCLIENTID&refresh_token=YOURREFRESHTOKENGUID形式のStringボディでPOST呼び出しを送信することで、refresh_tokenを試すことができることです。

74
Freddy

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()
};
41
Mauricio

トークンを維持するために配列を使用する必要があるとは思わない。トークンとして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);
    }
}
8

Freddy's answer は、これを機能させるのに大いに役立ちました。完全を期すために、トークンのハッシュを実装する方法を次に示します。

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