LoginSignup
1427
1103

More than 3 years have passed since last update.

IDトークンが分かれば OpenID Connect が分かる

Last updated at Posted at 2016-09-16

はじめに

「解説記事を幾つも読んだけど OpenID Connect を理解できた気がしない」― この文書は、そういう悩みを抱えたエンジニアの方々に向けた OpenID Connect 解説文書です。概念的・抽象的な話を避け、具体例を用いて OpenID Connect を解説していこうと思います。

この文書では、JWS (RFC 7515)、JWE (RFC 7516)、JWK (RFC 7517)、JWT (RFC 7519)、ID トークンの説明をおこないます。

追記(2020-03-20)
この記事の内容を含む、筆者本人による『OAuth & OIDC 入門編』解説動画を公開しました!

1. 『ID トークン』を発行するための仕様

一般の方々に対しては「OpenID Connect は認証の仕様である」という説明で良いと思います。一方、技術的な理解を渇望しているエンジニアの方々に対しては、「OpenID Connect は『ID トークン』を発行するための仕様である」という割り切り方を勧めたほうが良いと考えています。というわけで、まず、ID トークンについて説明しようと思います。

2. ID トークンの外観

一般の方々に対しては「ID トークンは認証の結果得られるトークンである」という説明で良いと思います。一方、エンジニアの方々には、「ID トークンは具体的にはこんな感じの文字列データです」と言って、まず具体例を見ていただくのが良いと考えています。そういうわけで、早速ですが、OpenID Connect の中心となる仕様書「OpenID Connect Core 1.0」の「A.2. Example using response_type=id_token」から抜粋した ID トークンの例を掲載します。なお、この ID トークンの例には改行が含まれていますが、本来は ID トークンの中に改行は含まれません。ここでは単に見やすいように改行を入れているだけです。

eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlz
cyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4
Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAi
bi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEz
MTEyODA5NzAsCiAibmFtZSI6ICJKYW5lIERvZSIsCiAiZ2l2ZW5fbmFtZSI6
ICJKYW5lIiwKICJmYW1pbHlfbmFtZSI6ICJEb2UiLAogImdlbmRlciI6ICJm
ZW1hbGUiLAogImJpcnRoZGF0ZSI6ICIwMDAwLTEwLTMxIiwKICJlbWFpbCI6
ICJqYW5lZG9lQGV4YW1wbGUuY29tIiwKICJwaWN0dXJlIjogImh0dHA6Ly9l
eGFtcGxlLmNvbS9qYW5lZG9lL21lLmpwZyIKfQ.rHQjEmBqn9Jre0OLykYNn
spA10Qql2rvx4FsD00jwlB0Sym4NzpgvPKsDjn_wMkHxcp6CilPcoKrWHcip
R2iAjzLvDNAReF97zoJqq880ZD1bwY82JDauCXELVR9O6_B0w3K-E7yM2mac
AAgNCUwtik6SjoSUZRcf-O5lygIyLENx882p6MtmwaL1hd6qn5RZOQ0TLrOY
u0532g9Exxcm-ChymrB4xLykpDj3lUivJt63eEGGN6DH5K6o33TcxkIjNrCD
4XB1CKKumZvCedgHHF3IAK4dVEDSUoGlH9z4pP_eWYNXvqQOjGs-rDaQzUHl
6cQQWNiDpWOl_lxXjQEvQ

この ID トークンの例の中には、ピリオドが二つ含まれています。一つ目は一行目の右から 9 文字目に、二つ目は九行目の右から 22 文字目にあります。この二つのピリオドにより、ID トークンは三つの部分に分けられます。 (三つにならないケースについては後述します。)

三つの部分は、先頭から順に、ヘッダー、ペイロード (本文)、署名、を表しています。つまり、ID トークンは形式的には次のような形になっています。

ヘッダー.ペイロード.署名

前掲の ID トークンの場合、ヘッダー、ペイロード、署名の値を別々に切り出すと、次のようになります。

# ヘッダー
eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ
# ペイロード
ewogImlz
cyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4
Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAi
bi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEz
MTEyODA5NzAsCiAibmFtZSI6ICJKYW5lIERvZSIsCiAiZ2l2ZW5fbmFtZSI6
ICJKYW5lIiwKICJmYW1pbHlfbmFtZSI6ICJEb2UiLAogImdlbmRlciI6ICJm
ZW1hbGUiLAogImJpcnRoZGF0ZSI6ICIwMDAwLTEwLTMxIiwKICJlbWFpbCI6
ICJqYW5lZG9lQGV4YW1wbGUuY29tIiwKICJwaWN0dXJlIjogImh0dHA6Ly9l
eGFtcGxlLmNvbS9qYW5lZG9lL21lLmpwZyIKfQ
# 署名
rHQjEmBqn9Jre0OLykYNn
spA10Qql2rvx4FsD00jwlB0Sym4NzpgvPKsDjn_wMkHxcp6CilPcoKrWHcip
R2iAjzLvDNAReF97zoJqq880ZD1bwY82JDauCXELVR9O6_B0w3K-E7yM2mac
AAgNCUwtik6SjoSUZRcf-O5lygIyLENx882p6MtmwaL1hd6qn5RZOQ0TLrOY
u0532g9Exxcm-ChymrB4xLykpDj3lUivJt63eEGGN6DH5K6o33TcxkIjNrCD
4XB1CKKumZvCedgHHF3IAK4dVEDSUoGlH9z4pP_eWYNXvqQOjGs-rDaQzUHl
6cQQWNiDpWOl_lxXjQEvQ

3. JSON Web Signature (JWS)

前節で紹介した「ヘッダー.ペイロード.署名」という形式は、実は RFC 7515 (JSON Web Signature (JWS)) の「7.1. JWS Compact Serialization」で定義されている「JSON Web Signature (JWS) の Compact Serialization 形式」です。具体的には次のように定義されています。 (RFC 7515JSON Serialization 形式も定義していますが、ここでは扱いません。)

BASE64URL(UTF8(JWS Protected Header)) || '.' ||
BASE64URL(JWS Payload) || '.' ||
BASE64URL(JWS Signature)

この定義を見ると、三つの部分はそれぞれ、元のデータを base64url (RFC 4648, 5. Base 64 Encoding with URL and Filename Safe Alphabet) でエンコードしたものだということが分かります。ということは、base64url でデコードすると、元のデータが得られます。早速やってみましょう。

3.1. JWS ヘッダーのデコード

まず、ヘッダー部をデコードしてみます。ここでは、base64-url-cli をインストールし、コマンドラインでデコードをおこないます。

$ npm install -g base64-url-cli
$ base64url decode eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ

結果、次のような JSON が得られます。

{"kid":"1e9gdk7","alg":"RS256"}

この JSON には、kidalg という二つのパラメーターが含まれています。これらに加え、その他の有効なパラメーターは RFC 7515 の「4.1. Registered Header Parameter Names」に列挙されています。

パラメーター名 説明
alg アルゴリズム
jku JWK セット URL
jwk JSON Web キー
kid キー ID
x5u X.509 URL
x5c X.509 証明書チェーン
x5t X.509 証明書 SHA-1 Thumbprint
x5t#S256 X.509 証明書 SHA-256 Thumbprint
typ JWS 自身のメディアタイプ
cty ペイロードのメディアタイプ
crit 必須パラメーター群指定

これらのうち、alg は署名で使用するアルゴリズムを指定するものです。署名については後述します。

3.2. JWS ペイロードのデコード

同様にして、ペイロードを base64url でデコードすると、

$ base64url decode ewogImlzcy{中略}LmpwZyIKfQ

次の JSON が得られます。

{
 "iss": "http://server.example.com",
 "sub": "248289761001",
 "aud": "s6BhdRkqt3",
 "nonce": "n-0S6_WzA2Mj",
 "exp": 1311281970,
 "iat": 1311280970,
 "name": "Jane Doe",
 "given_name": "Jane",
 "family_name": "Doe",
 "gender": "female",
 "birthdate": "0000-10-31",
 "email": "janedoe@example.com",
 "picture": "http://example.com/janedoe/me.jpg"
}

この内容については、のちほど詳述します。なお、この例では、ペイロード部分の元データが上記のように JSON となっていますが、RFC 7515 自体は JSON を要求しておらず、任意のデータ (an arbitrary sequence of octets) で構わないとしています。

3.3. JWS 署名のデコード

ヘッダーに含まれている alg パラメーターの値は、署名のアルゴリズムを示しています。alg の値として有効な値は、RFC 7515 ではなく、別仕様書 RFC 7518 (JSON Web Algorithms (JWA)) の「3.1. "alg" (Algorithm) Header Parameter Values for JWS」に列挙されています。

alg の値 アルゴリズム
HS256 HMAC using SHA-256
HS384 HMAC using SHA-384
HS512 HMAC using SHA-512
RS256 RSASSA-PKCS1-v1_5 using SHA-256
RS384 RSASSA-PKCS1-v1_5 using SHA-384
RS512 RSASSA-PKCS1-v1_5 using SHA-512
ES256 ECDSA using P-256 and SHA-256
ES384 ECDSA using P-384 and SHA-384
ES512 ECDSA using P-521 and SHA-512
PS256 RSASSA-PSS using SHA-256 and MGF1 with SHA-256
PS384 RSASSA-PSS using SHA-384 and MGF1 with SHA-384
PS512 RSASSA-PSS using SHA-512 and MGF1 with SHA-512
none No digital signature or MAC performed

「3.1. JWS ヘッダーのデコード」の結果、alg の値は RS256 だということが分かっています。「3.1. "alg" (Algorithm) Header Parameter Values for JWS」の表と照合すると、RS256 は「RSASSA-PKCS1-v1_5 using SHA-256」を示していますので、RSA アルゴリズムによる署名ということが分かります。

RSA の署名はバイナリデータなので、base64url でデコードするとバイナリデータが得られます (ヘッダーやペイロードのように JSON は得られません)。署名部分を base64url デコードし、続けて 10 進数表記すると、次のようになります。(見ても意味は分かりませんが。)

$ base64url decode rHQjEmBqn9{中略}l_lxXjQEvQ | od -tu1 -An
 239 191 189 116  35  18  96 106 239 191 189 239 191 189 107 123
  67 239 191 189 239 191 189  70  13 239 191 189 239 191 189  64
 239 191 189  68  42 239 191 189 106 239 191 189 199 129 108  15
  77  35 239 191 189  80 116  75  41 239 191 189  55  58  96 239
 191 189 239 191 189 239 191 189  14  57 239 191 189 239 191 189
 239 191 189   7 239 191 189 239 191 189 122  10  41  79 114 239
 191 189 239 191 189  88 119  34 239 191 189  29 239 191 189   2
  60 203 188  51  64  69 239 191 189 125 239 191 189  58   9 239
 191 189 239 191 189  60 209 144 239 191 189 111   6  60 216 144
 218 184  37 239 191 189  45  84 125  59 239 191 189 239 191 189
 239 191 189  13 239 191 189 239 191 189  78 239 191 189  51 105
 239 191 189 112   0  32  52  37  48 239 191 189  41  58  74  58
  18  81 239 191 189  92 127 227 185 151  40   8 200 177  13 239
 191 189 239 191 189  54 239 191 189 239 191 189  45 239 191 189
   6 239 191 189 239 191 189  23 122 239 191 189 126  81 100 239
 191 189  52  76 239 191 189 239 191 189  98 239 191 189  57 239
 191 189 104  61  19  28  92 239 191 189 239 191 189 239 191 189
 239 191 189 106 239 191 189 239 191 189  18 239 191 189 239 191
 189 239 191 189 239 191 189 239 191 189  85  34 239 191 189 239
 191 189 122 239 191 189 239 191 189   6  24 222 131  31 239 191
 189 239 191 189 239 191 189 125 239 191 189 115  25   8 239 191
 189 239 191 189 239 191 189  15 239 191 189 239 191 189 239 191
 189  34 239 191 189 239 191 189 102 111   9 239 191 189  96  28
 113 119  32   2 239 191 189 117  81   3  73  74   6 239 191 189
 127 115 239 191 189 239 191 189 239 191 189 121 102  13  94 239
 191 189 239 191 189  58  49 239 191 189 239 191 189 239 191 189
 239 191 189  67  53   7 239 191 189 239 191 189  16  65  99  98
  14 239 191 189 239 191 189 239 191 189 239 191 189 113  94  52
   4 239 191 189  10

3.4. JWS 署名の対象

そもそも何に対する署名なのか、という話が最後になってしまいました。署名処理に対する入力データは、「ヘッダー.ペイロード」です。これまでの例でいうと、次のデータが署名に対する入力データとなります。

eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlz
cyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4
Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAi
bi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEz
MTEyODA5NzAsCiAibmFtZSI6ICJKYW5lIERvZSIsCiAiZ2l2ZW5fbmFtZSI6
ICJKYW5lIiwKICJmYW1pbHlfbmFtZSI6ICJEb2UiLAogImdlbmRlciI6ICJm
ZW1hbGUiLAogImJpcnRoZGF0ZSI6ICIwMDAwLTEwLTMxIiwKICJlbWFpbCI6
ICJqYW5lZG9lQGV4YW1wbGUuY29tIiwKICJwaWN0dXJlIjogImh0dHA6Ly9l
eGFtcGxlLmNvbS9qYW5lZG9lL21lLmpwZyIKfQ

3.5. JWS デコードのまとめ

ID トークンは Compact Serialization 形式で表現されているため、下記に再掲するように、そのままでは表現している情報の内容が分かりません。

eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlz
cyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4
Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAi
bi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEz
MTEyODA5NzAsCiAibmFtZSI6ICJKYW5lIERvZSIsCiAiZ2l2ZW5fbmFtZSI6
ICJKYW5lIiwKICJmYW1pbHlfbmFtZSI6ICJEb2UiLAogImdlbmRlciI6ICJm
ZW1hbGUiLAogImJpcnRoZGF0ZSI6ICIwMDAwLTEwLTMxIiwKICJlbWFpbCI6
ICJqYW5lZG9lQGV4YW1wbGUuY29tIiwKICJwaWN0dXJlIjogImh0dHA6Ly9l
eGFtcGxlLmNvbS9qYW5lZG9lL21lLmpwZyIKfQ.rHQjEmBqn9Jre0OLykYNn
spA10Qql2rvx4FsD00jwlB0Sym4NzpgvPKsDjn_wMkHxcp6CilPcoKrWHcip
R2iAjzLvDNAReF97zoJqq880ZD1bwY82JDauCXELVR9O6_B0w3K-E7yM2mac
AAgNCUwtik6SjoSUZRcf-O5lygIyLENx882p6MtmwaL1hd6qn5RZOQ0TLrOY
u0532g9Exxcm-ChymrB4xLykpDj3lUivJt63eEGGN6DH5K6o33TcxkIjNrCD
4XB1CKKumZvCedgHHF3IAK4dVEDSUoGlH9z4pP_eWYNXvqQOjGs-rDaQzUHl
6cQQWNiDpWOl_lxXjQEvQ

しかし、RFC 7515 の情報をもとに base64url デコードしていくことにより、上記の無味乾燥な文字列は、次の情報を持っていることが分かりました。

{"kid":"1e9gdk7","alg":"RS256"}
{
 "iss": "http://server.example.com",
 "sub": "248289761001",
 "aud": "s6BhdRkqt3",
 "nonce": "n-0S6_WzA2Mj",
 "exp": 1311281970,
 "iat": 1311280970,
 "name": "Jane Doe",
 "given_name": "Jane",
 "family_name": "Doe",
 "gender": "female",
 "birthdate": "0000-10-31",
 "email": "janedoe@example.com",
 "picture": "http://example.com/janedoe/me.jpg"
}
署名

ここまでの話で、ID トークンとは何かを具体的にイメージできるようになったのではないでしょうか。

3.6. Unsecured JWS

せっかくですので、Unsecured JWS を紹介します。Unsecured JWS は、署名の無い JWS です。下記は、RFC 7515A.5. Example Unsecured JWS から抜粋した Unsecured JWS の例です。

eyJhbGciOiJub25lIn0
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt
cGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.

Unsecured JWS は次のような形式となっており、JWS 内の三番目のフィールドが空になっています。

ヘッダー.ペイロード.

署名が無いということは、署名アルゴリズムも無いので、JWS ヘッダーの alg には none を指定することになっています。上記例の JWS ヘッダー部分である eyJhbGciOiJub25lIn0 を base64url でデコードすると、

$ base64url decode eyJhbGciOiJub25lIn0

次のような JSON が得られます。

{"alg":"none"}

確かに algnone になっていることが分かります。

4. JSON Web Encryption (JWE)

さて、ID トークンの形式には、これまで説明してきた「ヘッダー.ペイロード.署名」の他に、次のように 5 つのフィールドを持つ形式もあります。

ヘッダー.キー.初期ベクター.暗号文.認証タグ

この形式は、RFC 7516 (JSON Web Encryption (JWE)) の「7.1. JWE Compact Serialization」で定義されている「JSON Web Encryption (JWE) の Compact Serialization 形式」です。具体的には次のように定義されています。 (RFC 7516JSON Serialization 形式も定義していますが、ここでは扱いません。)

BASE64URL(UTF8(JWE Protected Header)) || '.' ||
BASE64URL(JWE Encrypted Key) || '.' ||
BASE64URL(JWE Initialization Vector) || '.' ||
BASE64URL(JWE Ciphertext) || '.' ||
BASE64URL(JWE Authentication Tag)

この形式は、ID トークンを暗号化したいときに利用されます。

暗号化の対象となる平文は、暗号化されたあと、4 番目のフィールドに置かれます。RFC 7516 自体は平文の内容は何でもよいとしていますが、ID トークンの文脈では、平文は「ヘッダー.ペイロード.署名」となります。つまり、JWE の中に JWS が入っている形になります。

4.1. 二段階の暗号処理

7.1. JWE Compact Serialization」によれば、Compact Serialization 形式の 2 番目のフィールドは「JWE Encrypted Key」となっています。Encryption ではなく Encrypted となっているので、日本語に訳すとすれば、「暗号キー」ではなく「暗号化されたキー」となります。このような表現になっているのには理由があります。

JWE に限った話ではありませんが、特に非対称鍵系暗号を用いる場合、暗号が次のような二段階で行われることがあります。

  1. 対称鍵系暗号の共有鍵を用いて、平文を暗号化する。
  2. 上記の暗号処理に用いた共有鍵自体を、別の非対称鍵系暗号の鍵を用いて暗号化する。

二段階にすることで、平文の暗号化に対称鍵系暗号を用いて暗号化・復号化にかかる時間を抑えつつ、非対称鍵系暗号の利点を得ることができます。なお、ここで用いる対称鍵系暗号の共有鍵は、事前に共有しておく必要はなく、暗号処理をおこなう側が暗号処理実行時にランダムに生成することができます。というのは、ランダムに生成しても、その共有鍵を非対称鍵系暗号の鍵で暗号化して相手側に渡せば、相手側は、その暗号化された共有鍵を非対称鍵系暗号の鍵で復号化することで、当該共有鍵を取り出せるからです。

下図は、二段階の暗号処理を図にしたものです。JWE Encrypted Key は図中の「暗号化された共有鍵」に相当します。

二段階の暗号処理.png

4.2. 暗号アルゴリズム

JWE では二段階の暗号処理をおこなっています。それぞれに暗号アルゴリズムがありますので、JWE には二つの暗号アルゴリズムが関わることになります。

平文を暗号文へと暗号化するときに使用するアルゴリズムは、RFC 7518 (JSON Web Algorithms) の「5.1. "enc" (Encryption Algorithm) Header Parameter Values for JWE」に次のようにリストアップされています。

enc の値 アルゴリズム
A128CBC-HS256 AES using 128-bit IV and SHA-256
A192CBC-HS384 AES using 192-bit IV and SHA-384
A256CBC-HS512 AES using 256-bit IV and SHA-512
A128GCM AES GCM using 128-bit key
A192GCM AES GCM using 192-bit key
A256GCM AES GCM using 256-bit key

一方、共有鍵を暗号化するときに使用するアルゴリズムは「4.1. "alg" (Algorithm) Header Parameter Values for JWE」に次のようにリストアップされています。

alg の値 アルゴリズム
RSA1_5 RSAES-PKCS1-v1_5
RSA-OAEP RSAES OAEP using default parameters
RSA-OAEP-256 RSAES OAEP using SHA-256 and MGF1 with SHA-256
A128KW AES Key Wrap with default initial value using 128-bit key
A192KW AES Key Wrap with default initial value using 192-bit key
A256KW AES Key Wrap with default initial value using 256-bit key
dir Direct use of a shared symmetric key as the CEK
ECDH-ES Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF
ECDH-ES+A128KW ECDH-ES using Concat KDF and CEK wrapped with "A128KW"
ECDH-ES+A192KW ECDH-ES using Concat KDF and CEK wrapped with "A192KW"
ECDH-ES+A256KW ECDH-ES using Concat KDF and CEK wrapped with "A256KW"
A128GCMKW Key wrapping with AES GCM using 128-bit key
A192GCMKW Key wrapping with AES GCM using 192-bit key
A256GCMKW Key wrapping with AES GCM using 256-bit key
PBES2-HS256+A128KW PBES2 with HMAC SHA-256 and "A128KW" wrapping
PBES2-HS384+A192KW PBES2 with HMAC SHA-384 and "A192KW" wrapping
PBES2-HS512+A256KW PBES2 with HMAC SHA-512 and "A256KW" wrapping

4.3. 共有鍵を暗号化するアルゴリズム dir

前節に挙げた「共有鍵を暗号化するアルゴリズム」の識別子のうち、dir は特殊です。というのは、RFC 7518 の「4.5. Direct Encryption with a Shared Symmetric Key」に次のように書かれているように、dir は、二段階暗号方式ではなく直接共有鍵暗号処理をおこなう方式だからです。

This section defines the specifics of directly performing symmetric key encryption without performing a key wrapping step.

RFC 7518 では、共有鍵の決め方について特にルールは定めていません。一方、OpenID Connect Core 1.0 の「10.2. Encryption」では、対称鍵系暗号使用時のキーに対する要求事項を次のように定めています。

Symmetric Encryption
The symmetric encryption key is derived from the client_secret value by using a left truncated SHA-2 hash of the octets of the UTF-8 representation of the client_secret. For keys of 256 or fewer bits, SHA-256 is used; for keys of 257-384 bits, SHA-384 is used; for keys of 385-512 bits, SHA-512 is used. The hash value MUST be left truncated to the appropriate bit length for the AES key wrapping or direct encryption algorithm used, for instance, truncating the SHA-256 hash to 128 bits for A128KW. If a symmetric key with greater than 512 bits is needed, a different method of deriving the key from the client_secret would have to be defined by an extension. Symmetric encryption MUST NOT be used by public (non-confidential) Clients because of their inability to keep secrets.

つまり、「クライアントシークレットを UTF-8 で表現し、SHA-2 系のアルゴリズムでハッシュ値を求め、適切な長さで切り詰めたものを、キーとして用いる」というルールを定めています。

4.4. JWE の例

次のものは、RFC 7516 の「A.1.7. Complete Representation」から抜粋した JWE の例です。途中改行が含まれていますが、見やすいように入れているだけであり、実際の JWE には改行は含まれません。なお、この JWE は ID トークンではありません。暗号化前の平文は「The true sign of intelligence is not knowledge but imagination.」という文字列です。

eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.
OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe
ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb
Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV
mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8
1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi
6UklfCpIMfIjf7iGdXKHzg.
48V1_ALb6US04U3b.
5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6ji
SdiwkIr3ajwQzaBtQD_A.
XFBoMYUZodetZdvTiFvSkQ

4.5. JWE ヘッダーのデコード

JWE 内のピリオドで区切られた 5 つのフィールドのうち、一番目のフィールドは JWE ヘッダーです。base64url でエンコードされてはいますが暗号化はされていないので、前節で挙げた JWE の例のヘッダー部分を base64url でデコードしてみましょう。

$ base64url decode eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ

結果、次のような JSON が得られます。

{"alg":"RSA-OAEP","enc":"A256GCM"}

ここで、alg は共有鍵を暗号化する際のアルゴリズム、enc は平文を暗号化する際のアルゴリズムを示しています。それぞれの有効な値は、「4.2. 暗号アルゴリズム」に挙げたとおりです。

algenc を含め、JWE ヘッダーで指定可能なパラメーターは、RFC 7516 の「4.1. Registered Header Parameter Names」に列挙されています。

パラメーター名 説明
alg 共有鍵を暗号化するアルゴリズム
enc 平文を暗号化するアルゴリズム
zip 圧縮アルゴリズム
jku JWK セット URL
jwk JSON Web キー
kid キー ID
x5u X.509 URL
x5c X.509 証明書チェーン
x5t X.509 証明書 SHA-1 Thumbprint
x5t#S256 X.509 証明書 SHA-256 Thumbprint
typ JWE 自身のメディアタイプ
cty ペイロードのメディアタイプ
crit 必須パラメーター群指定

4.6. その他の JWE フィールド

説明は割愛します。詳細は RFC 7516 を参照してください。

5. JSON Web Key (JWK)

JWS の署名や JWE の暗号に非対称鍵系アルゴリズムが使われている場合、そのような JWS や JWE を受け取った側は、署名を検証したり暗号文を復号化するために、署名・暗号で使用された鍵とペアとなる鍵が必要となります。これは、何らかの形で鍵に関する情報を表現し、JWS や JWE を受け取った側に渡す必要があるということです。

そこで登場するのが、RFC 7517 (JSON Web Key (JWK)) です。この仕様により、暗号鍵の情報を JSON 形式で表現することができるようになりました。

5.?. TODO

後日追記します。 動画『OAuth & OIDC 入門編』の 1:06:26 〜 をご覧ください。

6. JSON Web Token (JWT)

JWS と JWE の説明が済んでいるので、JWT の説明をすることができます。JWT はジョットと読みます。この読み方は、RFC 7519Introduction に書かれています。

The suggested pronunciation of JWT is the same as the English word "jot".

JWT の仕様は RFC 7519 (JSON Web Token (JWT)) で定められています。簡単に言うと、「JWT とは、JSON 形式で表現されたクレーム (claim) の集合を、JWS もしくは JWE に埋め込んだもの」です。

「JSON 形式で表現されたクレームの集合」がどのように JWS や JWE に埋め込まれるのかを先に説明し、その後、クレーム集合の部分を説明します。

6.1. JWS 形式の JWT

JWS 形式の JWT の場合、JSON 形式で表現されたクレームの集合は、BASE64URL エンコード後、JWS の二番目のフィールドに埋め込まれます。

jws-jwt.png

6.2. JWE 形式の JWT

JWE 形式の JWT の場合は、クレームの集合は平文として用いられ、暗号化と BASE64URL エンコードを経て、JWE の四番目のフィールドに埋め込まれます。

jwe-jwt.png

6.3. Nested JWT

署名もしたいし暗号化もしたい、という場合、JWS を JWE でくるむか、もしくはその逆で、JWE を JWS でくるむか、のどちらかをおこなえば実現できます。このように、JWE/JWS の中に JWS/JWE がネストしている形式の JWT は、Nested JWT と呼ばれます。次の図は、JWS が JWE に含まれているパターンの Nested JWT を示しています。

nested-jwt.png

なお、ID トークンの文脈では、署名は必須で、暗号化をする場合は「signed then encrypted」(署名後に暗号化) という順番が MUST とされています。

(OpenID Connect Core 1.0, 2. ID Token より抜粋)

ID Tokens MUST be signed using JWS and optionally both signed and then encrypted using JWS and JWE respectively, thereby providing authentication, integrity, non-repudiation, and optionally, confidentiality, per Section 16.14. If the ID Token is encrypted, it MUST be signed then encrypted, with the result being a Nested JWT, as defined in JWT.

そのため、暗号化された ID トークンは、ちょうど上記の図と同じ「JWS が JWE に含まれている形の Nested JWT」になります。

説明が前後してしまいましたが、ID トークンは JWT の一種です

6.4. JWT クレーム

JWT はクレームの集合なわけですが、形式的には、クレームは JSON オブジェクト内のキー・バリューのペアとして表現されています。つまり、クレーム集合の部分は次のような形式になっています。

{
    "クレーム名": クレーム値,
    "クレーム名": クレーム値,
    ......
}

次に挙げる JSON は、RFC 7519 の「3.1. Example JWT」から抜粋したクレーム集合部分の例です。

{"iss":"joe",
 "exp":1300819380,
 "http://example.com/is_root":true}

RFC 7519 では、「4.1. Registered Claim Names」において幾つかクレーム名を定義しています。

クレーム名 説明
iss Issuer クレーム
sub Subject クレーム
aud Audience クレーム
exp Expiration Time クレーム
nbf Not Before クレーム
iat Issued At クレーム
jti JWT ID クレーム

3.1. Example JWT」から抜粋した例にもあるように、クレーム名にはこれら以外のものを用いても構いませんが、名前衝突を避けるなどの配慮が必要です。

JWT に含めるべきクレームとして必須のものは、実はありません。「4. JWT Claims」の第二段落には次のように書かれており、JWT がどのようなクレーム群を含むべきかは、JWT を利用する個々のアプリケーションの定める範疇としています。

The set of claims that a JWT must contain to be considered valid is context dependent and is outside the scope of this specification. Specific applications of JWTs will require implementations to understand and process some claims in particular ways. However, in the absence of such requirements, all claims that are not understood by implementations MUST be ignored.

RFC 7519 の立場から見ると、OpenID Connect Core 1.0 が定める ID トークンは、JWT 応用例の一つとなります。勘の良い方はすぐに予想されたかもしれませんが、ID トークンの仕様では、RFC 7519 で定義されているクレームの幾つかが必須のクレームとされています。具体的には、iss, sub, aud, exp, iat は必須とされています。

7. ID トークン

JWT の説明が済んだので、いよいよ ID トークンの説明に入ります。

既に述べてはいますが、改めまして、ID トークンは JWT の一種です。ここで、OpenID Connect Core 1.0 の「2. ID Token」の第一段落を見てみましょう。

The primary extension that OpenID Connect makes to OAuth 2.0 to enable End-Users to be Authenticated is the ID Token data structure. The ID Token is a security token that contains Claims about the Authentication of an End-User by an Authorization Server when using a Client, and potentially other requested Claims. The ID Token is represented as a JSON Web Token (JWT).

これによれば、OpenID Connect が OAuth 2.0 に対しておこなった主要な拡張が、まさに ID トークンというデータ構造だということが分かります。ID トークンが JWT として表現されることも、この段落の末尾に明記されています。

ID トークンには、エンドユーザー認証に関するクレーム群 (Claims about the Authentication) とその他のクレーム群が含まれます。ID トークンで利用するクレーム群のうち、主要なものは OpenID Connect Core 1.0 の「2. ID Token」と「5.1. Standard Claims」で説明されています。また、細かい話ですが、「3.3.2.11. ID Token」では、at_hash, c_hash というクレームも定義されています。

それでは、ID トークンのクレームを一つ一つ見ていくことにします。

7.1. iss クレーム

仕様 説明
RFC 7519 The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The "iss" value is a case-sensitive string containing a StringOrURI value. Use of this claim is OPTIONAL.
OIDC Core REQUIRED. Issuer Identifier for the Issuer of the response. The iss value is a case sensitive URL using the https scheme that contains scheme, host, and optionally, port number and path components and no query or fragment components.

iss クレームは、JWT の発行者 (issuer) を識別するための識別子です。iss クレームの値は、RFC 7519 では文字列か URI (StringOrURI) とされていますが、OpenID Connect Core 1.0 のほうの定義では、より条件が多くなっており、「https:// で始まる URL (ただしクエリー部とフラグメント部は含まない)」とされています。

次に挙げる例は、iss クレームの値として有効な URL です。

https://example.com

ID トークン発行サーバーは、他者の識別子との衝突を避けるため、iss クレームの値を自分の管理下にあるドメイン名の URL とすべきです。これに加えて、もしも、OpenID Connect Discovery 1.0 という仕様をサポートするのであれば、「{issクレームの値}/.well-known/openid-configuration」という URL でリクエストを受け付ける必要があることを念頭に置いて iss クレームの値を決める必要があります。

例えば、もし上記の例のように iss クレームの値を https://example.com とし、かつ、OpenID Connect Discovery 1.0 をサポートするのであれば、次の URL で HTTP GET リクエストを受け付けて適切な JSON データを返せなければなりません。

https://example.com/.well-known/openid-configuration

参考までに、Google 社による実装はこちらになります。
        https://accounts.google.com/.well-known/openid-configuration

7.2. sub クレーム

仕様 説明
RFC 7519 The "sub" (subject) claim identifies the principal that is the subject of the JWT. The claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The "sub" value is a case-sensitive string containing a StringOrURI value. Use of this claim is OPTIONAL.
OIDC Core REQUIRED. Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client, e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. It MUST NOT exceed 255 ASCII characters in length. The sub value is a case sensitive string.

sub クレームは、ユーザーの一意識別子を表します。値の形式は、RFC 7519 では文字列か URI (StringOrURI) とされており、OpenID Connect Core 1.0 では 255 文字以内の ASCII 文字列とされています。

突然、「ユーザーの一意識別子」というものがでてきました。これまで説明はしていませんでしたが、ID トークンはユーザーを認証した結果発行されるものなので、どのユーザーが認証されたのかの情報が含まれることになります。その「どのユーザーが認証されたのか」という情報にあたる部分が、sub クレームです。sub クレームの値は、ID トークン発行者のシステム内において、ユーザーを一意に特定することが可能な識別子です。

ユーザーの一意識別子は、一般的には、データベース内のユーザーテーブルのプライマリーキーやそれに準ずるものです。特に気にしないのであれば、プライマリーキーの値をそのまま sub の値として用いてもかまいません。気にするのであれば、何らかのロジックを用いて元々の値を変換してから sub クレームの値として用いることになります。場合によっては、ID トークンの発行を依頼したプログラムの違いによって、同じプライマリーキーから異なる sub クレーム値を生成してもかまいません。これについては、OpenID Connect Core 1.0 の「8. Subject Identifier Types」に言及があります。

7.3. aud クレーム

仕様 説明
RFC 7519 The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. In the general case, the "aud" value is an array of case-sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL.
OIDC Core REQUIRED. Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. It MAY also contain identifiers for other audiences. In the general case, the aud value is an array of case sensitive strings. In the common special case when there is one audience, the aud value MAY be a single case sensitive string.

aud クレームは、当該 JWT が、誰を対象として発行されたのかを示すものです。別の言い方をすると、JWT の受け取り手が誰であるべきかを示しています。ID トークンの場合、この aud クレームの値は、ID トークンの発行を依頼したクライアントアプリケーションのクライアント ID となります。

7.4. exp クレーム

仕様 説明
RFC 7519 The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the "exp" claim requires that the current date/time MUST be before the expiration date/time listed in the "exp" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL.
OIDC Core REQUIRED. Expiration time on or after which the ID Token MUST NOT be accepted for processing. The processing of this parameter requires that the current date/time MUST be before the expiration date/time listed in the value. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. See RFC 3339 for details regarding date/times in general and UTC in particular.

exp クレームは JWT の有効期限を示しています。ID トークンでは、Unix エポック (1970 年 1 月 1 日 (世界標準時)) からの経過秒数となっています。単位はミリ秒ではなく秒なので、ID トークンを生成もしくはパースするプログラムを書く際には注意が必要です。

7.5. iat クレーム

仕様 説明
RFC 7519 The "iat" (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL.
OIDC Core REQUIRED. Time at which the JWT was issued. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.

iat クレームは JWT が発行された日時を示しています。exp クレームと同様、Unix エポックからの経過秒数で表現されています。

7.6. auth_time クレーム

仕様 説明
OIDC Core Time when the End-User authentication occurred. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. When a max_age request is made or when auth_time is requested as an Essential Claim, then this Claim is REQUIRED; otherwise, its inclusion is OPTIONAL. (The auth_time Claim semantically corresponds to the OpenID 2.0 PAPE auth_time response parameter.)

auth_time クレームは、ユーザーが認証された時刻を Unix エポックからの経過秒数で表現しています。このクレームは RFC 7519 では定義されていません。

ユーザー認証は、ID トークンの発行と同じタイミングで実行されるとは限りません。認証後しばらくたってから ID トークン発行依頼が届いたものの、「既にログイン状態だから認証が終わっているものとみなす」ということで、認証処理をスキップして ID トークンが発行されることがあります。このようなケースの場合、ユーザーが認証された時刻 (auth_time) と ID トークンが発行された時刻 (iat) には、差がでてきます。

auth_time クレームを ID トークンに含めるかどうかは、基本的には任意です。しかし、ID トークン発行依頼が「認証後許容経過時間」を指定する max_age リクエストパラメーターを伴っていたり (「OAuth & OpenID Connect 関連仕様まとめ」の「17. 認証後許容経過時間」参照)、ID トークン発行依頼を出したクライアントアプリケーションのメタ情報設定が「ID トークン発行時に auth_time クレームを必須とする」となっていたりすると (OpenID Connect Dynamic Client Registration 1.0 の「2. Client Metadata」の require_auth_time 参照)、auth_time クレームは必須となります。

7.7. nonce クレーム

仕様 説明
OIDC Core String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the value of the nonce parameter sent in the Authentication Request. If present in the Authentication Request, Authorization Servers MUST include a nonce Claim in the ID Token with the Claim Value being the nonce value sent in the Authentication Request. Authorization Servers SHOULD perform no other processing on nonce values used. The nonce value is a case sensitive string.

主にリプレイアタックを防ぐ目的で、ID トークン発行依頼に nonce というリクエストパラメーターが付いてくることがあります。このとき、ID トークン発行側は、受け取った nonce の値をそのまま ID トークンに埋め込みます。

7.8. acr クレーム

仕様 説明
OIDC Core OPTIONAL. Authentication Context Class Reference. String specifying an Authentication Context Class Reference value that identifies the Authentication Context Class that the authentication performed satisfied. The value "0" indicates the End-User authentication did not meet the requirements of ISO/IEC 29115 level 1. Authentication using a long-lived browser cookie, for instance, is one example where the use of "level 0" is appropriate. Authentications with level 0 SHOULD NOT be used to authorize access to any resource of any monetary value. (This corresponds to the OpenID 2.0 OpenID 2.0 PAPE nist_auth_level 0.) An absolute URI or an RFC 6711 registered name SHOULD be used as the acr value; registered names MUST NOT be used with a different meaning than that which is registered. Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific. The acr value is a case sensitive string.

acr クレームは、ユーザー認証が満たした認証コンテキストのクラスを示しています。例えば、パスワードによる認証という基準を満たしたとか、X.509 での認証という基準を満たした、というような情報です。認証コンテキストに関連する仕様についは、「OAuth & OpenID Connect 関連仕様まとめ」の「16. 認証コンテキストクラス」を参照してください。

7.9. amr クレーム

仕様 説明
OIDC Core OPTIONAL. Authentication Methods References. JSON array of strings that are identifiers for authentication methods used in the authentication. For instance, values might indicate that both password and OTP authentication methods were used. The definition of particular values to be used in the amr Claim is beyond the scope of this specification. Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific. The amr value is an array of case sensitive strings.

amr クレームは、認証手法を示しています。その具体的な値については、(少なくとも) OpenID Connect Core 1.0 の仕様の範囲外とされており、amr クレームの利用者が規則を決めて運用すべきもの、という扱いになっています。

7.10. azp クレーム

仕様 説明
OIDC Core OPTIONAL. Authorized party - the party to which the ID Token was issued. If present, it MUST contain the OAuth 2.0 Client ID of this party. This Claim is only needed when the ID Token has a single audience value and that audience is different than the authorized party. It MAY be included even when the authorized party is the same as the sole audience. The azp value is a case sensitive string containing a StringOrURI value.

azp クレームは、認可された対象者を示しています。認可されたクライアントアプリケーションのクライアント ID の値です。このクレームは、ID トークンの発行を依頼したクライアントアプリケーションと認可されたクライアントアプリケーションが異なる場合、必要となります。しかし、発行依頼したクライアントと認可されたクライアントが異なるというのは、あまりきかないユースケースです。

7.11. ユーザー属性クレーム群

ユーザーの属性に関するクレームは、「5.1. Standard Claims」でまとめて定義されています。

クレーム名 説明
sub ユーザーの一意識別子
name フルネーム
given_name
family_name
middle_name ミドルネーム
nickname ニックネーム
preferred_username 好みのユーザー名
profile プロフィールページの URL
picture プロフィール画像の URL
website Web サイトやブログの URL
email メールアドレス
email_verified メールアドレスが検証済みか否かの真偽値
gender 性別。femalemale が定義済み。
birthdate 誕生日。YYYY-MM-DD という書式。
zoneinfo タイムゾーン。Europe/Paris など。
locale ロケール。en-US など。
phone_number 電話番号
phone_number_verified 電話番号が検証済みか否かの真偽値
address 住所。書式は「5.1.1. Address Claim」に記載。
updated_at 情報最終更新日。Unix エポックからの経過秒数。

これらのクレーム群を ID トークンに含める際、クレームによっては多言語化可能なものがあります。例えば、family_name クレームの場合、英語なら「Kawasaki」、日本語なら「川崎」や「カワサキ」というように、値が幾つか考えられます。このような多言語化への対応のため、クレーム名の後ろに「#言語タグ」をつけるという仕組みがあり、「5.2. Claims Languages and Scripts」で詳細に説明されています。

次の JSON の断片は、言語タグ付きの family_name クレームの例です。

{
    "family_name": "Kawasaki",
    "family_name#ja-Hani-JP": "川崎",
    "family_name#ja-Kana-JP": "カワサキ",

言語タグそのものの詳細については RFC 5646 (Tags for Identifying Languages) を参照してください。

7.12. ハイブリッドフロー関連のクレーム群

詳細は後編で説明する予定ですが、ID トークンと同時に、アクセストークンや認可コードというものが一緒に発行される場合があります。もしこれらが同時に発行される場合、ID トークンに幾つかクレームが追加されます。「3.3.2.11. ID Token」に詳細説明があります。

クレーム名 説明
at_hash Access Token hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the access_token value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash the access_token value with SHA-256, then take the left-most 128 bits and base64url encode them. The at_hash value is a case sensitive string. If the ID Token is issued from the Authorization Endpoint with an access_token value, which is the case for the response_type value code id_token token, this is REQUIRED; otherwise, its inclusion is OPTIONAL.
c_hash Code hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the code value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is HS512, hash the code value with SHA-512, then take the left-most 256 bits and base64url encode them. The c_hash value is a case sensitive string. If the ID Token is issued from the Authorization Endpoint with a code, which is the case for the response_type values code id_token and code id_token token, this is REQUIRED; otherwise, its inclusion is OPTIONAL.

ID トークンと同時にアクセストークンも発行される場合、at_hash というクレームが追加されます。at_hash の値は、アクセストークンのハッシュをとり、結果として得られたハッシュ値の左半分のビット群を base64url でエンコードしたものです。同様に、認可コードが同時に発行される場合は、c_hash というクレームが追加されます。

加えて、nonce クレームが必須となります。

まとめ

「OpenID Connect は『ID トークン』を発行するための仕様である」という割り切り方のもと、ID トークンとは具体的にどんなものであるかを理解することに努めました。

ID トークンは、形式は JWT (RFC 7519)、意味内容はクレームの集合、ということが分かりました。JWT という形式なので、署名や暗号化が可能であり、認証の事実やユーザー属性情報のやりとりが、より安全に行えるようになっています。


こちらも併せてご覧いただければと思います。


追記(2020-03-20)
この記事の内容を含む、筆者本人による『OAuth & OIDC 入門編』解説動画を公開しました!

1427
1103
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1427
1103