LaravelでOkta(OIDC)と連携する方法

## この記事でわかること、できること
- LaravelアプリでOkta(OIDC)と連携する方法がわかる
- OktaでのSSO(シングルサインオン)、SLO(シングルログアウト)が実装できる
- Oktaに登録されているユーザー情報の取得が実装できる


## 概要
- LaravelにおけるOkta連携の実装手順をまとめます。
- 大きく分けると以下の手順になります。
  - （1）Oktaの管理アカウント作成
  - （2）OktaのGUIでアプリ設定を作成
  - （3）LaravelにOktaライブラリインストール
  - （4）LaravelでOkta連携のためのコード追記/修正
- 以下どちらでも大丈夫です。
  - 既存のLaravelアプリのOkta連携機能追加
  - 新規でLaravelアプリ構築してOkta連携機能追加


## 前提
- Oktaの認証プロトコルはOIDC(Open ID Connect)の前提で記載する（Okta自体はSAML等もサポートしているが本手順には載せない）
- Laravelのバージョンは10.x系を前提とするが、後述のライブラリがインストールできさえすればどのバージョンでも多分組み込み可能
- Laravelの環境構築が完了していて、画面表示ができる状態であること
- 既に他の認証機能(LaravelBreeze等)が組み込まれていても問題はないが、認証処理を記述する`Controller`と`routes/web.php`にOkta用の処理を記述するので、メソッド名やURLが被らないように注意すること
- `composer install`ができること（依存関係のエラー等が出る場合は先に解決すること）


# Okta
## （1）Oktaの管理アカウント作成
- https://developer.okta.com/signup/ からアカウントを作成する
- ![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/dd759eec-5418-f986-f799-e11e37916386.png)
- 商用で使用する場合は `Secure my employees, contractors, & partners` を選択する
- 開発者向けの無料枠を使う場合は `Okta Developer Edition Service` を選択する
  - 今回は無料枠を使うので `Okta Developer Edition Service` を選択する
  - 無料枠の制限
    - Okta内に設定できるアプリが5個まで（＝シングルサインオンで認証情報を共有可能なアプリが5個まで）
    - 月間アクティブユーザーが最大15,000人まで [(ソース)](https://www.okta.com/jp/press-room/press-releases/okta-developer-starter/)
- 作成が完了するとメールが届くので、リンクをクリックしてパスワードなどの初期設定をする


## （2）OktaのGUIでアプリ設定を作成
- 管理コンソールにログインする
- サイドバーの `Applications` -> `Applications` をクリックする
- ![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/7306543a-a520-fbc6-fbde-0e361d174377.png)
- `Create App Integration` をクリックする
- ![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/0cb38ebd-4e0e-c0d0-7e07-98a6b1bba7af.png)
- `Create a new app integration`のモーダルが表示されるので、
  - `Sign-in method`：`OIDC` 
  - `Application type`：`Web Application` 
  - を選択して `Next` をクリックする
  - ![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/98e78877-125a-bbf4-0cdf-53f715ea9806.png)
- `New Web App Integration`の画面に遷移するので以下を設定(後で変更可能)
  - `My Web App`：`任意のアプリ名`、
  - `Sign-in redirect URIs`：`http://localhost/authorization-code/callback` （後述のenvおよびルーティングと一致させる）
  - `Sign-out redirect URIs`：`http://localhost` （Oktaからログアウトした後に遷移させたいルートを記載）
  - `Controlled access`：`Allow everyone in your organization to access`
  - `Enable immediate access(Recommended)`：チェックを入れる
  - を入力して `Save` をクリックする
  - ![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/31924089-cf97-002c-fb1f-ace875c76594.png)
  - ![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/51ca757a-5df8-ebc1-6b58-0783a8109311.png)
- Okta内にアプリが作成される
- アプリ画面の`Client ID `と`CLIENT SECRETS`を控えておく（後述のenvに記載する）
- ![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/55a13ea5-b1a1-1c2c-5cce-fc8e6cd1246c.png)


# Laravel
- Oktaの開発者向けドキュメント https://developer.okta.com/blog/2019/09/05/laravel-authentication をベースに進める。(実装過程で過不足があったため以下に再現手順を記載する。)

## （3）LaravelにOktaライブラリインストール
  - ドキュメントに従ってターミナルで
  - `composer require laravel/socialite socialiteproviders/okta`
  - でSP(サービスプロバイダ)としてOktaと連携するためのライブラリをインストールする
  - [ライブラリのドキュメントはこちら](https://www.larablocks.com/package/socialiteproviders/okta)


## （4）LaravelでOkta連携のためのコード追記/修正
### .envにOktaの接続情報を記載する
```env
OKTA_BASE_URL= // OktaのBaseURL（例：https://dev-XXXXXXXX.okta.com）
OKTA_CLIENT_ID= // 上で控えたOktaのClient ID
OKTA_CLIENT_SECRET= // 上で控えたOktaのClient Secret
OKTA_REDIRECT_URI= // 上で設定したSign-in redirect URIs
```





### ドキュメントでスキップしても問題ない箇所
- 上記ドキュメントの「Before running the database migrations, you need to make some changes to the migrations and the User model」〜「Run the database migrations:」まではスキップしても問題なかったので実施しない
  - 「アカウントの検証/パスワードのリセットがアプリで処理されなくなることに対処するため」とあるので、不都合があれば対応を検討してください。


### config/app.php
- （ドキュメントそのまま。以下を追加）$providers配列に追加して、 config/app.php Socialiteプロバイダーを構成します。
```
$providers = [
    ...
    \SocialiteProviders\Manager\ServiceProvider::class,
    ...
]
```


### app/Providers/EventServiceProvider.php
- （ドキュメントそのまま。以下を追加）$listenの配列に以下を追加しますapp/Providers/EventServiceProvider.php。
```
protected $listen = [
    ...
    \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        'SocialiteProviders\\Okta\\OktaExtendSocialite@handle',
    ],
    ...
];
```


### config/services.php
- （ドキュメントそのまま。以下を追加）次に、更新しますconfig/services.php:
```
'okta' => [
    'client_id' => env('OKTA_CLIENT_ID'),
    'client_secret' => env('OKTA_CLIENT_SECRET'),
    'redirect' => env('OKTA_REDIRECT_URI'),
    'base_url' => env('OKTA_BASE_URL')
],
```


### routes/web.php
- （ここはドキュメントと違います）
- routes/web.php に任意のルーティングを追加する。
- ログイン、コールバック(Okta認証後にアプリに返ってきた時の処理)、ログアウトのルーティングを追加。
- 以下は例。
```
use App\Http\Controllers\Auth\LoginController;
Route::get('/', [LoginController::class, 'redirectToOkta'])->name('okta.login');
Route::get('/authorization-code/callback', [LoginController::class, 'handleCallbackFromOkta'])->name('okta.callback');
Route::get('/logout', [LoginController::class, 'logout'])->name('okta.logout');
```


### LoginController.php
- (ここもドキュメントと違います)
- ログイン処理を記述するコントローラーに以下のメソッドを追加する。
- `redirectToOkta()` と `logout()` は基本どのアプリでも以下の書き方で動作するはず。
- `handleCallbackFromOkta()` の記述がミソ。OktaのID+パスワードで認証した後にリダイレクトがここのルーティングに飛んでくるので、その時にユーザーを作成or更新してログインさせる処理を書く。ご自身のアプリ毎に異なります。
- 以下例では`$okta_user->user['preferred_username']`がユーザーを特定するキーになるので、それを使ってユーザーを作成or更新しています。
- キー以外にもOktaから取得したい属性がある場合は、`$okta_user->属性`等で取得して保存してください。
- `id_token` はOktaから返ってくるトークンで、ログアウト(SLO)の際に使うため保存しておく。
```
    public function redirectToOkta()
    {
        // Oktaのログイン画面にリダイレクトリクエストを投げる（既に認証済みなら自動ログインになる）
        return Socialite::driver('okta')->redirect();
    }

    public function handleCallbackFromOkta()
    {
        $okta_user = Socialite::driver('okta')->user();

        // Okta認証されたらユーザーを作成or更新（IDトークンだけ保存）
        $user = User::updateOrCreate([
            // Oktaのpreferred_username属性がユーザーを特定するキー
            'account_id' => $okta_user->user['preferred_username']
        ], [
            'id_token' => $okta_user->id_token,
        ]);

        Auth::login($user);

        // ダッシュボードにリダイレクト
        return redirect()->route('dashboard');
    }

    public function logout()
    {
        $user = Auth::user();

        $logout_url = Socialite::driver('okta')->getLogoutUrl($user->id_token, URL::to('/'));

        Auth::logout();

        return redirect($logout_url);
    }
```

### Seeder
- （ここはドキュメントに無いです）
- id_tokenをユーザーテーブルに追加する。
- `php artisan make:migration add_id_token_to_users_table --table=users` でマイグレーションファイルを作成する
- `$table->longText('id_token')->nullable();` を追加してマイグレーションを実行する
- ユーザーモデルにfillableがあれば`id_token`を追加する


### 動作確認
- うまく組み込めていれば、`http://localhost` にアクセスするとOktaのログイン画面にリダイレクトされる
- Oktaのユーザー(管理コンソールにログインしたユーザー)でログインすると、`http://localhost/authorization-code/callback` にリダイレクトされて、ログイン処理が走る
- ログイン処理が成功すると、`handleCallbackFromOkta()`の最後のリダイレクト先(上の例ではdashboard)に遷移する


# その他
## 【運用】Oktaにユーザーを追加する
- Oktaの管理コンソールにログインする
- サイドバーの `Directory` -> `People` をクリックする
- `Add Person` をクリックする
- ![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/7e6b34d8-4454-c0ee-e3cb-4a7a2699263b.png)
- 任意の情報を入力して `Save` をクリックする
- ![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/69200104-78f9-5ffc-763d-f31e8bd4c1c6.png)


## 【運用】Oktaからアプリに連携するユーザー属性を設定する
- Oktaに設定した属性（氏名、ニックネーム、メールアドレス、生年月日など）をLaravel側に渡すこともできる。
- Oktaの管理コンソールにログインする
- サイドバーの`Directory` -> `Profile Editor` をクリックする
- 対象のアプリを選択する
- ![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/6bef4544-d812-4394-9071-e8e706242b2c.png)
- `Mappings`をクリックする
- ![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/322b27c2-6831-c409-cfd4-454cf1a7dd5e.png)
- `Okta User to アプリ名`が選択されている状態で、任意のマッピングを行う。
  - 左がOkta上に設定している属性、右がLaravel側で`Socialite::driver('okta')->user();`で取得する項目名
  - Okta右側の`userName` だけ、Laravelでは`preferred_username` になるので注意
  - ![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639188/e1c46167-35fc-7479-b431-3e873beb4864.png)


## 備考
### グループ機能
- Oktaでグループを設定することもできるが、Laravelの上記ライブラリではグループ情報を取得する機能が無さそうだった。
- OktaのAPIキーを使って、OktaAPIを直接コールすれば取得できた。
- サイドバーの`Security` -> `API` -> `Tokens` でAPIキーを生成できる
- OktaAPIのドキュメント グループリスト取得：https://developer.okta.com/docs/api/openapi/okta-management/management/tag/User/#tag/User/operation/listUserGroups


## 参考文献
- OAuth OIDCの説明
  - [一番分かりやすい OAuth の説明](https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be)
  - [一番分かりやすい OpenID Connect の説明](https://qiita.com/TakahikoKawasaki/items/498ca08bbfcc341691fe)
- Laravelの実装
  - [認証を使用したシンプルな Laravel アプリを構築する](https://developer.okta.com/blog/2019/09/05/laravel-authentication)
  - [socialiteproviders/okta](https://www.larablocks.com/package/socialiteproviders/okta)
- Okta
  - [Okta Developer Edition Service](https://www.okta.com/jp/press-room/press-releases/okta-developer-starter/)
  - [Okta APIドキュメント](https://developer.okta.com/docs/api/)