web-dev-qa-db-ja.com

IdentityServer4クライアントのアクセストークンを更新する

ハイブリッドフローを使用し、ASP.NET CoreMVCを使用して構築されたIdentityServer4クライアントのアクセストークンを更新する方法を知りたいです。

概念全体を正しく理解している場合、更新トークンを使用できるようにするには、クライアントが最初に「offline_access」スコープを持っている必要があります。これは、短期間のアクセストークンを有効にするためのベストプラクティスであり、更新トークンを取り消して、新しいアクセストークンを防止する機能です。クライアントに発行されます。

アクセストークンと更新トークンを正常に取得できましたが、MVCクライアントでアクセストークンの実際の更新手順をどのように処理する必要がありますか?

OpenId Connect(OIDC)ミドルウェアはこれを自動的に処理できますか?または、基本的にアクセストークンの有効期限が切れているか、すぐに期限切れになるか(今後30秒)を確認して、WEB Apiを呼び出すすべての場所でアクセストークンの有効期限を確認し、更新トークンを使用してトークンエンドポイントを呼び出してアクセストークンを更新する必要があります。 ?

トークンエンドポイントを呼び出すために、コントローラーアクションメソッドで IdentityModel2 ライブラリTokenClient拡張メソッドRequestRefreshTokenAsyncを使用することをお勧めしますか?

OIDCミドルウェアイベントでアクセストークンを要求し、応答ストアを使用して、有効期限を含むクレームを格納するコードを見ました。問題は、私のOIDCが何らかの形ですでにアクセストークンを自動的に要求しているため、最初のアクセストークンを受け取った直後に新しいアクセストークンを要求するのは気分が悪いことです。

コントローラアクションメソッドwithoutアクセストークン更新ロジックの例:

public async Task<IActionResult> GetInvoices()
    {
        var token = await HttpContext.Authentication.GetTokenAsync("access_token");

        var client = new HttpClient();
        client.SetBearerToken(token);

        var response = await client.GetStringAsync("http://localhost:5001/api/getInvoices");
        ViewBag.Json = JArray.Parse(response).ToString();

        return View();
    }
10
Jonas

OIDCミドルウェアはnotでこれを処理します。 HTTP 401応答を検出すると実行され、ユーザーをIdentityServerログインページにリダイレクトします。 MVCアプリケーションへのリダイレクト後、クレームはClaimsIdentityに変換され、これがCookieミドルウェアに渡されます。CookieミドルウェアはそれをセッションCookieに具体化します。

Cookieがまだ有効である限り、他のすべてのリクエストにはOIDCミドルウェアは含まれません。

したがって、これは自分で処理する必要があります。考慮したいもう1つのことは、アクセストークンを更新するときはいつでも、既存のトークンを更新して、紛失しないようにする必要があるということです。これを行わないと、セッションCookieには常に同じトークン(元のトークン)が含まれ、毎回更新されます。

私が見つけた解決策は、それをCookiesミドルウェアにフックすることです。一般的なフローは次のとおりです。

  • リクエストごとに、Cookieミドルウェアイベントを使用してアクセストークンを検査します
  • 有効期限が近づいている場合は、新しいものをリクエストしてください
  • ClaimsIdentityの新しいアクセストークンと更新トークンを置き換えます
  • 新しいトークンが含まれるようにセッションCookieを更新するようにCookieミドルウェアに指示します

このアプローチで私が気に入っているのは、MVCコードでは、トークンの参照が連続して数回失敗し続けない限り、常に有効なアクセストークンを持っていることがほぼ保証されていることです。

私が気に入らないのは、MVC(より具体的にはCookiesミドルウェア)に非常に関連しているため、実際には移植性がないことです。

あなたは見ることができます このGitHubリポジトリ 私はまとめました。確かにIdentityModelを使用します。これはすべてを処理し、IdentityServerに対して行う必要のあるHTTP呼び出しの複雑さのほとんどを隠します。

14

ASP.NET Core2.0のOIDCミドルウェアを使用してアクションフィルタートゲッターに基づいたソリューションを作成しました。

AJAXリクエストもアクションフィルターを経由するため、アクセストークン/更新トークンを更新します。

https://Gist.github.com/devJ0n/43c6888161169e09fec542d2dc12af09

1
Jonas

私は2つの可能な解決策を見つけました。どちらも同じですが、OIDCミドルウェアでは異なる時間に発生します。イベントでは、アクセストークンの有効期限の値を抽出し、それをクレームとして保存します。この値を後で使用して、現在のアクセストークンでWeb APIを呼び出してもよいかどうか、または更新を使用して新しいアクセストークンをリクエストする必要があるかどうかを確認できます。トークン。

誰かがこれらのイベントのどれを使用するのが好ましいかについて何か意見を与えることができれば幸いです。

var oidcOptions = new OpenIdConnectOptions
{
      AuthenticationScheme = appSettings.OpenIdConnect.AuthenticationScheme,
      SignInScheme = appSettings.OpenIdConnect.SignInScheme,

      Authority = appSettings.OpenIdConnect.Authority,
      RequireHttpsMetadata = _hostingEnvironment.IsDevelopment() ? false : true,
      PostLogoutRedirectUri = appSettings.OpenIdConnect.PostLogoutRedirectUri,

      ClientId = appSettings.OpenIdConnect.ClientId,
      ClientSecret = appSettings.OpenIdConnect.ClientSecret,
      ResponseType = appSettings.OpenIdConnect.ResponseType,

      UseTokenLifetime = appSettings.OpenIdConnect.UseTokenLifetime,
      SaveTokens = appSettings.OpenIdConnect.SaveTokens,
      GetClaimsFromUserInfoEndpoint = appSettings.OpenIdConnect.GetClaimsFromUserInfoEndpoint,

      Events = new OpenIdConnectEvents
      {
          OnTicketReceived = TicketReceived,
          OnUserInformationReceived = UserInformationReceived
      },

      TokenValidationParameters = new TokenValidationParameters
      {                    
          NameClaimType = appSettings.OpenIdConnect.NameClaimType,
          RoleClaimType = appSettings.OpenIdConnect.RoleClaimType
      }
  };
  oidcOptions.Scope.Clear();
  foreach (var scope in appSettings.OpenIdConnect.Scopes)
  {
      oidcOptions.Scope.Add(scope);
  }
  app.UseOpenIdConnectAuthentication(oidcOptions);

そして、これが私が選ぶことができるいくつかのイベントの例です:

        public async Task TicketReceived(TicketReceivedContext trc)
    {
        await Task.Run(() =>
        {
            Debug.WriteLine("TicketReceived");

            //Alternatives to get the expires_at value
            //var expiresAt1 = trc.Ticket.Properties.GetTokens().SingleOrDefault(t => t.Name == "expires_at").Value;
            //var expiresAt2 = trc.Ticket.Properties.GetTokenValue("expires_at");
            //var expiresAt3 = trc.Ticket.Properties.Items[".Token.expires_at"];

            //Outputs:
            //expiresAt1 = "2016-12-19T11:58:24.0006542+00:00"
            //expiresAt2 = "2016-12-19T11:58:24.0006542+00:00"
            //expiresAt3 = "2016-12-19T11:58:24.0006542+00:00"

            //Remove OIDC protocol claims ("iss","aud","exp","iat","auth_time","nonce","acr","amr","azp","nbf","c_hash","sid","idp")
            ClaimsPrincipal p = TransformClaims(trc.Ticket.Principal);

            //var identity = p.Identity as ClaimsIdentity;

            // keep track of access token expiration
            //identity.AddClaim(new Claim("expires_at1", expiresAt1.ToString()));
            //identity.AddClaim(new Claim("expires_at2", expiresAt2.ToString()));
            //identity.AddClaim(new Claim("expires_at3", expiresAt3.ToString()));

            //Todo: Check if it's OK to replace principal instead of the ticket, currently I can't make it work when replacing the whole ticket.
            //trc.Ticket = new AuthenticationTicket(p, trc.Ticket.Properties, trc.Ticket.AuthenticationScheme);
            trc.Principal = p;                
        });
    }

UserInformationReceivedイベントもありますが、TicketReceivedイベントの代わりにこれを使用する必要があるかどうかわかりません。

        public async Task UserInformationReceived(UserInformationReceivedContext uirc)
    {
        await Task.Run(() =>
        {
            Debug.WriteLine("UserInformationReceived");

            ////Alternatives to get the expires_at value
            //var expiresAt4 = uirc.Ticket.Properties.GetTokens().SingleOrDefault(t => t.Name == "expires_at").Value;
            //var expiresAt5 = uirc.Ticket.Properties.GetTokenValue("expires_at");
            //var expiresAt6 = uirc.Ticket.Properties.Items[".Token.expires_at"];
            //var expiresIn1 = uirc.ProtocolMessage.ExpiresIn;

            //Outputs:
            //expiresAt4 = "2016-12-19T11:58:24.0006542+00:00"
            //expiresAt5 = "2016-12-19T11:58:24.0006542+00:00"
            //expiresAt6 = "2016-12-19T11:58:24.0006542+00:00"
            //expiresIn = "60" <-- The 60 seconds test interval for the access token lifetime is configured in the IdentityServer client configuration settings

            var identity = uirc.Ticket.Principal.Identity as ClaimsIdentity;

            //Keep track of access token expiration
            //Add a claim with information about when the access token is expired, it's possible that I instead should use expiresAt4, expiresAt5 or expiresAt6 
            //instead of manually calculating the expire time.
            //This claim will later be checked before calling Web API's and if needed a new access token will be requested via the IdentityModel2 library.
            //identity.AddClaim(new Claim("expires_at4", expiresAt4.ToString()));
            //identity.AddClaim(new Claim("expires_at5", expiresAt5.ToString()));
            //identity.AddClaim(new Claim("expires_at6", expiresAt6.ToString()));
            //identity.AddClaim(new Claim("expires_in1", expiresIn1.ToString()));
            identity.AddClaim(new Claim("expires_in", DateTime.Now.AddSeconds(Convert.ToDouble(uirc.ProtocolMessage.ExpiresIn)).ToLocalTime().ToString()));
            //identity.AddClaim(new Claim("expires_in3", DateTime.Now.AddSeconds(Convert.ToDouble(uirc.ProtocolMessage.ExpiresIn)).ToString()));

            //The following is not needed when to OIDC middleware CookieAuthenticationOptions.SaveTokens = true
            //identity.AddClaim(new Claim("access_token", uirc.ProtocolMessage.AccessToken));
            //identity.Claims.Append(new Claim("refresh_token", uirc.ProtocolMessage.RefreshToken));
            //identity.AddClaim(new Claim("id_token", uirc.ProtocolMessage.IdToken));                
        });
    }
0
Jonas