通常、bearerトークンとしてIDトークンを使用することは推奨されません。
IDトークンとアクセストークンは、それぞれ異なる目的を持っています。
-
IDトークン (ID Token):
- 認証 (Authentication) のためのものです。
- ユーザーが「誰であるか」を証明する情報(ユーザーID、名前、メールアドレスなど)が含まれます。
- 主にクライアントアプリケーションがユーザーの認証状態を確認するために使用されます。
- OpenID Connect (OIDC) の一部として発行されます。
- 通常、JSON Web Token (JWT) 形式であり、署名によって改ざんされていないことを検証できます。
- リソースサーバーがアクセス権限を判断するための情報(認可情報)は含まれていません。
-
アクセストークン (Access Token):
- 認可 (Authorization) のためのものです。
- 保護されたリソース(APIなど)へのアクセス権限を付与するために使用されます。
- クライアントがリソースサーバーにリクエストを送信する際に、そのリクエストが正当な権限を持つものかを判断するために使用されます。
- 通常、不透明な文字列(JWT形式の場合もあります)であり、リソースサーバー側でその有効性や権限を検証します。
- リソースサーバーがAPIへのアクセスを許可するかどうかを判断するために必要な情報が含まれています。
なぜIDトークンをbearerトークンとして使用すべきではないのか?
主な理由は以下の通りです。
- 目的の不一致: IDトークンはユーザーの認証情報を示すものであり、リソースへのアクセス権限を示すものではありません。リソースサーバーがIDトークンを受け取っても、そのユーザーが特定のリソースにアクセスする権限を持っているかどうかを判断できません。
- 認可情報の欠如: IDトークンには、リソースサーバーが認可を判断するためのスコープや権限に関する情報が含まれていません。
-
セキュリティ上の懸念:
- IDトークンにはユーザーの個人情報が含まれることがあり、これを不必要にリソースサーバーに送信することは、情報漏洩のリスクを高める可能性があります。
- IDトークンはクライアントアプリケーションが消費することを想定しており、リソースサーバーが検証するための「対象オーディエンス (audience)」がリソースサーバーではない場合が多いです。もしリソースサーバーがIDトークンを検証しようとしても、
aud
(audience) クレームがリソースサーバー自身を示していないため、検証に失敗するか、不適切な検証が行われる可能性があります。
- ベストプラクティスからの逸脱: OAuth 2.0 と OpenID Connect の標準的な設計では、認証にはIDトークン、認可にはアクセストークンを使用するという明確な役割分担があります。この役割分担に従うことで、よりセキュアで管理しやすいシステムを構築できます。
そんなことは言っても、メールアドレスとかはIDトークンからしか取れないし、クライアントからIDトークンからもらいたい!
そのような場合は ユーザー情報エンドポイント (UserInfo Endpoint) を利用しましょう。
認証プロバイダーが提供するUserInfo
エンドポイントに、アクセストークンを提示してリクエストを送信することで、認証されたユーザーの標準的なプロフィール情報を取得できます。これが、サーバーがユーザーの身元情報を取得する最も一般的な方法です。
Authorizationヘッダーにはアクセストークンを入れましょう。
具体的にどうするの?
サーバーが「誰であるか」を証明する情報は、以下の流れで取得するのが推奨されるベストプラクティスです。
- クライアントがIDトークンとアクセストークンを取得。
- クライアントはIDトークンを検証し、ユーザーの身元情報を利用。
- サーバー(API)は、クライアントから受け取ったアクセストークンを検証し、リソースへの認可を判断。
- サーバーがユーザーのプロフィール情報が必要な場合、アクセストークンを使用して認証プロバイダーの
UserInfo
エンドポイントに問い合わせる。
この流れに沿って例を出してみたいと思います。
このサンプルでは、以下の仮定を置きます。
-
認証プロバイダー(IdP) が
https://your-auth-provider.com
というURLでUserInfo
エンドポイントを提供している。 - クライアントから渡されるアクセストークンは、
Authorization
ヘッダーのBearer
スキームで提供される。
必要なモジュールのインストール
まず、プロジェクトフォルダを作成し、ExpressとHTTPリクエストを送信するためのaxios
(またはnode-fetch
など)をインストールします。
mkdir userinfo-express-example
cd userinfo-express-example
npm init -y
npm install express axios dotenv
環境変数の設定
UserInfo
エンドポイントのURLなど、機密性の高い情報は環境変数として管理するのがベストプラクティスです。プロジェクトのルートに.env
ファイルを作成し、以下の内容を記述します。
USERINFO_ENDPOINT=https://your-auth-provider.com/userinfo
重要: USERINFO_ENDPOINT
は、実際に使用する認証プロバイダーのUserInfo
エンドポイントのURLに置き換えてください。
Expressアプリケーションの作成
app.js
(またはindex.js
)というファイルを作成し、以下のコードを記述します。
require('dotenv').config(); // .envファイルから環境変数を読み込む
const express = require('express');
const axios = require('axios'); // HTTPリクエストライブラリ
const app = express();
const port = 3000;
// UserInfoエンドポイントのURLを環境変数から取得
const USERINFO_ENDPOINT = process.env.USERINFO_ENDPOINT;
// UserInfoエンドポイントが設定されているか確認
if (!USERINFO_ENDPOINT) {
console.error('Error: USERINFO_ENDPOINT environment variable is not set.');
process.exit(1); // アプリケーションを終了
}
// JSONボディをパースするためのミドルウェア
app.use(express.json());
// UserInfoを取得するエンドポイント
app.get('/get-userinfo', async (req, res) => {
// クライアントからAuthorizationヘッダーを取得
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authorization Bearer token is missing or malformed.' });
}
// "Bearer " の部分を除去してアクセストークンを取得
const accessToken = authHeader.split(' ')[1];
try {
// UserInfoエンドポイントにリクエストを送信
// アクセストークンをAuthorizationヘッダーに含める
const response = await axios.get(USERINFO_ENDPOINT, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
// 取得したユーザー情報をクライアントに返す
res.json(response.data);
} catch (error) {
console.error('Error fetching user info:', error.response ? error.response.data : error.message);
// エラーレスポンスをクライアントに返す
const statusCode = error.response ? error.response.status : 500;
const errorMessage = error.response ? error.response.data : 'Failed to fetch user information.';
res.status(statusCode).json({ error: errorMessage });
}
});
// サーバーを起動
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
console.log(`UserInfo Endpoint: ${USERINFO_ENDPOINT}`);
});
コードの説明
-
dotenv
の読み込み:.env
ファイルから環境変数をロードします。 - ExpressとAxiosのインポート: ウェブサーバーのフレームワークとHTTPリクエストライブラリをインポートします。
-
USERINFO_ENDPOINT
の取得:.env
ファイルで設定したUserInfo
エンドポイントのURLを読み込みます。 -
/get-userinfo
エンドポイント:- このエンドポイントは、クライアントからのGETリクエストを受け取ります。
-
req.headers.authorization
から**Bearer
トークン(アクセストークン)** を抽出します。 - アクセストークンが存在しない、または形式が不正な場合は401 Unauthorizedエラーを返します。
-
axios.get()
を使って、設定されたUSERINFO_ENDPOINT
に対してGETリクエストを送信します。 - この際、リクエストヘッダーの
Authorization
に**Bearer <アクセストークン>
**を含めることが重要です。 - 認証プロバイダーから返されたユーザー情報(通常はJSON形式)をクライアントに返します。
- エラーが発生した場合は、適切なステータスコードとエラーメッセージを返します。
- サーバーの起動: アプリケーションを指定されたポートでリッスンします。
実行方法
-
上記のファイルを保存します(例:
app.js
)。 -
ターミナルでプロジェクトフォルダに移動し、以下を実行します。
node app.js
サーバーが起動し、「Server running at http://localhost:3000」のようなメッセージが表示されます。
テスト方法
PostmanやcURLなどのツールを使用して、このエンドポイントにリクエストを送信できます。
リクエスト例 (cURL):
curl -X GET \
http://localhost:3000/get-userinfo \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN_HERE'
YOUR_ACCESS_TOKEN_HERE
の部分を、有効なアクセストークンに置き換えてください。認証プロバイダーから取得したアクセストークンを使用する必要があります。
このリクエストを送信すると、UserInfo
エンドポイントが認証プロバイダーからユーザー情報を取得し、その情報をクライアントに返します。
それじゃあなんでIDトークンの使い道は?
IDトークンは、主にクライアントアプリケーションがユーザーを認証し、その身元情報を取得するために使うべきです。例えば、
-
ユーザーの認証状態の確認:
- Webアプリケーション(SPAなど)やモバイルアプリケーションが、ユーザーがログインしているかどうかを確認する際にIDトークンを使用します。IDトークンは署名されており、改ざんされていないか検証することで、そのユーザーが「誰であるか」を信頼性高く判断できます。
- 有効期限が切れていないかを確認し、セッションの継続や再認証の必要性を判断します。
-
ユーザーインターフェースのパーソナライズ:
- IDトークンに含まれるユーザー情報(例:名前、プロフィール画像URL、メールアドレスなど)を抽出し、アプリケーションのUIに表示することで、ユーザーにパーソナライズされた体験を提供します。「こんにちは、[ユーザー名]さん!」といった表示がその典型です。
-
クライアントサイドでのユーザーセッション管理:
- 認証が成功した後、クライアントアプリケーションはIDトークンを安全な方法(例:HTTP Only Cookieやローカルストレージなど、セキュリティを考慮した保存場所)で保存し、その後のセッション管理に利用します。これにより、ページのリロード時やアプリケーションの再起動時にもユーザーの認証状態を維持できます。
-
アクセストークンの発行元ユーザーの識別:
- OpenID Connectのフローにおいて、IDトークンはアクセストークンと一緒に発行されます。IDトークンは、そのアクセストークンがどのユーザーのために発行されたのかをクライアントに示します。クライアントはこのIDトークンを確認することで、受け取ったアクセストークンが正しいユーザーのものであることを検証できます。
まとめ
IDトークンは、ユーザーの認証に関する情報を提供するものであり、クライアントアプリケーションがユーザーの身元を確認し、その情報を基にUIを構築したり、セッションを管理したりするために利用されます。
決して、IDトークンをリソースサーバーへのアクセス認可(API呼び出しなど)のために直接使うべきではありません。 リソースへのアクセスにはアクセストークンを使用するという役割分担を理解することが、OAuth 2.0とOpenID Connectを正しく、かつセキュアに利用する上で非常に重要です。
ちなみに
不透明なアクセストークンについて
アクセストークンにも種類があり、auth0の場合、不透明なアクセストークンというものがあります。
不透明なアクセストークンはjwtの検証だけでは不十分で、/userinfoエンドポイントに問い合わせて検証することで、有効なアクセストークンとして扱うことができます。
Bearerトークンの存在意義について
そもそもBearerトークンは、セキュリティトークンのうちその利用可否が「トークンの所有」のみで決定されるもので、そのトークンを持っているのが誰であるか(=認証)までは行いません。
それで認可に使われているので認証用のIDトークンはそういった意味でもBearerトークンに入れてはいけないのですね。
アクセストークンの代わりにIDトークンを入れるとセキュリティホールになる?
実は何も起こりません。 というのもきちんと検証していれば改竄された怪しいトークンはシャットアウトできますし、scopeが必要なのであればそもそもIDトークンを入れてないはずなのです。
トークンを拾った誰かがIDトークンをデコードすれば、IDトークンに含まれるユーザー情報からどんなユーザーでログインできるのかは推測できますが、そもそも拾うことも少ないリスクですし、そこからユーザーネームとパスワードまで当てるのは至難の業です。
基本的なことをやっていれば問題ありません。