Native SSO

コンセプト

OpenID Connect Native SSO for Mobile Apps 1.0 (以下Native SSO) は、同一ベンダーの管理下にある複数のモバイルアプリケーション間でシングルサインオン (SSO) を実現する仕組みを標準仕様として定義します。この仕様が実装されていると、ユーザはモバイルアプリケーション毎にユーザ認証を行う必要はなく、Native SSOで連携したアプリケーション群に対して一回のユーザ認証で済みます。


仕様概要

詳細に踏み込む前に、仕様の概要を紹介します。

まず、一つ目のアプリケーションが認可コードフローを用いて下記のトークン群を取得します。注目すべき点は、IDトークンがNative SSO仕様に準拠していること、および、デバイスシークレットという新しいタイプのトークンが含まれていることです。

  1. アクセストークン
  2. リフレッシュトークン (任意)
  3. ID トークン (Native SSO仕様準拠)
  4. デバイスシークレット

一つ目のアプリケーションは、二つ目のアプリケーションがアクセスできる共有ストレージにIDトークンとデバイスシークレットを保存します。

次に、二つ目のアプリケーションが共有ストレージからIDトークンとデバイスシークレットを取り出し、それらをパラメータとしてトークン交換リクエストを送信します。

応答として、二つ目のアプリケーション用のトークン群を含むトークン交換レスポンスが返却されます。

まとめると、Native SSO仕様の概要図は次のようになります。


デバイスシークレット

デバイスシークレットはNative SSO仕様で次のように説明されています。

The device secret contains relevant data to the device and the current users authenticated with the device. The device secret is completely opaque to the client and as such the AS MUST adequately protect the value such as using a JWE if the AS is not maintaining state on the backend.

デバイスシークレットには、デバイスおよび現在そのデバイスで認証されているユーザに関連するデータが含まれます。 デバイスシークレットはクライアントにとって完全に不透明であるため、認可サーバ (AS) はその値を適切に保護しなければなりません。 例えば、バックエンドで状態を保持しない場合にはJWEを使用するなどの手段が求められます。

デバイスシークレットを発行する際、OpenIDプロバイダは何らかの方法でデバイスの情報を取得し、デバイスシークレットと紐付けます。 そして、トークン交換リクエストを受け取った際、リクエストに含まれるデバイスシークレットに紐付くデバイスが、リクエスト送信元のデバイスと一致するかを確認します。


仕様詳細

アプリ1の認可リクエスト

一つ目のアプリケーションは、認可コードフローに基づく認可リクエストをWebブラウザを介してOpenIDプロバイダに送信します。Native SSO仕様固有の要求事項は、scopeパラメータにopenidスコープとdevice_ssoスコープを含めることです。device_ssoスコープは同仕様が定義するスコープです。

Native SSOに準拠する認可リクエストに最低限必要なリクエストパラメータは次の通りです。

パラメータ 説明
client_id クライアント識別子です。例えばapp_1
response_type 要求するトークン群を空白文字区切りで列挙したものです。認可コードを要求する場合はcodeを含めます。
scope 要求するスコープ群を空白文字区切りで列挙したものです。Native SSO仕様に準拠するためにはopeniddevice_ssoを含めます。
redirect_uri リダイレクトURIです。OpenID Connect仕様により、openidスコープを要求する際はredirect_uriパラメータが必須となります。

下記は認可リクエストの例です。

https://trial.authlete.net/api/authorization?client_id=app_1&response_type=code&scope=openid+device_sso&redirect_uri=https://nextdev-api.authlete.net/api/mock/redirection

アプリ1のトークンリクエスト

上記の認可リクエストの結果得られた認可コードを用いてトークンリクエストを組み立てます。必要となるリクエストパラメータは次の通りです。

パラメータ 説明
grant_type グラントタイプです。どのフローを用いるかに関わらず必須のパラメータです。認可コードフローの場合は値としてauthorization_codeを指定します。
code 認可コードフローの場合に必須のパラメータです。認可リクエストの結果得られた認可コードを値として指定します。
redirect_uri リダイレクトURIです。先行する認可リクエストにredirect_uriパラメータを含めていた場合、トークンリクエストでもredirect_uriパラメータが必須となります。その値は認可リクエストで指定したものと同一でなければなりません。

また、上記に加え、アプリケーションのクライアントタイプ (RFC 6749 Section 2.1) がクレデンシャルかパブリックか、また、クライアントタイプがコンフィデンシャルの場合にどのクライアント認証方式を用いるかにより、追加のパラメータが必要になります。

例えば、クライアントタイプがパブリックの場合はclient_idリクエストパラメータが必須となります。一方、クライアントタイプがコンフィデンシャルでクライアント認証方式としてprivate_key_jwtを用いる場合はclient_assertionおよびclient_assertion_typeリクエストパラメータが必須となります。クライアント認証方式の詳細については『OAuth 2.0クライアント認証』を参照してください。

下記はクライアントタイプがパブリックの場合のトークンリクエストの例です。

POST https://trial.authlete.net/api/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

client_id=app_1&grant_type=authorization_code&code={{authorization_code}}&redirect_uri=https://nextdev-api.authlete.net/api/mock/redirection

アプリ1のトークンレスポンス

Native SSOに準拠するトークンレスポンスには、アクセストークンやリフレッシュトークン (任意) に加えて、Native SSOに準拠するIDトークンおよびデバイスシークレットが含まれます。 デバイスシークレットはdevice_secretプロパティの値として返却されます。

{
  "access_token": "R28TIqhCydVvH2x2a3XsOzJykFEs7yFotO4ip-a2MbY",
  "token_type": "Bearer",
  "expires_in": 86400,
  "scope": "openid device_sso",
  "refresh_token": "lmlNXafSApRDAq7gZvy40ojya9bplgFSHczms46mTms",
  "id_token": "eyJraWQiOiJaWUdJT0hZdUE5SXBVaWpWd1FOdWwzbkU1MzZ4MUpTV0hpT2ZkUzdzYWRnIiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiOiJodHRwczovL3RyaWFsLmF1dGhsZXRlLm5ldCIsInN1YiI6IjEwMDQiLCJhdWQiOlsiYXBwXzEiXSwiZXhwIjoxNzQ2NDM3MTE5LCJpYXQiOjE3NDYzNTA3MTksImF1dGhfdGltZSI6MTc0NjM1MDY3MiwiZHNfaGFzaCI6IlhrYmdHQ1JKUTFOQUhuS25NbjhKMFhIS25fOEVNenhCOWFRdUZITk0ycDQiLCJzaWQiOiJub2RlMDM4Y2F0N2ozMDhzZzE4MjhtMXNnMmRleGwzIn0.JAYlCEbGhjJwpgSZ4lUNaXkWD2ICeDs6FCBd3bKRvKPhrrGZKUAZDRij_Bmn_AF7DyTQS5ALHl82cJqjaLCcIw",
  "device_secret": "b81d5ae9-9f85-4c6d-8658-1a36ffa42c83"
}

トークンレスポンスに含まれるid_tokenプロパティの値がIDトークンです。

eyJraWQiOiJaWUdJT0hZdUE5SXBVaWpWd1FOdWwzbkU1MzZ4MUpTV0hpT2ZkUzdzYWRnIiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiOiJodHRwczovL3RyaWFsLmF1dGhsZXRlLm5ldCIsInN1YiI6IjEwMDQiLCJhdWQiOlsiYXBwXzEiXSwiZXhwIjoxNzQ2NDM3MTE5LCJpYXQiOjE3NDYzNTA3MTksImF1dGhfdGltZSI6MTc0NjM1MDY3MiwiZHNfaGFzaCI6IlhrYmdHQ1JKUTFOQUhuS25NbjhKMFhIS25fOEVNenhCOWFRdUZITk0ycDQiLCJzaWQiOiJub2RlMDM4Y2F0N2ozMDhzZzE4MjhtMXNnMmRleGwzIn0.JAYlCEbGhjJwpgSZ4lUNaXkWD2ICeDs6FCBd3bKRvKPhrrGZKUAZDRij_Bmn_AF7DyTQS5ALHl82cJqjaLCcIw

このIDトークンのペイロード部をbase64urlでデコードすると次のようになります。Native SSO仕様に準拠するIDトークンにはds_hashクレームとsidクレームが含まれます。

{
  "iss": "https://trial.authlete.net",
  "sub": "1004",
  "aud": [
    "app_1"
  ],
  "exp": 1746437119,
  "iat": 1746350719,
  "auth_time": 1746350672,
  "ds_hash": "XkbgGCRJQ1NAHnKnMn8J0XHKn_8EMzxB9aQuFHNM2p4",
  "sid": "node038cat7j308sg1828m1sg2dexl3"
}

ds_hashクレームはデバイスシークレットのハッシュ値です。ハッシュ値をどのように計算するかは実装依存とされていますが、ds_hashクレームにより、IDトークンとデバイスシークレットを関連付けることができます。

sidクレームはユーザの認証セッションを一意に特定する文字列です。いわゆるセッションIDです。

アプリケーションは、取得したIDトークンとデバイスシークレットを、Native SSOで連携したい他のアプリケーション群がアクセスできる場所に保存します。


アプリ2のトークンリクエスト

Native SSO仕様は、RFC 8693: OAuth 2.0 Token Exchange仕様を拡張し、Native SSOを実現するための要求事項を追加しています。 二つ目のアプリケーションは、一つ目のアプリケーションが保存したIDトークンとデバイスシークレットを取り出し、それらを用いてNative SSO仕様に準拠するトークン交換リクエストを組み立てます。

Native SSO仕様に準拠するトークン交換リクエストのリクエストパラメータは次の通りです。

パラメータ 説明
grant_type グラントタイプです。トークン交換リクエストではurn:ietf:params:oauth:grant-type:token-exchangeを指定します。
audience トークン交換リクエストにより発行されるトークンを使用する対象です。Native SSO用のトークン交換リクエストではOpenIDプロバイダの識別子を指定します。
subject_token 誰のためのトークン交換リクエストであるかを示すトークンです。Native SSO用のトークン交換リクエストではIDトークンを指定します。
subject_token_type subject_tokenのタイプを示す識別子です。Native SSO用のトークン交換リクエストではsubject_tokenは常にIDトークンなので、subject_token_typeパラメータの値にはurn:ietf:params:oauth:token-type:id_tokenを指定します。
actor_token トークン交換リクエストの実行者を表すトークンです。Native SSO用のトークン交換リクエストではデバイスシークレットを指定します。
actor_token_type actor_tokenのタイプを示す識別子です。Native SSO用のトークン交換リクエストではactor_tokenは常にデバイスシークレットなので、actor_token_typeパラメータの値にはurn:openid:params:token-type:device-secretを指定します。この値はNative SSO仕様が新たに定義するトークンタイプです。
scope トークン交換リクエストの結果発行されるアクセストークンに紐付けるスコープです。このパラメータは任意です。

また、上記に加え、アプリケーションのクライアントタイプ (RFC 6749 Section 2.1) がクレデンシャルかパブリックか、また、クライアントタイプがコンフィデンシャルの場合にどのクライアント認証方式を用いるかにより、追加のパラメータが必要になります。

例えば、クライアントタイプがパブリックの場合はclient_idリクエストパラメータが必須となります。一方、クライアントタイプがコンフィデンシャルでクライアント認証方式としてprivate_key_jwtを用いる場合はclient_assertionおよびclient_assertion_typeリクエストパラメータが必須となります。クライアント認証方式の詳細については『OAuth 2.0クライアント認証』を参照してください。

下記はクライアントタイプがパブリックの場合のトークン交換リクエストの例です。

POST https://trial.authlete.net/api/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

client_id=app_2&grant_type=urn:ietf:params:oauth:grant-type:token-exchange&audience=https://trial.authlete.net&subject_token={{id_token}}&subject_token_type=urn:ietf:params:oauth:token-type:id_token&actor_token={{device_secret}}&actor_token_type=urn:openid:params:token-type:device-secret&scope=openid

アプリ2のトークンレスポンス

トークン交換リクエストに対するレスポンスは、認可コードフローのトークンレスポンスとほぼ同じです。 唯一の違いはissued_token_typeプロパティが含まれていることです。 Native SSOの場合、issued_token_typeの値はurn:ietf:params:oauth:token-type:access_tokenです。

{
  "access_token": "rH9115-g83z9zIiCJ1mzIe8mza3bX4NaBTWmGs5qqow",
  "token_type": "Bearer",
  "expires_in": 86400,
  "scope": "openid",
  "refresh_token": "_F7NMCU1ny8DQ-3Pru_owgII52gIew0T6wuWKeIrfL4",
  "id_token": "eyJraWQiOiJaWUdJT0hZdUE5SXBVaWpWd1FOdWwzbkU1MzZ4MUpTV0hpT2ZkUzdzYWRnIiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiOiJodHRwczovL3RyaWFsLmF1dGhsZXRlLm5ldCIsInN1YiI6IjEwMDQiLCJhdWQiOlsiYXBwXzIiXSwiZXhwIjoxNzQ2NDM4MzUxLCJpYXQiOjE3NDYzNTE5NTEsImRzX2hhc2giOiJYa2JnR0NSSlExTkFIbktuTW44SjBYSEtuXzhFTXp4QjlhUXVGSE5NMnA0Iiwic2lkIjoibm9kZTAzOGNhdDdqMzA4c2cxODI4bTFzZzJkZXhsMyJ9.8jNNF5mpeHnbqp1FTK_1adR8FlgPmHK9_rwUzaz-o5P7RMyaelBaSj74IhxHY6wbCJeD0n_N14h8vD8zWYh-8w",
  "device_secret": "b81d5ae9-9f85-4c6d-8658-1a36ffa42c83",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}

上記のレスポンス例からIDトークンを取り出し、

eyJraWQiOiJaWUdJT0hZdUE5SXBVaWpWd1FOdWwzbkU1MzZ4MUpTV0hpT2ZkUzdzYWRnIiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiOiJodHRwczovL3RyaWFsLmF1dGhsZXRlLm5ldCIsInN1YiI6IjEwMDQiLCJhdWQiOlsiYXBwXzIiXSwiZXhwIjoxNzQ2NDM4MzUxLCJpYXQiOjE3NDYzNTE5NTEsImRzX2hhc2giOiJYa2JnR0NSSlExTkFIbktuTW44SjBYSEtuXzhFTXp4QjlhUXVGSE5NMnA0Iiwic2lkIjoibm9kZTAzOGNhdDdqMzA4c2cxODI4bTFzZzJkZXhsMyJ9.8jNNF5mpeHnbqp1FTK_1adR8FlgPmHK9_rwUzaz-o5P7RMyaelBaSj74IhxHY6wbCJeD0n_N14h8vD8zWYh-8w

ペイロード部をbase64urlでデコードすると次のようになります。

{
  "iss": "https://trial.authlete.net",
  "sub": "1004",
  "aud": [
    "app_2"
  ],
  "exp": 1746438351,
  "iat": 1746351951,
  "ds_hash": "XkbgGCRJQ1NAHnKnMn8J0XHKn_8EMzxB9aQuFHNM2p4",
  "sid": "node038cat7j308sg1828m1sg2dexl3"
}

ds_hashクレームとsidクレームの値は一つ目のアプリケーションが受け取ったIDトークンと同じですが、audクレームの値は異なっています。このIDトークンでは、二つ目のアプリケーションの識別子であるapp_2aud配列に含まれています。


利用設定

Authleteでは、バージョン3.0以降でNative SSOをサポートします。


サービス設定


nativeSsoSupportedプロパティ

Native SSOをサポートするかどうかを示す新しい真偽値プロパティnativeSsoSupportedがサービスに追加されました。 このプロパティのデフォルト値はfalseなので、Native SSOを利用する場合は明示的にtrueに設定する必要があります。

nativeSsoSupportedプロパティは、Native SSO仕様が定義するサーバメタデータのnative_sso_supportedに対応しています。 nativeSsoSupportedtrueに設定されている場合、Authleteの/service/configurationAPIが生成するディスカバリ文書 (OpenID Connect Discovery 1.0) に次のエントリが追加されます。

"native_sso_supported": true

nativeSsoSupportedfalseの場合、AuthleteはNative SSOについて何も知らないかのような動作をします。 例えば、device_ssoスコープは特別な意味を持たなくなり、トークンタイプurn:openid:params:token-type:device-secretは未知のトークンタイプとして扱われます (actor_token_typeの値として指定するとエラーになります)。


device_ssoスコープ

Native SSOのため、認可サーバがdevice_ssoスコープをサポートしている必要があります。 明示的にdevice_ssoスコープを追加登録してください。

OAuth 2.0仕様の要請により、認可サーバは未知のスコープをエラー扱いせず、単に無視します。 そのため、device_ssoスコープを登録し忘れていると、認可リクエストにdevice_ssoスコープを含めていても、何の警告もなくNative SSO用の処理は実行されないので注意してください。


サービスのグラントタイプ

サービスがサポートするグラントタイプにTOKEN_CHANGEを追加してください。

また、AuthleteにはToken Exchangeに関する設定項目が複数存在するので、それらが意図通りの設定になっているか確認してください。特に次の二つには留意してください。

プロパティ 説明
tokenExchangeByConfidentialClientsOnly トークン交換リクエストを実行できるクライアントをコンフィデンシャルクライアントのみに限定するかどうかを示す真偽値プロパティです。この値がtrueに設定されていると、パブリッククライアントからのトークン交換リクエストは受け付けられなくなります。
tokenExchangeByPermittedClientsOnly トークン交換リクエストを実行できるクライアントを事前に許可を与えられたクライアントのみに限定するかどうかを示す真偽値プロパティです。この値がtrueに設定されていると、トークン交換リクエストを実行することを明示的に許可されていないクライアントからのトークン交換リクエストは受け付けられなくなります。

クライアント設定


device_ssoスコープ

設定により、クライアントが要求できるスコープが限定されている場合があります。 そのような設定になっている場合、device_ssoスコープを要求可能なスコープのリストに追加してください。


クライアントのグラントタイプ

クライアントが利用する可能性のあるグラントタイプのリストにTOKEN_EXCHANGEを追加してください。

また、サービスのtokenExchangeByPermittedClientsOnlyプロパティがtrueに設定されていると、明示的に許可を与えられていないクライアントからのトークン交換リクエストは拒否されてしまいます。サービスの設定がそのようになっている場合、クライアントのextension.tokenExchangePermittedプロパティにtrueを設定する必要があります。


OpenIDプロバイダ実装

認証セッションとデバイスシークレットの管理

ユーザ認証・ユーザ管理とOAuth 2.0/OpenID Connectプロトコル処理を完全に分離し、後者のみの機能を提供するのがAuthleteの大きな特長となっており、市場から大きな支持を得ています。 この独特のアーキテクチャのため、Authleteはユーザの認証セッションの管理を全くおこないません。 そのため、認証セッションの管理はAuthlete利用者 (OpenIDプロバイダ実装者) がおこないます。 Native SSOに準拠するIDトークンにはセッションIDの値をsidクレームの値として埋め込む必要がありますが、その値はOpenIDプロバイダが管理することになります。

また、Authleteアーキテクチャの別の特長に、「バックエンドで動き、クライアントアプリケーションと直接通信をしない」、というものがあります。 結果として、Authleteはクライアントアプリケーションが動いているデバイスの情報を直接知ることはできません。 Native SSOに準拠するOpenIDプロバイダはデバイス情報に紐付くデバイスシークレットを発行できなければなりませんが、Authleteはそれができないということです。 デバイスシークレットの生成・管理もOpenIDプロバイダ側の責任範囲となります。

一方で、Native SSOに準拠するIDトークンやトークンレスポンスの生成はAuthleteがおこないます。 これらの生成に必要なセッションID、デバイスシークレット (およびデバイスシークレットハッシュ) は、Authlete APIのリクエストパラメータを介してOpenIDプロバイダからAuthleteに渡すことになります。


認可エンドポイントの実装

認可リクエストがNative SSOを要求していると判断された場合、具体的には次の条件が全て満たされた場合、Authleteの/auth/authorizationAPIからのレスポンスには、nativeSsoRequestedという真偽値プロパティが値trueで含まれます。

条件
1 サービスがNative SSOをサポートしている。 (Service.nativeSsoSupportedの値がtrueと設定されている。)
2 サービスがopenidスコープとdevice_ssoスコープをサポートしている。
3 クライアントがopenidスコープとdevice_ssoスコープを要求することを許可されている。(Requestable Scopes機能による制限を受けていない。)
4 認可リクエストのscopeopeniddevice_ssoが含まれている。
5 サービスが認可コードフローをサポートしている。(Service.supportedGrantTypesAUTHORIZATION_CODEが含まれている。)
6 クライアントが認可コードフローを使用すると宣言している。(Client.grantTypesAUTHORIZATION_CODEが含まれている。)
7 認可リクエストのresponse_typecodeが含まれている。
8 サービスがそのレスポンスタイプをサポートしている。(Service.supportedResponseTypesに当該レスポンスタイプが含まれている。)
9 クライアントがそのレスポンスタイプを使用すると宣言している。(Client.responseTypesに当該レスポンスタイプが含まれている。)

この表の条件は複雑に見えますが、サービスやクライアントの設定条件を除いて認可リクエストの条件だけに限って言えば、「scopeopeniddevice_ssoが含まれていてresponse_typecodeが含まれている」場合、Native SSOを要求していると判断されます。

nativeSsoRequestedプロパティの値がtrueの場合、認可エンドポイントの実装は/auth/authorization/issueAPIを呼ぶ際にsessionIdリクエストパラメータを含めなければなりません。 その値には、現在のユーザの認証セッションを表す識別子 (いわゆるセッションID) を指定します。ここで指定された値は、IDトークンのsidクレームの値として用いられます。

認可エンドポイントの実装は、実際のセッションIDの値をsessionIdパラメータの値としてAuthleteに渡してもよいですし、何らかの変換を加えてからAuthleteに渡してもかまいません。 ただし、あまり長い文字列は使えません。おおむね150文字程度が上限となります。sessionIdとして渡された文字列を暗号化してbase64urlエンコードした結果得られる文字列の長さが255を超えるとエラーになります (暗号化のロジックは非公開)。

/auth/authorization APIのレスポンス内のnativeSsoRequestedの値がtruefalseかに関係なく、常に/auth/authorization/issueAPIにsessionIdパラメータを渡すという実装でもかまいません。認可リクエストがNative SSOを要求していない場合、たとえsessionIdパラメータが指定されていたとしても、AuthleteはIDトークンにsidクレームを埋め込みません。


トークンエンドポイントの実装 (検証)

トークンリクエストがNative SSO用のものだと判断された場合、具体的には次の条件セットのいずれかが満たされた場合、Authleteの/auth/tokenAPIに含まれるactionプロパティの値はNATIVE_SSOになります。

条件セット1: 認可コードフロー
1 サービスがNative SSOをサポートしている。 (Service.nativeSsoSupportedの値がtrueと設定されている。)
2 サービスがopenidスコープとdevice_ssoスコープをサポートしている。
3 クライアントがopenidスコープとdevice_ssoスコープを要求することを許可されている。(Requestable Scopes機能による制限を受けていない。)
4 対応する認可リクエストのscopeopeniddevice_ssoが含まれている。
5 サービスが認可コードフローをサポートしている。(Service.supportedGrantTypesAUTHORIZATION_CODEが含まれている。)
6 クライアントが認可コードフローを使用すると宣言している。(Client.grantTypesAUTHORIZATION_CODEが含まれている。)
7 grant_typeパラメータの値がauthorization_codeである。
条件セット2: リフレッシュトークンフロー
1 サービスがNative SSOをサポートしている。 (Service.nativeSsoSupportedの値がtrueと設定されている。)
2 サービスがdevice_ssoスコープをサポートしている。
3 クライアントがdevice_ssoスコープを要求することを許可されている。(Requestable Scopes機能による制限を受けていない。)
4 サービスがリフレッシュトークンフローをサポートしている。(Service.supportedGrantTypesREFRESH_TOKENが含まれている。)
5 クライアントがリフレッシュトークンフローを使用すると宣言している。(Client.grantTypesREFRESH_TOKENが含まれている。)
6 grant_typeパラメータの値がrefresh_tokenである。
7 トークンリクエストのscopeパラメータの指定によりスコープの範囲が狭められたとしても、device_ssoスコープが依然としてカバーされている。
8 提示されたリフレッシュトークンがユーザ認証セッションに紐付けられている。(実質的にNative SSOに準拠した認可コードフローにより生成されたリフレッシュトークンしか使えない。)
条件セット3: トークン交換フロー
1 サービスがNative SSOをサポートしている。 (Service.nativeSsoSupportedの値がtrueと設定されている。)
2 サービスがトークン交換フローをサポートしている。(Service.supportedGrantTypesTOKEN_EXCHANGEが含まれている。)
3 クライアントがトークン交換フローを使用すると宣言している。(Client.grantTypesTOKEN_EXCHANGEが含まれている。)
4 サービスのトークン交換フローの各種設定 (tokenExchangeByConfidentialClientsOnly等) がクライアントのトークン交換リクエストを拒絶しない。
5 grant_typeパラメータの値がurn:ietf:params:oauth:grant-type:token-exchangeである。
6 actor_token_typeパラメータの値がurn:openid:params:token-type:device-secretである。

上記の条件群は複雑に見えますが、要は、「Native SSOに準拠したIDトークンとトークンレスポンスを生成する必要がある」とAuthleteが判断した場合、actionの値がNATIVE_SSOになります。

actionの値がNATIVE_SSOの場合、トークンエンドポイントの実装はトークンリクエストの処理を完了させるために/nativessoAPIをコールする必要があります。しかし、それに先立ち、セッションIDやデバイスシークレットの検証、必要に応じてデバイスシークレットの生成を行わなければなりません。


セッションIDの検証

/auth/tokenAPIからのレスポンスに含まれるactionの値がNATIVE_SSOの場合、そのレスポンスにはsessionIdパラメータが含まれ、その値はユーザ認証セッションを表す値、すなわちセッションIDです。トークンエンドポイントの実装は、このセッションIDが依然として有効かどうかを確認しなければなりません。無効の場合は/nativessoAPIを呼ばず、代わりにinvalid_grantエラーを示すトークンレスポンスを生成してクライアントに返却してください。

HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store

{
    "error": "invalid_grant",
    "error_description": "The session ID is no longer valid."
}

/auth/tokenAPIのレスポンスに含まれるsessionIdの値は、元々は/auth/authorization/issueAPIのsessionIdリクエストパラメータの値としてOpenIDプロバイダからAuthleteに渡されたものです。 AuthleteはそのセッションIDが有効かどうか判断しない (判断できない) ので、セッションIDの検証はOpenIDプロバイダ側で実施する必要があります。

トークンリクエストが認可コードフローまたはリフレッシュトークンフローのものである場合、sessionIdの値は、認可コードまたはリフレッシュトークンに紐付くセッションIDです。

一方、トークンリクエストがトークン交換フローのものである場合、sessionIdの値はsubject_tokenパラメータの値として提示されたIDトークンのsidクレームの値です。


デバイスシークレットの検証 (認可コードフローとリフレッシュトークンフロー)

actionの値がNATIVE_SSOで、当該トークンリクエストが認可コードフローもしくはリフレッシュトークンフローのものである場合 (grantTypeAUTHORIZATION_CODEまたはREFRESH_TOKENの場合)、/auth/tokenAPIのレスポンスにはdeviceSecretパラメータが含まれている可能性があります。 その値は、トークンリクエストのdevice_secretリクエストパラメータの値です。 このリクエストパラメータ自体はオプショナルです。

deviceSecretの値がnullでない場合、その値が有効かどうかを検証してください。 もし有効であれば、その値をそのまま/nativessoAPIに渡してください。 一方、deviceSecretの値が存在しない、または無効の場合、新しいデバイスシークレットを生成し、その値を/nativessoAPIに渡してください。


デバイスシークレットの検証 (トークン交換フロー)

actionの値がNATIVE_SSOで、当該トークンリクエストがトークン交換フローのものである場合 (grantTypeTOKEN_EXCHAGEの場合)、/auth/tokenAPIからのレスポンスには必ずdeviceSecretパラメータとdeviceSecretHashパラメータが含まれます。

deviceSecretの値は、トークンリクエストのactor_tokenパラメータの値として指定されたデバイスシークレットです。 deviceSecretHashの値は、トークンリクエストのsubject_tokenパラメータの値として指定されたIDトークンに含まれるds_hashクレームの値です。

トークンエンドポイントの実装では、デバイスシークレットハッシュがデバイスシークレットに対応するものかどうか確認してください。 対応するものでない場合は/nativessoAPIを呼ばず、代わりにinvalid_grantエラーを示すトークンレスポンスを生成してクライアントに返却してください。

HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store

{
    "error": "invalid_grant",
    "error_description": "The device secret hash in the subject token does not correspond to the device secret."
}

トークンエンドポイントの実装 (nativesso APIコール)

セッションIDの検証およびデバイスシークレットの検証または生成の完了後、Native SSOに準拠するIDトークンとトークンレスポンスを生成するため、Authleteの/nativessoAPIをコールしてください。


nativessoリクエスト

/nativessoAPIは、application/jsonまたはapplication/x-www-form-urlencoded形式のHTTP POSTリクエストを受け付けます。リクエストパラメータは下表の通りです。

パラメータ 要否 説明
accessToken 必須 /auth/tokenAPIのレスポンスにjwtAccessTokenが含まれていればその値を、含まれていなければaccessTokenの値を指定します。指定された値は/nativessoAPIが用意するトークンレスポンスのaccess_tokenプロパティの値になります。
refreshToken 任意 /auth/tokenAPIのレスポンスに含まれるrefreshTokenの値を指定します。指定された値は/nativessoAPIが用意するトークンレスポンスのrefresh_tokenプロパティの値になります。
deviceSecret 必須 /auth/tokenAPIのレスポンスにdeviceSecretが含まれていればその値を、含まれていなければ新しいデバイスシークレットを生成してその値を指定します。指定された値は/nativessoAPIが用意するトークンレスポンスのdevice_secretプロパティの値になります。
deviceSecretHash 推奨 デバイスシークレットのハッシュ値を指定します。デバイスシークレットからハッシュ値を求めるロジックはOpenIDプロバイダの実装依存です。このパラメータが省略された場合、/nativessoAPIの実装はdeviceSecretパラメータの値のSHA-256ハッシュを計算し、そのハッシュ値をbase64urlエンコードしたものをデバイスシークレットハッシュとします。deviceSecretHashパラメータで指定された値、または/nativessoAPIが生成した値は、/nativessoAPIが生成するIDトークンにds_hashクレームの値として埋め込まれます。
sub 任意 /nativessoAPIが生成するIDトークンのsubクレームの値です。このパラメータが省略された場合、accessTokenパラメータで指定されたアクセストークンに紐付くサブジェクトがsubクレームの値として用いられます。

IDトークンの生成を伴うAuthlete APIにはsubリクエストパラメータがあるため、/nativessoAPIもこのリクエストパラメータを受け付けます。しかしながら、Native SSOの文脈でアクセストークンのサブジェクトと異なる値をsubクレームの値に用いると、意図しない不整合を起こす可能性があるので、このsubパラメータを使う際は慎重におこなってください。

このsubパラメータの値に関わらず、Native SSOのトークン交換フローでアクセストークンを新規作成する際、Authleteはサブジェクトトークン (過去の/nativessoAPIコールにより生成されたIDトークン) のsubクレームの値をアクセストークンに紐付くサブジェクトとして設定します。/auth/tokenAPIがレスポンスを返した時点で既にアクセストークンの生成は完了しており、/nativessoAPIのsubパラメータではアクセストークンに紐付くサブジェクトを変更することはできません。
claims 任意 IDトークンに埋め込む追加のクレーム群を指定します。形式はJSONオブジェクトを表す文字列でなければなりません。
idtHeaderParams 任意 IDトークンのJWSヘッダに埋め込む追加のパラメータ群を指定します。形式はJSONオブジェクトを表す文字列でなければなりません。
idTokenAudType 任意 IDトークンのaudクレームの形式を指定します。arrayを指定した場合はaudクレームの値はJSON配列となり、stringを指定した場合はJSON文字列となります。このidTokenAudTypeパラメータを省略した場合、サービスのidTokenAudTypeプロパティの設定が参照されます。サービスの当プロパティが設定されていない場合、audクレームの値はJSON配列となります。

nativessoレスポンス

/nativessoAPIからのレスポンスのメッセージボディの形式はJSONです。他の多くのAuthlete APIと同様に、/nativessoAPIのレスポンスにもactionプロパティが含まれています。トークンエンドポイントの実装では、このactionの値に従ってトークンレスポンスを組み立ててください。

actionOKの場合、/nativessoAPIの処理が全て成功裡に終わったことを示します。 このとき、トークンエンドポイントの実装はクライアントに成功応答 (200 OK) を返すようにします。 /nativessoAPIからのレスポンスに含まれるresponseContentプロパティの値は、トークンレスポンスのメッセージボディとしてそのまま使えます。 このため、成功応答は次のように構築できます。

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

(responseContentの値をここに置く)

actionINTERNAL_SERVER_ERRORの場合、Authlete側で何か問題が発生したことを意味します。 例えば、accessTokenパラメータで指定されたアクセストークンをデータベースから取り出す際にデータベースエラーが発生した、といった問題です。 このとき、トークンエンドポイントの実装はクライアントにエラーレスポンスを返すべきです。 最も単純な実装では、500 Internal Server Errorを返します。

HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Cache-Control: no-store

(responseContentの値をここに置く)

しかし、商用環境では、500エラーとは異なる抽象度の高い (エラーの内容を直接記述しない) エラーを返したほうがよいかもしれません。

actionCALLER_ERRORの場合、API呼び出し側 (OpenIDプロバイダの実装) に問題があることを示しています。 例えば、必須パラメータであるaccessTokenを含めなかった、といった問題です。 CALLER_ERRORが返された場合はOpenIDプロバイダの実装を見直してください。


シングルログアウト

一回の操作で複数のアプリケーションからログアウトすることをシングルログアウト (SLO) と呼びます。 シングルサインオンと対となります。

Native SSO仕様はシングルログアウトを実現するための具体的なプロトコルを定義していません。 しかし、特定のセッションIDに紐付くアクセストークン・リフレッシュトークン群をまとめて削除することにより、シングルログアウトを実現できます。

Authleteは/nativesso/logoutAPIによりシングルログアウトの機能を提供します。 このAPIはapplication/jsonまたはapplication/x-www-form-urlencoded形式のHTTP POSTリクエストを受け付けます。リクエストパラメータはsessionId一つのみです。 このリクエストパラメータに対象となるセッションIDを指定して/nativesso/logoutAPIを呼ぶと、そのセッションIDに紐付いたアクセストークン・リフレッシュトークンが全て削除されます。

POST https://{{authlete_api_server}}/api/{{service_id}}/nativesso/logout HTTP/1.1
Authorization: Bearer {{authlete_access_token}}
Content-Type: application/json

{
    "sessionId": "{{session_id}}"
}
{
  "action": "OK",
  "count": 2,
  "resultCode": "A503001",
  "resultMessage": "[A503001] The /nativesso/logout API call successfully deleted 2 access/refresh token record(s)."
}

/nativesso/logoutは、指定されたセッションIDに紐付けられたアクセストークン・リフレッシュトークンが存在せずに結果として削除件数が0件だったとしてもエラーにはなりません。

{
  "action": "OK",
  "count": 0,
  "resultCode": "A503002",
  "resultMessage": "[A503002] The /nativesso/logout API call completed without deleting any access/refresh token records."
}

サンプル実装

OpenIDプロバイダ側のNative SSOサンプル実装はjava-oauth-serverauthlete-java-jaxrsライブラリに含まれています。どちらもJava言語によるオープンソース実装です。

以下はサンプル実装を読む際のヒントです。

  • /auth/authorization/issueAPIに渡すセッションIDは、AuthorizationDecisionEndpointの中でHttpServletRequest.getSession(false).getId()を実行することで取得しています。ただし、この処理で得られるセッションはWebサーバとWebブラウザ間のHTTPセッションであるため、Native SSOの商用実装では別の仕組みを用いることになると思います。
  • 取得したセッションIDはAuthorizationDecisionHandlerSpiインターフェースを介してAuthorizationDecisionHandlerに渡ります。
  • AuthorizationDecisionHandlerはAuthleteApiCallercallAuthorizationIssueメソッドを介してAuthleteの/auth/authorization/issueAPIを呼びます。
  • /auth/tokenAPIのレスポンスのactionに基づくディスパッチ処理はTokenRequestHandlerに書かれています。
  • TokenRequestHandlerは、actionNATIVE_SSOの場合、TokenRequestHandlerSpiインターフェースのnativeSsoメソッドを呼びます。
  • TokenRequestHandlerSpiインターフェースの実装であるTokenRequestHandlerSpiImplは、nativeSsoメソッドの中からNativeSsoProcessorprocessメソッドを呼びます。
  • NativeSsoProcessorは、/auth/tokenAPIからのレスポンスを表すTokenResponseクラスのインスタンスから、セッションID、デバイスシークレット、デバイスシークレットハッシュを取り出します。
  • NativeSsoProcessorは、retrieveDeviceId()メソッドの中で、トークンエンドポイントにアクセスしてきたデバイスのデバイス識別子を取得します。ただし、サンプル実装ではこのメソッドの実装は空なので注意してください
  • NativeSsoProcessorは、validateParametersメソッドの中で、セッションID、デバイスシークレット、デバイスシークレットハッシュ、デバイス識別子の検証をおこなっています。
  • セッションIDが有効かどうかのチェックはSessionTrackerisActiveSessionId(String)メソッドを呼び出すことで行っています。SessionTrackerはHttpSessionListenerインターフェースを実装しており、セッションの生成と削除を監視しています。SessionTrackerはweb.xml内でリスナーとして登録されています。
  • デバイスシークレットとセッションID、デバイスシークレットハッシュ、デバイス識別子の関係はDeviceSecretクラスで表現されています。
  • DeviceSecretのインスタンス群はDeviceSecretManagerが管理しています。

Authlete実装

Authleteバージョン

Authleteでは、バージョン3.0以降でNative SSOをサポートします。


Native SSOバージョン

Authlete Native SSOの最初のバージョンはOpenID Connect Native SSO for Mobile Apps 1.0仕様のドラフト07に基づいて実装されました。そのため、古い版で使われていたurn:x-oath:params:*識別子にかわってurn:openid:params:*識別子が使われています。


トークン交換リクエスト検証

トークン交換リクエストにactor_token_typeパラメータが含まれており、その値がurn:openid:params:token-type:device-secretである場合、Authleteはトークン交換リクエスト固有のリクエストパラメータに対して下記の検証をおこないます。これらを全てパスした場合のみ、/auth/tokenAPIのレスポンスのactionNATIVE_SSOになります。

  1. audienceパラメータが指定されており、その値がサービスのOpenIDプロバイダ識別子 (Service.issuerに設定されている値) と一致する。なお、audienceパラメータは複数指定することが許されており、複数指定された場合は、いずれかの値が一致すればよい。
  2. requested_token_typeパラメータが指定されている場合、その値が既知のトークンタイプである。
  3. subject_token_typeパラメータが指定されており、その値がurn:ietf:params:oauth:token-type:id_tokenである。
  4. subject_tokenパラメータが指定されており、その値 (IDトークン) が次の検証項目を全てパスする。
    • JWTとしてパースできる。
    • expクレームを含んでおり、値の型が数値である。注: Native SSOの文脈ではexpの値が現在時刻より未来であることを確認しない。これは期限切れのIDトークンをサブジェクトトークンとして使えることを意味する (参照: id_token usage)。
    • iatクレームを含んでおり、値が現在時刻または過去を示している。
    • nbfクレームを含む場合、値が現在時刻または過去を示している。
    • issクレームを含んでおり、値がサービスのOpenIDプロバイダ識別子 (Service.issuerに設定されている値) と一致する。
    • subクレームを含んでおり、値の型が文字列である。
    • audクレームを含んでおり、値の型が文字列または配列である。また、配列の場合、一つ以上の要素を含み、全ての要素の型が文字列である。
    • nonceクレームを含む場合、値の型が文字列である。
    • sidクレームを含んでおり、値の型が文字列である。
    • ds_hashクレームを含んでおり、値の型が文字列である。
    • JWEではない。
    • 署名されている。
    • 署名検証に成功する。
  5. actor_tokenパラメータが指定されている。

参考情報