こんにちは!新人エンジニアの皆さん、そしてセキュリティの勉強を始めたばかりの皆さん、日々の開発やお疲れ様です。
システムを作っていると、必ずと言っていいほど直面するのが「ログイン機能」や「APIの安全な呼び出し」ですよね。「ユーザーが本当に本人か」「他の人にデータを勝手に盗まれないか」を考えるのは、まるで大切な家を守る警備システムを設計するようなもの。最初は難しく感じるかもしれませんが、一歩ずつ安全な仕組みを紐解いていけば、必ず理解できるようになりますよ。
今回は、現代のWeb開発で必須の知識となっている「API認証におけるOAuth 2.0とOpenID Connect(OIDC)」について、家の防犯に例えながら優しく解説していきます。実務ですぐに使えるコード例も用意したので、一緒にマスターしていきましょう!
—
1. 家の鍵で例える「OAuth 2.0」と「OpenID Connect」
セキュリティの話をするとき、よく使われるのが「合鍵」と「身分証明書」の例えです。
- OAuth 2.0(オーオーエス・ニーテンゼロ)は「合鍵(アクセストークン)」
- 例えば、家事代行サービスの人に「リビングだけに入ってもいいよ」という期間・場所限定のデジタル合鍵を渡すイメージです。家全体(サーバーの全データ)の鍵ではなく、限られた部屋(特定のAPI)だけに出入りするための「認可」の仕組みになります。
- OpenID Connect(オーアイディシー)は「身分証明書(IDトークン)」
- こちらは「あなたは間違いなく〇〇さんですね」と証明するためのパスポートや運転免許証のようなものです。「認証」の役割を果たします。
この2つを組み合わせて、「私は〇〇です(OIDC)」と証明した上で、「このアプリにデータの読み書きを許可します(OAuth 2.0)」という安全なやり取りを実現するのが現代のスタンダードです。
—
2. 攻撃者が狙う盲点:なぜ「暗黙のフロー」はダメなのか?
昔のWebやスマホアプリでは、ログインした瞬間に直接合鍵(アクセストークン)をバーンと渡してしまう「インプリシット(暗黙の)フロー」というやり方がよく使われていました。
しかし、これは例えるなら、「合鍵を大きく印刷した紙を、街中に貼り出して持ち歩く」ようなもの。もしアプリの隙(脆弱性)を突かれて、URLのフラグメント(#access_token=...の部分)がブラウザの履歴や不正なスクリプトに覗き見されたら、一瞬で合鍵が盗まれてしまいますよね。
そこで登場するのが、今や業界の絶対的なルールとなっている「認可コードフロー + PKCE(ピーシーイー)」です。
—
3. 鉄壁のペア!「認可コードフロー with PKCE」の仕組み
PKCE(Proof Key for Code Exchange)と聞くと呪文のようですが、防犯に例えれば「合い言葉付きの金庫」です。
1. お出かけ前の仕込み(クライアント側)
- アプリ側でランダムな秘密の文字列(コード・ベリファイアー)を作り、それをハッシュ化した「合言葉(コード・チャレンジ)」を認証サーバーにこっそり伝えます。
2. 合鍵ではなく「引き換え券」をもらう
- ユーザーがログインに成功すると、サーバーは合鍵そのものではなく、一度きりしか使えない「引き換え券(認可コード)」をアプリに渡します。もしここで引き換え券が盗まれても、本物の合鍵には変えられません。
3. 秘密の合言葉で合鍵と交換する
- アプリ側は、受け取った引き換え券と、最初に用意した「秘密の合言葉(コード・ベリファイアー)」をセットでサーバーに提示します。サーバーは「おっ、合言葉が一致したね」と確認して初めて、本物の合鍵(アクセストークン)を渡すのです。
この仕組みのおかげで、途中で引き換え券が盗み見られても、合言葉を知らない攻撃者は合鍵を手に入れることができません。
—
4. 実装の具体例:フロントエンドでのPKCEリクエスト構築
それでは、実際にJavaScriptを使って、安全なログイン要求(リクエスト)を組み立てるコードを見てみましょう。実務でそのままベースとして使えるようにコメントを丁寧に書いています。
// ランダムな文字列(コード・ベリファイアー)を生成する関数
function generateCodeVerifier() {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
// URLセーフなBase64エンコードに変換
return btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// コード・ベリファイアーからハッシュ値(コード・チャレンジ)を生成する関数
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode.apply(null, new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// 認証サーバーへリクエストを飛ばすメインの処理
async function startLoginFlow() {
// 1. 秘密の合言葉(ベリファイアー)を作成し、後で使うのでブラウザのセッションストレージに一時保存
const codeVerifier = generateCodeVerifier();
sessionStorage.setItem('code_verifier', codeVerifier);
// 2. 合言葉からハッシュ化されたチャレンジを作成
const codeChallenge = await generateCodeChallenge(codeVerifier);
// 3. 認証サーバーのエンドポイントURL
const authEndpoint = 'https://auth.example.com/oauth/authorize';
const clientId = 'your_client_id_12345'; // あなたのアプリのID
const redirectUri = 'https://myapp.com/callback'; // ログイン後に戻ってくるURL
// 4. パラメータを組み立ててリダイレクト(引っ越し)する
const authUrl = `${authEndpoint}?` +
`response_type=code` + // 引き換え券(コード)を要求する指定
`&client_id=${encodeURIComponent(clientId)}` +
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
`&scope=openid profile api:read` + // 欲しい権限(OIDCのスコープ含む)
`&code_challenge=${encodeURIComponent(codeChallenge)}` + // PKCEのチャレンジ
`&code_challenge_method=S256`; // ハッシュアルゴリズムの指定
// ユーザーを認証画面へ誘導
window.location.href = authUrl;
}
—
5. アクセストークンの有効期限管理とリフレッシュトークンの安全な取り扱い
無事に合鍵(アクセストークン)を手に入れた後も、セキュリティ上の重要なポイントがあります。それは「鍵の寿命」です。
アクセストークンの有効期限は「短く」する
もしアクセストークンの有効期限を「1年」などに設定してしまうと、万が一その鍵が盗まれたとき、1年間ずっと侵入され放題になってしまいます。そのため、アクセストークンの寿命はあえて「15分〜1時間程度」と非常に短く設定します。
では、毎回ログインし直すの? そこで「リフレッシュトークン」
「15分で有効期限が切れたら、ユーザーが何度もパスワードを入れ直さないといけないのでは?」と思いますよね。そこで登場するのがリフレッシュトークン(身分更新カード)です。
- アクセストークンが切れたら、アプリは裏側でリフレッシュトークンを認証サーバーに送り、「新しいアクセストークンをください」とこっそりお代わりを請求します。
- これにより、ユーザーに面倒な再ログインを強いることなく、セキュリティの安全性を保つことができます。
リフレッシュトークンの保管場所に関する重大な注意点
ここが実務で最もミスしやすい泥臭いポイントです。リフレッシュトークンは、アクセストークンよりも強力な「新しい合鍵を生み出す元」です。
- 絶対にやってはいけないこと:
localStorageやsessionStorageなどのブラウザのストレージに保存すること。もしサイトにXSS(クロスサイトスクリプティング)という脆弱性があった場合、JavaScriptから簡単にリフレッシュトークンが盗まれ、悪意ある第三者に渡ってしまいます。 - 推奨される対策:
- SPA(シングルページアプリケーション)の場合: バックエンドのサーバー(BFF: Backend for Frontend)を挟み、リフレッシュトークンはJavaScriptから触れない
HttpOnlyかつセキュアなCookieに保存します。 - スマホアプリ(ネイティブアプリ)の場合: OSが提供する安全な領域(iOSならKeychain、AndroidならEncryptedSharedPreferences)に厳重に暗号化して保存します。
—
まとめ:安全な扉を開くために
今回は、OAuth 2.0とOpenID Connectの基本から、PKCEを使った安全なコードフロー、そしてトークンの有効期限や保存場所の注意点までを解説しました。
- ログインやAPI連携には「認可コードフロー + PKCE」を使う。
- アクセストークンは短命にし、リフレッシュトークンでスマートに更新する。
- 強力なトークンはJavaScriptから触れない場所(HttpOnly CookieやOSの安全な領域)に隠す。
セキュリティの仕組みは、一見すると制約が多くて面倒くさいと感じるかもしれません。しかし、それはユーザーの大切なデータやプライバシーを守るための頑丈な盾となります。
一つひとつの仕組みの意味を理解すれば、自信を持って安全なシステムを設計できるようになります。焦らず、一歩ずつセキュアな開発を楽しんでいきましょう!
コメント