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


80

Visual Studio2013に付属のWebApi 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
 };

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

私はたくさんのグーグルをしました、そして、何も役に立つものを見つけることができません。

だから、これは私が何とか到達したところです。「WTFdoInow」の段階になりました。

クラスのプロパティに従って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/...
フィリップ・バーグストロム

回答:


76

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

次の2つのフロー ABについて説明します(フローBが必要なことをお勧めします)。

A) access_tokenとrefresh_tokenの有効期限は、デフォルトの1200秒または20分と同じです。このフローでは、最初にクライアントがclient_idとclient_secretをログインデータとともに送信して、access_token、refresh_token、expiration_timeを取得する必要があります。refresh_tokenを使用すると、新しいaccess_tokenを20分間(または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に長期の有効期限を設定します。テストの目的AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10)で、access_tokenを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仕様とMicrosoftAPIドキュメント以外の資料を手伝うことができず申し訳ありません。ここにリンクを投稿しますが、私の評判では2つ以上のリンクを投稿することはできません。

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

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

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

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


1
あなたのコメントの1つは、「ハンドルのハッシュを格納することを検討してください」と言っていますが、そのコメントは上の行に適用されるべきではありませんか?チケットには元のGUIDが含まれていますが、GUIDのハッシュのみがに格納されているRefreshTokensため、RefreshTokensリークされた場合、攻撃者はその情報を使用できません!?
esskar 2014年

それのようです。OAを尋ねた:github.com/thinktecture/Thinktecture.IdentityModel/commit/...
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()
};

これにより、毎回新しい更新トークンが作成されて返されます。新しいアクセストークンを返すことに専念しているだけで、新しい更新トークンを返していない場合でも同様です。たとえば、アクセストークンを要求しているが、資格情報(ユーザー名/パスワード)ではなく更新トークンを使用している場合です。これを回避する方法はありますか?
マティアス

できますが、きれいではありません。には、付与タイプを見つけてそれに応じてトークンを追加できる場所を提供context.OwinContext.EnvironmentするMicrosoft.Owin.Form#collectionキーが含まれていますFormCollection。実装がリークされており、将来のアップデートでいつでも壊れてしまう可能性があり、OWINホスト間で移植可能かどうかはわかりません。
hvidgaard 2016年

3
次のように、OwinRequestオブジェクトから「grant_type」値を読み取ることで、毎回新しい更新トークンを発行することを回避できます。 var form = await context.Request.ReadFormAsync(); var grantType = form.GetValue("grant_type"); 次に、許可タイプが「refresh_token」でない場合は更新トークンを発行します
Duy

1
@mattiasそのシナリオでも、新しい更新トークンを返したいと思うでしょう。そうしないと、2番目のアクセストークンの有効期限が切れ、資格情報の再入力を求めずに更新する方法がないため、クライアントは最初に更新した後、混乱したままになります。
Eric Eskildsen 2017

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

フレディの答えは、これを機能させるのに大いに役立ちました。完全を期すために、トークンのハッシュを実装する方法は次のとおりです。

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

この場合、ハッシュはどのように役立ちますか?
アヤックス2016

3
@Ajaxe:元のソリューションはGUIDを格納していました。ハッシュでは、プレーンテキストトークンではなくハッシュを保持します。たとえば、トークンをデータベースに保存する場合は、ハッシュを保存することをお勧めします。データベースが侵害された場合、トークンは暗号化されている限り使用できません。
Knelis 2016

外部の脅威から保護するだけでなく、(データベースにアクセスできる)従業員がトークンを盗むのを防ぐためにも。
lonix
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.