Slide 1

Slide 1 text

Laravel で学ぶ OAuth と OpenID Connect の基礎と実装 the よしだ 2026.03.21 PHPerKaigi 2026

Slide 2

Slide 2 text

  2 X: @theyoshida3 ● フリーでは珍しい PHPer ● 担当するプロダクトは PHP と Go ● 最近よく触るのは Terraform ● 好きなお酒は メガジョッキハイボール ● 愛⽤しているプロテインは X-PLOSION the よしだ Koji Yoshida フリー株式会社

Slide 3

Slide 3 text

  3 ● OAuth と OpenID Connect の違いを理解する ● Laravel での実装イメージを掴む 本⽇のゴール

Slide 4

Slide 4 text

4 01 OAuth とは 02 OpenID Connect とは 03 Laravel での実装例 04 まとめ ⽬次

Slide 5

Slide 5 text

5 01 OAuth とは 02 OpenID Connect とは 03 Laravel での実装例 04 まとめ ⽬次

Slide 6

Slide 6 text

6 OAuthは 認可のための仕組み 「OAuth = ログイン」ではない 1. ユーザーが認可 2. アクセストークン発⾏ 3. リソースへのアクセス OAuth とは

Slide 7

Slide 7 text

7 認証(Authentication) 本⼈確認 ● ID/PW でのログイン ● ⽣体認証 ● SMS 認証 認可(Authorization) 権限の移譲 ● Googleドライブで「閲覧のみ」の権限を付与 ● サードパーティアプリで Googleフォトの写真を読み込む ● フリーナンス by freee で freee請求書の請求書を読み込む 認証と認可の違い

Slide 8

Slide 8 text

8 グラントタイプ 「アクセストークンを発⾏するための具体的な⼿順」のこと ● 認可コードグラント ● クライアントクレデンシャルグラント ● リフレッシュトークングラント 今回は「認可コードグラント」を説明 ⽤語の解説

Slide 9

Slide 9 text

9 スコープ スコープ = 権限の範囲 スコープ名と紐づく権限はサービスごとに異なる Githubの場合 ⽤語の解説 スコープ 権限の内容 repo 全権限 公開・非公開リポジトリの 読み書き、管理すべて public_repo 公開リポジトリのみの書き込みアクセス repo:status コミットステータス(CIの結果など)の読み書き repo_deployment デプロイステータスの読み書き

Slide 10

Slide 10 text

10 登場⼈物 ● リソースオーナー ○ ユーザー(データ所有者) ● クライアント ○ 許可を得てリソースにアクセスするアプリ ● 認可サーバー ○ ユーザーを認証し、認可コードやトークンを発⾏するサーバー ● リソースサーバー ○ アクセストークンを検証し、実際のデータ(プロフィール、写真など)を提 供するサーバー ⽤語の解説

Slide 11

Slide 11 text

11 認可コードグラントの流れ

Slide 12

Slide 12 text

12 認可コードグラントの流れ: 認可コード取得

Slide 13

Slide 13 text

13 連携開始

Slide 14

Slide 14 text

14 Google の認可サーバーへアクセスする場合 認可サーバーへアクセス GET /o/oauth2/v2/auth ?response_type=code &client_id={client_id} &scope={scope} &redirect_uri={redirect_uri} &state={state} HTTP/1.1 Host: accounts.google.com ● response_type 認可コードグラントは「code」固定 ● client_id 事前に認可サーバーに登録されるクライアント ID ● scope アクセスを許可してほしい情報の範囲 ● redirect_uri 事前に認可サーバーに登録されるリダイレクト URI ● state クライアントで⽣成するランダムな⽂字列 認可レスポンス時に値チェック

Slide 15

Slide 15 text

15 認証‧認可 画⾯

Slide 16

Slide 16 text

16 Google の認可サーバーレスポンスの場合 認可レスポンス HTTP/1.1 302 Found Location: {redirect_uri} ?code={authorization_code} &state={state} ● redirect_uri リダイレクト先 ● authorization_code アクセストークンと交換するためのコード 有効期限は短く、⼀度使うと無効になる ● state 認可サーバーへアクセス時に⽣成したもの 値チェック(CSRF 対策) この後のリダイレクト先でアクセストークン取得を実施

Slide 17

Slide 17 text

17 認可コードグラントの流れ: アクセストークン取得

Slide 18

Slide 18 text

18 Google のトークンリクエストの場合 トークンリクエスト POST /token ?grant_type=authorization_code &code={authorization_code} &client_id={client_id} &client_secret={client_secret} &redirect_uri={redirect_uri} HTTP/1.1 Host: oauth2.googleapis.com Content-Type: application/x-www-form-urlencoded ● grant_type 「authorization_code」固定 ● code 認可レスポンスで取得した authorization_code ● client_id 事前に認可サーバーに登録されるクライアント ID ● client_secret 事前に認可サーバーから発⾏されるシークレット ● redirect_uri 事前に認可サーバーに登録されるリダイレクト URI

Slide 19

Slide 19 text

19 ● Authorization ヘッダーでもOK ○ Authorization: Basic {Base64(client_id:client_secret)} ○ client_id, client_secret はボディに含めない ● public clientの場合はAuthorizationやclient_secretは含めないでもOK トークンリクエスト補⾜

Slide 20

Slide 20 text

20 Google のトークンレスポンスの場合 トークンレスポンス HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 { "access_token": "{access_token}", "expires_in": 3920, "refresh_token": "{refresh_token}", "scope": "https://www.googleapis.com/auth/drive.metadata.readonly", "token_type": "Bearer" }

Slide 21

Slide 21 text

21 ● access_token API 実⾏時に使⽤するトークン 有効期間は短い ● expires_in アクセストークンの有効期限(秒) ● refresh_token access_token 期限切れの際に再取得するためのトークン 無効になると再認可が必要 ● scope アクセスを許可された情報の範囲 OAuth の仕様上、必須ではないが Google の場合は含まれる トークンレスポンス

Slide 22

Slide 22 text

22 認可コードグラントの流れ: リソースアクセス

Slide 23

Slide 23 text

23 Google Drive API の場合 リソース要求 GET /drive/v3/files HTTP/1.1 Host: www.googleapis.com Authorization: Bearer {access_token} ● URL やレスポンスは提供元ごとに独⾃ ● Authorization: Bearer {access_token} は共通仕様

Slide 24

Slide 24 text

24 現在はドラフト段階 セキュリティ上のベストプラクティスを標準化した内容となっている ● 全クライアントでの PKCE 必須化 ○ これまでは パブリッククライアントのみ推奨 ● 安全性の低いグラントタイプ廃⽌ ○ インプリシットグラント ○ リソースオーナーパスワードクレデンシャル ● リダイレクト URI の完全⼀致 検証 ○ これまでは前⽅⼀致が許容されるケースがあった ● リフレッシュトークンのセキュリティ要件 ○ 送信者制限付きリフレッシュトークン発⾏ ○ リフレッシュトークンのローテーション OAuth 2.1 の変更点

Slide 25

Slide 25 text

25 認可コードの横取り(なりすまし)を防ぐための合⾔葉 下記の⼿順でなりすましができてしまう ● スマホアプリでは、 my-app://callback?code=xxx (カスタム URL スキー ム)を使う ● 偽アプリの URL スキームを正規のアプリと同じものとして設定 ● 認可コード取得後にブラウザからアプリに戻る際に偽アプリが反応 PKCE とは

Slide 26

Slide 26 text

26 認可コード横取りの流れ

Slide 27

Slide 27 text

27 ● code_verifier ランダムな⽂字列として、クライアントで⽣成 ● code_challenge code_verifier を SHA256 でハッシュ化したもの ハッシュ化をしないことも可能だが⾮推奨 ● code_challenge_method S256 or plain PKCE で登場するパラメーター

Slide 28

Slide 28 text

28 PKCE の流れ

Slide 29

Slide 29 text

29 Google の認可サーバーへアクセスする場合 認可サーバーへアクセス GET /o/oauth2/v2/auth ?response_type=code &client_id={client_id} &scope={scope} &redirect_uri={redirect_uri} &state={state} &code_challenge={code_challenge} &code_challenge_method=S256 HTTP/1.1 Host: accounts.google.com ● code_challenge(code_verifier のハッシュ化), code_challenge_method を含める ● 認可サーバー側では認可コードと紐づけて保存する

Slide 30

Slide 30 text

30 Google のトークンリクエストの場合 トークンリクエスト POST /token ?grant_type=authorization_code &code={authorization_code} &client_id={client_id} &client_secret={client_secret} &redirect_uri={redirect_uri} &code_verifier={code_verifier} HTTP/1.1 Host: oauth2.googleapis.com Content-Type: application/x-www-form-urlencoded ● code_verifier を含める ● 認可サーバーでは code_challenge と⼀致するか検証する ● 認可コードの横取り時は検証が失敗する

Slide 31

Slide 31 text

31 01 OAuth とは 02 OpenID Connect とは 03 Laravel での実装例 04 まとめ ⽬次

Slide 32

Slide 32 text

32 OAuth をベースに拡張した 認証のための仕組み OIDC とも呼ばれている 特徴 ● openid スコープ 認可リクエストの scope パラメーターに必ず openid を含める ● ID トークン ユーザーの⾝元情報が⼊った証明書 ● UserInfo エンドポイント 詳細なユーザー情報を取得するための標準化されたエンドポイント OpenID Connect とは

Slide 33

Slide 33 text

33 グラントタイプ に相当するものをフローと呼ぶ 今回は「認可コードフロー」を説明 共通する⽤語も多いが、下記は表現が異なる OAuth 2.0 と OIDC の ⽤語マッピング OAuth 2.0 OIDC リソースオーナー エンドユーザー クライアント リライイング・パーティ(RP) 認可サーバー OpenID プロバイダー(IdP) リソースサーバー UserInfo エンドポイント

Slide 34

Slide 34 text

34 認可コードフローの流れ

Slide 35

Slide 35 text

35 認可コードフローの流れ: 認可コード取得

Slide 36

Slide 36 text

36 IDプロバイダーへアクセス GET /o/oauth2/v2/auth ?response_type=code &client_id={client_id} &scope=openid profile email &redirect_uri={redirect_uri} &state={state} &nonce={nonce} HTTP/1.1 Host: accounts.google.com ● scope openid が必須 profile, email, address, phone がある ● nonce ID トークンの中に指定した値が記載される ID トークンの検証に利⽤ ● リフレッシュトークン取得 Google では独⾃仕様 access_type=offline, prompt=consent

Slide 37

Slide 37 text

37 認可コードフローの流れ: トークン取得と検証

Slide 38

Slide 38 text

38 トークンレスポンス HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 { "access_token": "{acceess_token}", "expires_in": 3599, "scope": "openid https://www.googleapis.com/auth/userinfo.email ...", "token_type": "Bearer", "id_token": {id_token}, "refresh_token": "{refresh_token}" }

Slide 39

Slide 39 text

39 ● id_token デジタル署名された、ユーザーの ID 情報を含む JWT ● refresh_token リフレッシュトークン要求のみ含まれる トークンレスポンス

Slide 40

Slide 40 text

40 id_token は JWT (JSON Web Token) という形式で、 ドットで区切られた 3 つのセクションで構成される 1. Header (ヘッダー) アルゴリズム(RS256 など)や、どの鍵で署名したか 2. Payload (ペイロード) 実際のユーザー情報 3. Signature (署名) このトークンが改ざんされていないことの証明 IDトークン (JWT) の構造

Slide 41

Slide 41 text

41 IDトークン (ペイロード) の構造 { "iss": "https://accounts.google.com", "aud": {client_id}, "sub": "{sub}", "iat": 1353601026, "exp": 1353604926, "nonce": {nonce} } ● iss ID プロバイダの URL ● aud リライング‧パーティのクライアント ID ● sub エンドユーザーの識別⼦ email ではなく sub でユーザーを識別すべし ● iat JWT の発⾏時間 ● exp ID トークンの有効期限 ● nonce 認証リクエスト時に送信した値

Slide 42

Slide 42 text

42 ● 署名の検証 IdP が公開している公開鍵 (JWK) を取得し、トークンが改ざんされていないか確認 ● 発⾏者の確認 iss の値が、信頼する IdP の URL と完全に⼀致するか確認 ● 対象者の確認 aud の値が、⾃分のアプリの Client ID と⼀致するか確認 ● 有効期限の確認 現在時刻が exp(有効期限)を過ぎていないか確認する。 ● 整合性の確認 認可リクエスト時に送信した nonce と、 ID トークン内の nonce クレームが完全に⼀致するか確認 ● 発⾏時刻の確認 iat が極端に古いものではないか、未来の時刻になっていないかを確認 IDトークンの検証

Slide 43

Slide 43 text

43 認可コードフローの流れ: ユーザー情報取得

Slide 44

Slide 44 text

44 ユーザー情報要求 GET /v1/userinfo HTTP/1.1 Host: openidconnect.googleapis.com Authorization: Bearer {access_token} ID トークンで賄える場合は使わなくても問題ない

Slide 45

Slide 45 text

45 詳細プロフィール提供 HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 { "sub": "{sub}", "name": "Jane Doe", "given_name": "Jane", "family_name": "Doe", "preferred_username": "j.doe", "email": "[email protected]", "email_verified": "true" } 指定した scope に対応したフィールドが返却される 詳細は OpenID Connect Core 1.0 incorporating errata set 2 参照

Slide 46

Slide 46 text

おまけ

Slide 47

Slide 47 text

  47 「この URL を⾒れば、その IdP のすべてのエンドポイント URL が書いてある」という 共通の場所が定義されている Google: https://accounts.google.com/.well-known/openid-configuration OpenID Connect Discovery { "issuer": "https://accounts.google.com", "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth", "token_endpoint": "https://oauth2.googleapis.com/token", "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo", "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs", "scopes_supported": ["openid", "email", "profile", ...], "response_types_supported": ["code", "token", "id_token", ...] }

Slide 48

Slide 48 text

  48 OAuth は「認可(権限)」の仕組みであり、「認証(本⼈確認)」を保証しない OIDC ではID トークンの中に、aud(クライアント ID)や nonce を含めることでリスクを防⽌ OAuth で認証を⾏うリスク ● アクセストークンを持っていること = 「本⼈」ではない アクセストークンを使って API でプロフィールが取れた場合でも、 「⽬の前のユーザーがログインした結果」であるという保証がない ● すり替え攻撃のリスク アクセストークンを別のアプリで発⾏したものとすり替えられたときに検証できない ● プロフィール取得が標準化されていない OAuthには標準化された、OpenID Connect Discovery や Claims が存在しない OAuth でも認証ができるのでは?

Slide 49

Slide 49 text

49 01 OAuth とは 02 OpenID Connect とは 03 Laravel での実装例 04 まとめ ⽬次

Slide 50

Slide 50 text

50 ● Laravel を使って Google アカウントで OIDC 認証 ● ⽣年⽉⽇を追加で取得する ● User モデルとは別に OAuthProviders モデルを⽤意 Laravel で実装例

Slide 51

Slide 51 text

51 Google の認可サーバーを使った認証の事前準備 1. Google Cloud で無料トライアルを申し込む 2. プロジェクトの作成(必要に応じて) 3. ブランディングを作成 4. クライアントを作成 5. テストユーザー設定 6. People API 有効化 Google Cloud 事前準備

Slide 52

Slide 52 text

52 Google Auth Platform → ブランディング アプリ名や連絡先の情報を⼊⼒ Google Cloud 事前準備: ブランディングを作成

Slide 53

Slide 53 text

53 Google Auth Platform → クライアント callback などの URI を設定 設定後にクライアントID、シークレットが表⽰ Google Cloud 事前準備: クライアントを作成

Slide 54

Slide 54 text

54 Google Auth Platform → 対象 審査を受けていないため、外部ユーザーは利⽤できない テストユーザーとして登録することで動作確認可能 Google Cloud 事前準備: テストユーザー設定

Slide 55

Slide 55 text

55 OAuth を 簡単に使える公式ライブラリ: https://github.com/laravel/socialite redirect は Google からの redirect(callback)を意味する Laravel Socialite の準備 composer require laravel/socialite # config/services.php 'google' => [ 'client_id' => env('GOOGLE_CLIENT_ID'), 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 'redirect' => env('GOOGLE_REDIRECT_URI'), ],

Slide 56

Slide 56 text

56 認可コード取得 フロー図

Slide 57

Slide 57 text

57 認可コード取得 画⾯

Slide 58

Slide 58 text

58 OAuthController (auth/redirect) 認可コード取得 実装 public function redirect() { return Socialite::driver('google') ->enablePKCE() ->scopes(['https://www.googleapis.com/auth/user.birthday.read']) ->with([ 'access_type' => 'offline', 'prompt' => 'consent', ]) ->redirect(); }

Slide 59

Slide 59 text

59 ● PKCE を有効にするため enablePKCE() を指定 ● access_type = offline, prompt = consent で refresh_token を取得 ● scope はデフォルトで openid, profile, email が設定されている ● cliend_id や redirect_uri, state 等のパラメーターは Socialite で指定している 認可コード取得 実装

Slide 60

Slide 60 text

60 トークン取得と検証 フロー図

Slide 61

Slide 61 text

61 OAuthController (auth/callback) トークン取得と検証 実装 public function callback(Request $request) { $googleUser = Socialite::driver('google') ->enablePKCE() ->user(); $googleSub = $googleUser->user['sub'] ?? null; $oauthProvider = OAuthProvider::where('provider', 'google') ->where('provider_user_id', $googleSub) ->first();

Slide 62

Slide 62 text

62 トークン取得と検証 実装 if ($oauthProvider) { $user = $oauthProvider->user; } else { $user = User::create([ 'email' => $googleUser->getEmail(), 'last_name' => $googleUser->user['family_name'] ?? '', 'first_name' => $googleUser->user['given_name'] ?? '', 'avatar_url' => $googleUser->user['picture'] ?? null, ]); $oauthProvider = OAuthProvider::create([ 'user_id' => $user->id, 'provider' => 'google', 'provider_user_id' => $googleSub, ]); }

Slide 63

Slide 63 text

63 ● PKCE を有効にするため enablePKCE() を指定 ● access_type = offline, prompt = consent で JWT に refresh_token が含まれる ● ユーザーは email でなく sub で特定 認可コード取得 実装

Slide 64

Slide 64 text

64 AbstractProvider トークン取得と検証 実装深堀り public function user() { if ($this->user) { return $this->user; } if ($this->hasInvalidState()) { throw new InvalidStateException; } $response = $this->getAccessTokenResponse($this->getCode()); $user = $this->getUserByToken(Arr::get($response, 'access_token')); return $this->userInstance($response, $user); }

Slide 65

Slide 65 text

65 トークンリクエストや ID トークンの検証を実施 トークンリクエスト JWT のデコードと署名検証 発⾏者(iss)の検証 オーディエンス(aud)の検証 公開鍵の取得や署名検証を内部で⾏ってくれているのが最⼤の良さ nonce 対応は独⾃で⾏う必要があるため注意 (今回は割愛) トークン取得と検証 実装深堀り Socialite::driver('google')->user() $this->getAccessTokenResponse($this->getCode()); # Laravel\Socialite\Two\GoogleProvider $this->getUserByToken(Arr::get($response, 'access_token'));

Slide 66

Slide 66 text

66 ユーザー情報取得 フロー図

Slide 67

Slide 67 text

67 OAuthController (auth/callback) ユーザー情報取得 実装 $response = Http::withToken($googleUser->token) ->get('https://people.googleapis.com/v1/people/me', [ 'personFields' => 'birthdays', ]); if ($response->successful()) { $data = $response->json(); if (isset($data['birthdays']) && is_array($data['birthdays']) && count($data['birthdays']) > 0) { $birthday = $data['birthdays'][0]['date'] ?? null; if ($birthday && isset($birthday['year'], $birthday['month'], $birthday['day'])) { $dateOfBirth = sprintf('%04d-%02d-%02d', $birthday['year'], $birthday['month'], $birthday['day']); } } }

Slide 68

Slide 68 text

68 ● profile: ⽒、名, プロフィール画像 ● email: email ● https://www.googleapis.com/auth/user.birthday.read: ⽣年⽉⽇ 結果

Slide 69

Slide 69 text

69 今回のサンプルは下記に⽤意 kyoshidaxx/laraoidc https://github.com/kyoshidaxx/laraoidc サンプルコード

Slide 70

Slide 70 text

70 01 OAuth とは 02 OpenID Connect とは 03 Laravel での実装例 04 まとめ ⽬次

Slide 71

Slide 71 text

71 ● 「権限の委譲(OAuth)」と「⾝元の証明(OIDC)」を混同しない ● 認可(OAuth)と認証(OIDC)、⽬的に合致した仕様を選択する ● 仕様(フロー)を理解することがセキュリティの根幹 まとめ

Slide 72

Slide 72 text

72 書籍 ● Auth屋 著『雰囲気で OAuth を使っているエンジニアが最新のベストプラク ティス OAuth2.1 を整理して学べる本』 ● Auth屋 著『OAuth、OAuth 認証、OpenID Connect の違いを整理して理解で きる本 [2024 改訂]』 技術解説‧リファレンス ● Auth Wiki - https://auth-wiki.logto.io/ja ● OpenID Connect Core 1.0 | OpenID Foundation - https://openid.net/specs/openid-connect-core-1_0.html 参考資料

Slide 73

Slide 73 text

73 実装ガイド ● OpenID Connect | Google Identity - https://developers.google.com/identity/openid-connect/openid-connect ?hl=ja ● OAuth 2.0 Policies | Google for Developers - https://developers.google.com/identity/protocols/oauth2/policies ● Laravel Socialite | Readouble - https://readouble.com/laravel/12.x/ja/socialite.html 参考資料

Slide 74

Slide 74 text

ご清聴ありがとうございました