OAuth 2.0 の勉強のために認可コードグラントに対応した簡易認可サーバをExpress.jsで実装してみました。
エンドポイント
下記のエンドポイントを実装しています。
- 認可エンドポイント
- トークンエンドポイント
トークンエンドポイントでは、リフレッシュトークンの発行にも対応させました。
const authzServer = {
authorizationEndpoint: serverAddress + "/authorize",
tokenEndpoint: serverAddress + "/token",
responseType: ["code", "token"],
};
クライアント
クライアント情報は静的に登録しています。
const clients = [
{
client_id: "client_id",
client_secret: "client_secret",
redirect_uris: [clientAddress + "/callback"],
scope: ["foo", "bar"],
},
];
余力があれば、動的クライアント登録(RFC7592)にも対応させたいと思っています。
シーケンス図
シーケンスはこんな感じ。
それぞれのエンドポイントで何をやっているか簡単に説明していきます。
認可エンドポイント
認可コードを発行するエンドポイントです。
適切なクライアントからの認可リクエストを受付け、このクライアントに権限を委譲してよいかユーザーへ確認を行います。
ユーザーからクライアントへの権限委譲の許可を得られたら、その証として、認可コードを認可レスポンスとしてクライアントへ返します。
認可リクエスト
GET http://localhost:3000/authorize?client_id=client_id&redirect_url=http://localhost:9000/callback&response_type=code&scope=foo%20bar&state=201
認可エンドポイントでは、認可リクエストパラメータのチェックを行います。
すべてのチェックが正常に行われれば、認可ページをブラウザに返します。
処理順 | パラメータ | 必須 | チェック内容 | エラーレスポンス |
---|---|---|---|---|
1 | client_id | ◯ | 登録されているクライアントIDと一致すること | 認可サーバのページに「Unknown client」を表示 |
2 | redirect_url | ー | 指定されている場合、クライアント情報に設定されているリダイレクトURLと一致すること | 認可サーバのページに「Invalid redirect URI」を表示 |
3 | response_type | ◯ | 認可サーバで対応している種別(code のみ)と一致すること |
リダイレクトURLへ「unsupported_response_type」エラーを返す |
4 | scope | ◯(※) | 認可サーバで対応しているscope値とすべて一致すること。 | リダイレクトURLへ「invalid_scope」エラーを返す |
※scope
は仕様では任意だが、今回作成した認可サーバでは、必須扱いとしています。
認可レスポンス
http://localhost:9000/callback?code=uEH2mhRn4Gfjthwh&state=201
認可ページでユーザーが認可を承認した場合、認可レスポンスとして、リダイレクトURLへのリダイレクトを返します。
そのとき、パラメータに発行した認可コードを付与します。
認可レスポンスではcode
パラメータは必須となり、state
パラメータは認可リクエストに含まれていた場合、必須となります。
トークンエンドポイント
アクセストークンおよびリフレッシュトークンを発行するエンドポイントです。
適切なクライアントからトークンリクエストを受付け、認可コードを元にトークンを発行します。
クライアント認証
トークンエンドポイントでは、クライアント認証も行います。
クライアント認証は、リクエストヘッダーやボディに含まれるクライアントIDとシークレットを検証することで行います。
Http Basic認証またはリクエストボディのいずれかの方法でclient_id
、client_secret
を渡せるようになっています。
トークンリクエスト
POST http://localhost:3000/token
トークンエンドポイントでは、トークンリクエストボディのパラメータチェックを行います。
すべてのチェックが正常に行われれば、トークンをクライアントに返します。
処理順 | パラメータ | 必須 | チェック内容 | エラーレスポンス |
---|---|---|---|---|
1 | grant_type | ◯ |
authorization_code またはrefresh_token であるかを確認 |
クライアントへ「unsupported_grant_type」エラーを返す |
2 | code | ◯ | 認可レスポンスで返した認可コードと一致すること | クライアントへ「invalid_grant」エラーを返す |
3 | client_id | ー | 指定されている場合、認可リクエスト時のclient_id と一致すること |
クライアントへ「invalid_grant」エラーを返す |
トークンレスポンス
{
"access_token": "w1boanear7T92toNv7UYfEgMrTnfAigL",
"refresh_token": "uCWgP7ONnbQaCAqrDifJbuLouPTYsygO",
"token_type": "Bearer",
"scope": "foo bar"
}
トークンレスポンスとして、トークン情報以外にも、token_type
やscope
を返します。
token_type
はアクセストークンがBearer(持参人)トークンとして、提示することを期待していることを表しています。
scope
は、アクセストークンがどのスコープで権限委譲されたかを表しています。
最後に
簡単に、今回実装した簡易認可サーバの仕様について説明しました。
OAuth 2.0 を知れば知るほど、わかってたつもりがよくわからなくなったりと、まだまだ完璧に理解した!とまで言えない奥が深い仕様だなーと改めて感じました。
まだまだ改善や拡張の残る状態ではありますが、そこについては今後の余力次第で対応していきたいと思っています。