クライアントアプリを作って実装ポイントを学ぶ
前のセクションでは、アプリポータルでアプリケーションを作成し、curlを使用してMoney Forwardの認可サーバーとやり取りし、トークンを取得して「事業者情報の取得API」などの保護されたリソースにアクセスする方法を紹介しました。このチュートリアルでは、Node.jsを用いてMoney Forwardの認可サーバーと連携するOAuthクライアントアプリケーションを構築しながら、実装ポイントを解説していきます。
このチュートリアルはハンズオン形式で、ローカル環境で実際にコードを書いて動作を確認しながら学べる内容になっています。環境設定や前提条件についても、ローカル環境でのセットアップを想定した内容になっています。OAuthクライアント機能の実装手順について、認可、アクセストークンの取得、アクセストークンが必要な保護されたAPIの呼び出し、トークンのリフレッシュ、トークンの取り消しまでを詳しく説明します。
1. 環境設定と前提条件
-
Node.js: Node.jsランタイム環境がインストールされていることを確認してください。この例はNode.jsバージョン22.9.0に基づいています。
-
OAuth2 クライアントライブラリ: OAuth2認可プロセスを正しく実装するため、
@badgateway/oauth2-client
ライブラリを使用します。他の言語向けのOAuthクライアントライブラリについては、OAuth2.0 Code Listを参照してください。 -
プロジェクトの実行方法については、GitHubリポジトリのREADMEに記載されている手順を参照してください。
注意: OAuth2.0の誤った実装は重大なセキュリティインシデントを引き起こす恐れがあります。必ず信頼性が高く、広く使われた実績のあるライブラリを利用してください。
2. プロジェクト構成の概要
次のルートと機能を備えた簡易的なOAuth2アプリクライアントを構築します:
/start_authorization
: 認可プロセスを開始する/callback
: 認可成功後のコールバックを処理し、アクセストークンを取得する/revoke
: アクセストークンとリフレッシュトークンを取り消します。/office
: 保護されたAPIリソースにアクセスする
3. OAuth2 クライアントの実装
OAuth2クライアントの設定
認可プロセスを開始する前に、OAuth2クライアントの情報を設定します。クライアントID、クライアントシークレット、リダイレクトURI、認可サーバーURLなどの必須情報を以下のように設定します。
const CLIENT_ID = 'YOUR CLIENT_ID'; // OAuth2クライアントのクライアントID
const CLIENT_SECRET = 'YOUR CLIENT_SECRET'; // OAuth2クライアントのクライアントシークレット
const REDIRECT_URI = 'YOUR_REDIRECT_URI'; // 登録済みリダイレクトURI, 例: http://localhost:12345/callback
const AUTHORIZATION_SERVER = 'https://api.biz.moneyforward.com'; // OAuth2認可サーバーの基本URL
// OAuth2クライアントを設定でインスタンス化
const client = new OAuth2Client({
server: AUTHORIZATION_SERVER,
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
authorizationEndpoint: '/authorize',
tokenEndpoint: '/token',
authenticationMethod: 'client_secret_post', // クライアント認証の方法
// authenticationMethod: 'client_secret_basic', // クライアント認証の方法 CLIENT_SECRET_BASIC を選択した場合
});
詳細な説明:
CLIENT_ID
: Money Forwardによって提供されるクライアントの一意の識別子。CLIENT_SECRET
: クライアントシークレット。安全に保管してください。REDIRECT_URI
: 認可が完了すると、このURIにリダイレクトされて認可コードが交換されます。AUTHORIZATION_SERVER
: Money Forward認可サーバーの基本URL。
注意: このサンプルコードでは
CLIENT_ID
とCLIENT_SECRET
がハードコーディングされていますが、実際のアプリケーションでは、これらを安全な場所に保管する必要があります。
設定が完了したら、認可プロセスの実装に進みます。
A. 認可
/start_authorization
ルートで認可リクエストを開始し、ユーザーをマネーフォワード クラウドの認可サーバーにリダイレクトします。codeVerifier
とstate
はオプションですが、セキュリティを強化するため特別な理由がない限りは指定してください。
async function startAuthorization(req: express.Request, res: express.Response) {
codeVerifier = await generateCodeVerifier(); // PKCE用のコードバリファイアを生成
state = crypto.randomBytes(16).toString('hex'); // CSRF防止のためにランダムな状態値を生成、state値についてはセキュリティ要件に応じて必要な強度のものを生成するようにしてください。
// 認可URLを生成
const authorizeUrl = await client.authorizationCode.getAuthorizeUri({
redirectUri: REDIRECT_URI,
codeVerifier,
state,
scope: ['mfc/admin/office.read'], // 認可のアクセス範囲
});
console.info('Redirecting to', authorizeUrl); // デバッグ用にURLをログ出力
res.redirect(authorizeUrl); // 認可URLにユーザーをリダイレクト
}
詳細な説明:
codeVerifier
の生成(オプション): PKCE(Proof Key for Code Exchange)セキュリティメカニズムの一部であり、認可コード交換のセキュリティを強化するためのランダム生成文字列です。state
の生成(オプション): CSRF防止のためのランダムなstate
値。- 認可URLの構築: リダイレクトURI、
codeVerifier
、state
、およびスコープを含みますが、ライブラリによって自動的に構築されるため、手動で認可URLを構築する必要はありません。 - 必要なスコープの定義: ここでは、
mfc/admin/office.read
を使用して保護されたリソース(/office)のアクセス権限を指定しています。利用するマネーフォワード クラウドの各サービスのAPIドキュメントに記載のをスコープを指定してください。 - 認可サーバーへのリダイレクト: ユーザーを認可サーバーにリダイレクトして認可プロセスを完了します。
B. アクセスおよびリフレッシュトークンの取得
/callback
ルートで、アプリケーションは認可サーバーからアクセストークンとリフレッシュトークンを取得します。
async function handleAuthorizationCallback(
req: express.Request,
res: express.Response,
) {
try {
const { code, state: returnedState } = req.query; // クエリパラメータからコードと状態を抽出
// 返された状態が初期の状態と一致することを確認してセキュリティを保護
if (
!crypto.timingSafeEqual(
Buffer.from(String(returnedState)),
Buffer.from(String(state)),
)
)
throw new Error('State does not match');
if (!codeVerifier) throw new Error('Code verifier is missing'); // PKCE用のコードバリファイアが存在するか確認
// トークン交換用のリクエストペイロードを作成
const authorizationCodeRequest: AuthorizationCodeRequest = {
grant_type: 'authorization_code', // 認可コードのグラントタイプ
code: code as string,
redirect_uri: REDIRECT_URI,
code_verifier: codeVerifier, // PKCE用のコードバリファイアを送信
};
// 認可コードを使用してアクセストークンとリフレッシュトークンを交換
tokenResponse = await client.request(
'tokenEndpoint',
authorizationCodeRequest,
);
console.info('Access Token Response:', tokenResponse); // トークンレスポンスをログ出力
res.redirect('/'); // ホームページにリダイレクト
} catch (error) {
console.error('Error during callback processing:', error); // エラーがあればログ出力
res
.status(500)
.send(
'アクセストークン取得に失敗しました / Failed to obtain access token.',
);
}
}
詳細な説明:
state
の検証(存在する場合): CSRF攻撃防止のため、返されたstate
が一致するか確認します。- トークンリクエストの構築: 先に生成された場合は
codeVerifier
を含め、そうでなけれ ば省略します。 - トークンの取得: 認可コードをアクセストークンとリフレッシュトークンに交換します。
C. トークンの保存
以後のAPI呼び出しのために、グローバル変数tokenResponse
に一時的にトークンを保存します。実際のアプリケーションでは、データベースなどの長期で永続化可能な保存方法を採用してください。
let tokenResponse: TokenResponse | null = null;
詳細な説明:
- グローバル変数
tokenResponse
: API呼び出し、トークンリフレッシュ、取り消しのためにトークン情報を保持します。
D. 保護されたAPIの呼び出し
/office
ルートでアクセストークンを使用して保護されたリソースにアクセスします。このサンプルコードでは事業者情報の取得APIの呼び出しを例にしていますが、要件に応じて必要なAPI呼び出しを実装してください。
async function fetchProtectedResource(
req: express.Request,
res: express.Response,
) {
if (!tokenResponse) {
res
.status(401)
.send(
'アクセストークンがありません。再度認可からやり直してください。 / Access token is missing. Please start authorize again.',
); // アクセストークンが存在するか確認
return;
}
try {
// リソースの初回取得を試行
let response = await fetch(
'https://bizapis.moneyforward.com/admin/office',
{
method: 'GET',
headers: {
Authorization: `Bearer ${tokenResponse.access_token}`, // アクセストークンを送信
},
},
);
// トークンが期限切れの場合、リフレッシュしてリソース取得を再試行
if (response.status === 401) {
console.info('Token expired. Refreshing token...');
const refreshStatus = await refreshAccessToken(req, res);
if (!refreshStatus) {
// リフレッシュが成功したことを確認
res
.status(401)
.send(
'アクセストークンが期限切れですが、リフレッシュに失敗しました。再度認可からやり直してください。 / Token expired and refresh failed. Please start authorize again.',
);
return;
}
// リフレッシュしたトークンでリソース取得を再試行
response = await fetch('https://bizapis.moneyforward.com/admin/office', {
method: 'GET',
headers: {
Authorization: `Bearer ${tokenResponse.access_token}`, // リフレッシュしたアクセストークンを送信
},
});
}
// 最終的なリソース取得の応答を処理
if (!response.ok) {
throw new Error(`Failed to fetch resource: ${response.statusText}`); // 応答エラーを処理
}
const data = await response.json(); // 応答JSONを解析
console.info('Protected Resource Response:', data); // 応答データをログ出力
res.json(data); // クライアントにデータを送信
} catch (error) {
console.error('Error fetching protected resource:', error); // エラーがあればログ出力
res
.status(500)
.send(
'保護されたリソースの取得に失敗しました。 / Failed to fetch protected resource.',
);
}
}
詳細な説明:
- トークンの確認:
tokenResponse
が有効なアクセストークンを含むことを確認し、不足している場合はユーザーにログインを促します。 - APIリクエストの構築: 保護されたリソースにアクセスするため、リクエストヘッダーにアクセストークンを含めます。
- トークン期限切れの処理: 応答ステータスが
401
(期限切れ)の場合、refreshAccessToken
を使用してトークンをリフレッシュします。 - APIリクエストの再試行: トークンのリフレッシュが成功した場合、新しいアクセストークンでリクエストを再試行します。
- 最終応答: 成功した場合はデータを解析して返却し、失敗した場合はエラーをログに記録します。
リフレッシュを自動的に行ってくれるOAuth2.0クライアントライブラリも存在しています。利用するライブラリの使用方法をよく確認してください。
E. トークンのリフレッシュ
トークンが期限切れの場合はリフレッシュを行ない、新しいアクセストークンを取得します。リフレッシュに失敗した場合は再認可を促します。
async function refreshAccessToken(
req: express.Request,
res: express.Response,
): Promise<boolean> {
// リフレッシュトークンが存在するか確認
if (!tokenResponse?.refresh_token) {
console.error('Refresh token is missing.'); // リフレッシュトークンがない場合エラーをログ出力
return false; // リフレッシュトークンがない場合falseを返す
}
try {
// リフレッシュトークンのリクエストペイロードを作成
const refreshRequest: RefreshRequest = {
grant_type: 'refresh_token', // リフレッシュトークンのグラントタイプ
refresh_token: tokenResponse.refresh_token,
};
console.info('Refreshing token with request:', refreshRequest); // リクエストペイロードをログ出力
// リフレッシュトークンを使用して新しいアクセストークンをリクエスト
tokenResponse = await client.request('tokenEndpoint', refreshRequest);
console.info('New Refreshed Token Response:', tokenResponse); // 更新されたトークンをログ出力
return true; // リフレッシュ成功の場合trueを返す
} catch (error) {
console.error('Error refreshing token:', error); // エラーがあればログ出力
return false; // リフレッシュ失敗の場合falseを返す
}
}
詳細な説明:
- リフレッシュトークンの確認: リフレッシュトークンが利用可能であることを確認します。
- リフレッシュリクエストの構築: リフレッシュトークンを使ったリクエストを作成します。
- トークンの更新: 現在のトークンをリフレッシュし、新しいアクセストークンとリフレッシュトークンを取得します。
F. トークンの取り消し
トークンを取り消してセキュリティを強化するため、/revokeルートを使用してアクセストークンおよびリフレッシュトークンを取り消します。これはオプションの操作です。
async function revokeAccessToken(req: express.Request, res: express.Response) {
if (!tokenResponse || !tokenResponse.access_token) {
res
.status(400)
.send('アクセストークンがありません / Access token is missing'); // アクセストークンが存在するか確認
return;
}
try {
// トークンを取り消すためにリクエストを送信
await client.request('revocationEndpoint', {
token: tokenResponse.access_token,
});
tokenResponse = null; // 取り消し後にトークンレスポンスをクリア
console.info('Token revoked successfully'); // 成功メッセージをログ出力
res.redirect('/'); // ホームページにリダイレクト
} catch (error) {
console.error('Error revoking token:', error); // エラーがあればログ出力
res
.status(500)
.send('トークンの取り消しに失敗しました / Failed to revoke token.');
}
}
詳細な説明:
- トークンの確認:
tokenResponse
が有効なトークンを含むことを確認します。 - 取り消しリクエスト: トークン無効化のため、エンドポイントに取り消しリクエストを送信します。
- トークンのクリア: 取り消しが成功した場合、
tokenResponse
をクリアします。
4. テストと検証
- 認可: Authorizeボタンをクリックまたは
/start_authorization
にアクセスして認可プロセスを開始し、トークンを取得します。 - 保護されたリソースへのアクセス: Fetch Protected Resourceボタンをクリックまたは
/office
にアクセスして、API呼び出しが成功しデータが表示されることを確認します。 - トークンの取り消し: Revoke Tokenボタンをクリックまたは
/revoke
にアクセスして、トークンを取り消します。
5. まとめ
本チュートリアルでは、Node.jsを使用してMoney Forwardの認可サーバーと連携するOAuth2クライアントアプリケーションの実装方法を学びました。そして以下の主要な実装ポイントをカバーしました。
- 環境設定と前提条件: 必要なツールとライブラリのセットアップ方法を確認しました。
- OAuth2クライアントの設定: クライアントID、クライアントシークレット、リダイレクトURIなどの基本設定を 行ないました。
- 認可プロセスの開始: ユーザーを認可サーバーにリダイレクトして認可コードを取得する手順を実装しました。
- アクセストークンとリフレッシュトークンの取得: 認可コードを使用してトークンを取得する方法を解説しました。
- 保護されたAPIの呼び出し: 取得したアクセストークンを使用して保護されたリソースにアクセスする方法を説明しました。
- トークンのリフレッシュと取り消し: トークンの有効期限が切れた際のリフレッシュ手順と、セキュリティ強化のためのトークン取り消し方法を紹介しました。
これらの実装ポイントを通じて、OAuth2クライアントアプリケーションの基本的な流れとセキュリティ上の考慮事項を説明しました。
6. 次のステップ
このチュートリアルは実装ポイントについての理解を目的としているため、実際のアプリにはそのまま使用できるものではありません。実際のアプリに利用するためには、以下のような内容について検討することを推奨しています。
- アプリケーションにOAuth2クライアントを統合する。
client_id
,client_secret
,redirect_uri
を適切に保存する。- 取得したアクセストークンおよびリフレッシュトークンを適切に保存する。