OAuth 2.0およびOpenID Connectの勉強のため、Google OAuth 2.0 API を使って、Node.jsでGoogle認証を行うクライアントアプリを実装してみました。Googleの認可/Idpサーバを使用し、トークンエンドポイントからアクセストークン
とIDトークン
を取得するところまで実装しています。
この記事では、OAuth 2.0 の認可コードフローに対応したクライアントアプリを実装しながら学んだことを整理しておきます。
シーケンス図
今回実装した、OpenID Connectのクライアントアプリのシーケンス図です。
基本的なOAuth 2.0の「認可コードフロー」になっています。
背景が薄い緑の部分が認可リクエストに関する処理、薄い水色がトークンリクエストに関する処理になります。
学習メモ
OAuth 2.0 クライアント設定
OAuth 2.0 ではクライアントアプリを識別するためのクライアント情報の登録が必要となります。Google OAuth 2.0 APIにおけるクライアント設定はGoogle API Consoleでクライアント情報を設定します。
クライアントを設定するとclient_id
とclient_secret
が払い出されます。この情報は主に認可サーバでのクライアント認証で使用されます。
また、認可サーバからのリダイレクト先として承認するURIをクライアントへ設定する必要があります。今回のリダイレクト先URIはhttp://localhost:8080/callback
としました。
const REDIRECT_URI = "http://localhost:8080/callback"
const CLIENT_ID = "クライアントID"
const CLIENT_SECRET = "シークレットID"
認可リクエスト
認可リクエストは認可エンドポイントへ下記のようなリクエストを送信します。
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
Googleはディスカバリドキュメント(.well-known/openid-configuration
)を公開しているので、この中のauthorization_endpoint
パラメータから認可エンドポイントURIを取得します。
また、認可リクエストはユーザーエージェントを介して送信する必要があるため、認可リクエストURLへのリダイレクトをブラウザに返します。
// ディスカバリドキュメントから認可エンドポイントを取得する。
const resp = await fetch(DISCOVERY_ENDPOINT);
const provider_cfg = await resp.json();
const authorization_endpoint = provider_cfg['authorization_endpoint']
// 認可リクエスト用のパラメータの設定
const params = {
client_id: CLIENT_ID,
response_type: "code",
scope: ["openid email profile"],
redirect_uri: REDIRECT_URI,
prompt: "select_account",
};
const query = new URLSearchParams(params);
const request_url = authorization_endpoint + `?${query}`
// ブラウザにリダイレクトを返す
response.writeHead(302, { 'Location': request_url })
response.end()
リクエストで指定するパラメータについては下記を参考にしました。
パラメータ | 必須 | 設定値例 | 備考 |
---|---|---|---|
client_id | ◯ | "クライアントID" |
クライアント設定で払い出されたクライアント識別子を指定 |
response_type | ◯ | "code" |
認可コードフローの場合は"code" を指定 |
scope | - | "openid" "email" "profile" |
スコープはスペース区切りで指定。値の順序は考慮しない。 |
redirect_uri | - | "http://localhost:8080/callback" |
絶対URIで指定。クライアントに設定した「承認済みリダイレクトURI」と完全一致しなければならない。 |
state | - | "xyz" |
設定が推奨されている。認可リクエストに含めたstate の値と後述の認可レスポンスで返されたstate の値が同一であることを確認することでCSRF対策が行える。 |
認可レスポンス
認可リクエストを送信後、ユーザーの認証+認可操作が正常に完了すると、下記のような認可レスポンスがブラウザに返されます。
HTTP/1.1 302 Found
Location: http://localhost:8080/callback?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz
リダイレクト先としては認可リクエストでredirect_uri
に指定したURIが設定され、クエリパラメータにcode
が含まれていることがわかります。
このcode
が認可サーバから返された認可コードです。認可コードはユーザーの認証・認可が正常に実施されたことを表しており、このcode
の値をトークンエンドポイントのリクエストパラメータに含めることで、アクセストークンと交換することができます。
レスポンスの各パラメータ項目の説明は下記となります。
パラメータ | 必須 | 設定値例 | 備考 |
---|---|---|---|
code | ◯ | SplxlOBeZQQYbYS6WxSbIA |
アクセストークンと交換するための短命なコード。有効期限は最大でも10分が推奨されている。 |
state | - | xyz |
認可リクエストでstate が含まれていた場合、必須。 |
トークンリクエスト
認可レスポンスで受け取った認可コードをトークンエンドポイント送信します。
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
Googleのディスカバリドキュメントのtoken_endpoint
パラメータからトークンエンドポイントURIを取得します。
//リクエストパラメータから認可コードを取得する
const query = url.parse(request.url).query;
const code = querystring.parse(query).code;
// ディスカバリドキュメントからトークンエンドポイントを取得する。
const resp = await fetch(DISCOVERY_ENDPOINT);
const provider_cfg = await resp.json();
const authorization_endpoint = provider_cfg['token_endpoint']
// リクエストボディの設定
const body = new URLSearchParams({
"code": code,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": REDIRECT_URI,
"grant_type": "authorization_code",
});
const resp_token = await fetch(authorization_endpoint, {
method: "POST",
headers:{
"Content-Type":"application/x-www-form-urlencoded",
},
body,
})
const token = await resp_token.json();
リクエストに含める各パラメータの説明は下記を参考にしました。
パラメータ | 必須 | 設定値例 | 備考 |
---|---|---|---|
grant_type | ◯ | "authorization_code" |
認可コードフローであることを表す |
code | ◯ | "xyz" |
認可サーバーから受け取った認可コード |
redirect_uri | - | "http://localhost:8080/callback" |
認可リクエストでredirect_uri が含まれていた場合、必須。 |
client_id | - | "クライアントID" |
クライアント認証済みの場合、指定は必須ではない。 |
トークンレスポンス
トークンリクエストを送信後、下記のような認可レスポンスがJSON形式で返されます。これで、アクセストークンとIDトークンの取得ができました。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
access_token: 'ya29.〜',
expires_in: 3599,
scope: 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid',
token_type: 'Bearer',
id_token: 'eyJ〜'
}
まだまだOAuth 2.0 やOpenID Connectについて学習途中で、誤った記載があるかもですが、学んだことを整理しました。
(もし、誤りがある場合は指摘いただけると助かります)
以上です。