OIDCで再認証を強制する

prompt=loginの仕組みは、ユーザーのエージェント(ブラウザー)から渡されるパラメーターを取り除くだけで動作不能にできます。これは証明書利用者(RP)が以下のようなものを表示したい場合に、UXヒントをOpenIDプロバイダー(OP)に提供するためだけに使用されるべきです:

「Joshさん、こんにちは。お客様ではありませんか?こちらをクリックしてください。」

ただし、これは新たに行われた認証の確認に利用するべきではありません。それを避けるために、再認証を理由にmax_ageが要求された場合には、クライアントはauth_timeクレームを使用して、再認証が行われたことを確認する必要があります。このクレームは、認証要求でprompt-loginまたはmax_age=0が渡されると、自動的にIDトークンに含められます。

max_ageパラメーターをAuthorization APIの/authorizeエンドポイントに渡す必要があります。Auth0.jsまたはLockを使用している場合には、ライブラリーの適切なオプションでパラメーターを設定することができます。

再認証の実装方法は特定のユースケースに依存します。機密性の高い操作に対して、再認証とステップアップ(多要素認証)は区別しなければなりません。それらは両方とも有効なセキュリティ対策です。前者ではエンドユーザーにパスワードの再入力を要求するの対し、後者ではあらかじめ構成済みの多要素認証も併せて要求します。

prompt=loginパラメーターの制限事項

OIDCの仕様では、再認証UI(通常はログインプロンプト)をトリガーするのにprompt=loginパラメーターが使用できると定義されています。

ただし、このパラメーターで再認証を確実にするには問題があります。RPには、再認証の操作が行われたことを確認する方法がありません。なぜそうなのかを理解するために、トラフィックを調べてみましょう。RPからの認証要求のフローは以下のようになります。

https://mydomain.auth0.com/authorize?
client_id=abcd1234
&redirect_uri= https://mydomain.com/callback

&scope=openid profile
&response_type=id_token
&prompt=login

Was this helpful?

/

ASで認証が成功すると、RPは以下のIDトークンを受け取ります。

{
  "nickname": "user",
  "name": "user@mydomain.auth0.com",
  "updated_at": "2019-04-01T14:43:03.445Z",
  "iss": "https://jcain0.auth0.com/",
  "sub": "auth0|l33t",
  "aud": "abcd1234",
  "iat": 1554129793,
  "exp": 1554165793
}

Was this helpful?

/

ASが返す信頼されたIDドキュメントには、最終ログインの時期を確認するクレームがありません。これは、当初の認可要求がエンドユーザーのブラウザーから302のリダイレクトとして送信された場合に問題となります。RPが要求した再認証の手順を悪意のある行為者が迂回したい場合、prompt=loginパラメーターを削除するだけで済むため、IDトークンに含まれるフィールドではRPがその違いを判別できません。

下の図は、prompt=loginパラメーターを用いた暗黙フローを簡単に説明したものです。

Force Re-Authentication OIDC Implicit Flow

エンドユーザーがprompt=loginパラメーターを削除するたけで、再認証がスキップできることに注意してください。

Simplified Implicit Flow Remove prompt=login

上の最初のフローで返されたトークンは、下のフローで返されたトークンと同じです。再認証が行われたことを確認するのに、RPには仕様で定義された方法がないため、prompt=loginが実際に再認証を生じさせたと信頼することはできません。

max_age認証要求パラメーター

prompt=loginと違って、max_age認証要求パラメーターには、指定の間隔内に再認証が行われたことをRPが確実に確認できる方法があります。OIDCの仕様には以下が定義されています。

定義にある最後の文章が最も重要な部分です。RPがmax_ageを要求した場合、RPにauth_timeクレームを渡す必要があります。つまり、max_ageの用途には以下の2つがあります。

  • セッションのフレッシュネスを指定する:アプリがユーザーに1日1回の再認証を要求する場合には、max_ageに値を指定すると、SSOセッションの有効期間を長くすることができます。これは秒単位で定義します。

  • 即座の再認証を強制する:アプリがユーザーにアクセス前の再認証を要求する場合には、max_ageパラメーターの値を0に設定すると、ASが新たにログインを強制します。

下の図はこの要件を説明したものです。

OIDC re-authentication max_age flow

再認証が行われたかを確認するために、適切な情報量のあるトークンをRPが受け取っていることに注意してください。RPはIDトークンのauth_timeクレームを確認し、max_ageパラメーターでの要求が満たされたかを判断できます。こうすることで、max_age=0パラメーターには、prompt=loginパラメーターの動作を妨げるのと同じようなクライアントの改ざんが通用しなくなります。

auth_timeクレームを使用する

OIDCの仕様により、再認証の実行を確実に確認する方法として、max_ageパラメーターは使用できても、prompt=loginが使用できないことを説明しました。これは、再認証を強制したい場合に安全なオプションを提供しません。

  • imprompt=loginpromptのみを含み、ASが実際に再認証したことは確認しません。

  • prompt=login & max_age=999999auth_timeクレームが使用されるように、任意のmax_ageを含めます。再認証が行われたことは確認できますが、パラメーターが乱雑になります。

  • max_age=0max_ageパラメーターのみを使用して、ログインプロンプトを強制的に表示します。最近更新された仕様ではこのパラメーターをさらに明確化し、実質的にはprompt=loginと同等だとしています。これは、UXパラメーターであるべきものをセッション維持パラメーターと混合させるため、適切ではありません。

そうではなく、Auth0はprompt=login要求パラメーターへの応答で、IDトークンにauth_timeクレームを含めて送信するようにしています。つまり、prompt=loginを使用すると同時に、再認証が行われたことを確認することができます。

auth_time確認の例

以下の例では、passport-auth0-openidconnectモジュールを使用して、再認証の確認方法を説明します。最初の(最も簡単な)例としては、max_age=0オプションをAuth0OidcStrategyに追加して確認する方法です。

var strategy = new Auth0OidcStrategy(
  {
    domain: process.env.AUTH0_DOMAIN,
    clientID: process.env.AUTH0_CLIENT_ID,
    clientSecret: process.env.AUTH0_CLIENT_SECRET,
    callbackURL: process.env.AUTH0_CALLBACK_URL || 'http://localhost:5000/callback',
    max_age: 0
  },
  function(req, issuer, audience, profile, accessToken, refreshToken, params, cb) {
    // No extra validation required!
    return cb(null, profile);
  });

Was this helpful?

/

ストラテジーがすでにmax_ageパラメーターの確認を処理しているため、確認の手順がこれ以上必要にならないことに注意してください。

// https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation - check 8.
if (meta.params.max_age && (!jwtClaims.auth_time || ((meta.timestamp - meta.params.max_age) > jwtClaims.auth_time))) {
  return self.error(new Error('auth_time in id_token not included or too old'));
}

Was this helpful?

/

同じコンテキストでprompt=loginを使うこともできますが、規格としてauth_timeはIDトークン応答の付随的な発生を要求しないため、手動で確認を処理しなければなりません。このストラテジーコンストラクターは以下のようになります。

var strategy = new Auth0OidcStrategy(
  {
    domain: process.env.AUTH0_DOMAIN,
    clientID: process.env.AUTH0_CLIENT_ID,
    clientSecret: process.env.AUTH0_CLIENT_SECRET,
    callbackURL: process.env.AUTH0_CALLBACK_URL || 'http://localhost:5000/callback',
    prompt: 'login'
  },
  function(req, issuer, audience, profile, accessToken, refreshToken, params, cb) {
    const tenSecondsAgo = (Date.now() / 1000) - 10;
    if (isNaN(profile.auth_time) || profile.auth_time < tenSecondsAgo) {
      return cb('prompt=login requested, but auth_time is greater than 10 seconds old', null);
    }

    return cb(null, profile);
  });

Was this helpful?

/

max_age=0とは違って、クライアントは手動でauth_timeパラメーターの確認を処理する必要があります。詳細については、「auth_timeクレームを使用する」をお読みください。

既知の問題

Auth0が保証できるのは、アップストリームのIDプロバイダーと交換が行われることだけです。これは、ユーザーが実際にサードパーティーのIDプロバイダーにサインインしたり、ユーザーに既存のセッションがあるため再サインインの必要がなかったりすることによって行われます。いずれにしても、Auth0がアップストリームのIDプロバイダーと交換し、結果的にauth_timeが更新されます。

アップストリームのIDプロバイダーで再認証を強制することは、すべてのプロバイダーがこれに対応しているわけではないため、Auth0の対応外になります。

下の図は、フェデレーション接続で再認証するユーザーのフローの例を説明したものです。

Federated connections do not force re-authentication diagram

この方法ではデータベース接続の使用を想定しています。外部のIDプロバイダーは再認証の強制に対応していても、していなくても構いません。prompt=loginまたはprompt=consentの使用は、一般的に外部の(ソーシャル)IDプロバイダーにユーザーの再認証を指示するもので、Auth0はそれを強制できません。

もっと詳しく