はじめに
この記事は、Authlete 社のウェブサイト上で公開されている OpenID Connect Native SSO for Mobile Apps 1.0 仕様について書かれた文書『Native SSO』から、コンセプト、仕様概要、仕様詳細、参考情報、のセクションを転載したものです。Native SSO のサンプル実装などに関する追加情報については Authlete 社の文書を参照してください。
コンセプト
OpenID Connect Native SSO for Mobile Apps 1.0 (以下 Native SSO) は、同一ベンダーの管理下にある複数のモバイルアプリケーション間でシングルサインオン (SSO) を実現する仕組みを標準仕様として定義します。この仕様が実装されていると、ユーザはモバイルアプリケーション毎にユーザ認証を行う必要はなく、Native SSO で連携したアプリケーション群に対して一回のユーザ認証で済みます。
仕様概要
詳細に踏み込む前に、仕様の概要を紹介します。
まず、一つ目のアプリケーションが認可コードフローを用いて下記のトークン群を取得します。注目すべき点は、ID トークンが Native SSO 仕様に準拠していること、および、デバイスシークレットという新しいタイプのトークンが含まれていることです。
- アクセストークン
- リフレッシュトークン (任意)
- ID トークン (Native SSO 仕様準拠)
- デバイスシークレット
一つ目のアプリケーションは、二つ目のアプリケーションがアクセスできる共有ストレージに 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 仕様に準拠するためには openid と device_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
この例は実際に動きます。認可ページが表示されたら、ログイン ID とパスワードに inga
、inga
と入力してください。ログイン済みになっていて、再度ログイン ID フィールドとパスワードフィールドを表示させたい場合は、認可リクエストの末尾に &prompt=login
を追加してください。
アプリ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
この例を実際に試す場合は {{authorization_code}}
を実際の値 (認可リクエストの結果得られた認可コードの値) で置き換えてください。
アプリ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 仕様に準拠するトークン交換リクエストを組み立てます。
RFC 8693については、解説記事『RFC 8693 OAuth 2.0トークン交換』もご参照ください。
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
この例を実際に試す場合は {{id_token}}
と {{device_secre}}
を実際の値で置き換えてください。
アプリ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_2
が aud
配列に含まれています。
参考情報
- 仕様: OpenID Connect Native SSO for Mobile Apps 1.0
- 仕様: RFC 8693: OAuth 2.0 Token Exchange
- Native SSO仕様書ソースコード: openid-connect-native-sso-1_0.xml
- Native SSO仕様Issue Tracker: bitbucket.org/openid/connect/issues
- 解説文書: RFC 8693 OAuth 2.0 トークン交換