PlayFramework + OneloginでOIDC認証

この記事はウェブクルー Advent Calendar 2023 16日目の記事です。
昨日は、[@kouki_kubota](https://qiita.com/kouki_kubota) さんの「関数型と単体テストの相性がとても良き」でした。

# はじめに
OneloginのOIDC認証を利用し、PlayFramework（Scala）で作成したアプリケーションへのログインを行う手順です。
今回はPlayFramework用のセキュリティライブラリである[play-pac4j](https://github.com/pac4j/play-pac4j)を使用して実装しました。

### OIDC（OpenID Connect）とは
サービス間で、利用者の同意に基づきID情報を流通するための標準仕様。
OIDCの認証フローについては下記の記事が参考になりました。

https://qiita.com/TakahikoKawasaki/items/4ee9b55db9f7ef352b47

### Oneloginとは
OneLogin社が提供するクラウドサービスにおけるID管理、アクセス制限などを提供するIDaaSサービス。

# 事前準備
- Oneloginの管理者権限のアカウントを用意する。
無ければ下記のサイトから開発者用ドメインが作成できる。（無料）
※ただし、1社（社用メールのドメイン）に対して1つしか作成できない模様。

https://www.onelogin.com/register/142498

- PlayFramework + Scalaでアプリケーションを作成し、ローカル環境で動作する状態にしておく。（`http://localhost:9000/`でアクセス可能）
※今回は下記のバージョンで作成したアプリケーションを使用
　・Scala：2.13.2
　・PlayFramework：2.8.0
　・sbt：1.3.10


# Oneloginの設定
1. 管理者権限のアカウントでOneloginにログインする。

2. ヘッダーの[Administration] ＞ [Applications]タブからApplications画面に遷移し、 [Add App]ボタンクリックでアプリケーションの新規作成に進む。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620288/6b0727c9-b0a1-31f0-6b6f-92ca02171220.png)

3. Find Applications画面の検索窓に"openid"と入力し、検索結果の[OpenId Connect(OIDC)]を選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620288/209e11cc-1c0c-e952-af98-6b1cfb892de8.png)


4. Add OpenId Connect(OIDC)画面でアプリ名（任意）を入力し、[Save]ボタンをクリックする。
※今回は、Oneloginのメニュー画面には表示しないので、[Visible in portal]のチェックは外す。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620288/ecfae200-26c4-6983-32b3-6ba34dbe3bbe.png)

5. 右側のメニューで[Configuration]を選択し、[Redirect URIs]にアプリケーションのコールバック用URLを入力して[Save]ボタンをクリックする。
   - Redirect URIs：`http://localhost:9000/callback?client_name=OidcClient`

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620288/aaa34f39-7eca-a896-bfa5-bae33416ee90.png)


6. 右側のメニューで[SSO]を選択し、下記の値を控えておく。
   - Client ID
   - Client Secret
   - Issuer URL

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620288/3dce81e0-be40-9695-86c5-c88e701f2948.png)

7. アプリケーションにユーザーを追加する。
・[Users]タブを選択し、ユーザー一覧から自分のアカウントを選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620288/19c8f753-0e98-a918-54f1-b276e0e2dc2e.png)

    ・右側のメニューで[Applications]を選択し、[+]ボタンをクリックする。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620288/fa97ed15-f20b-4f61-e150-f6314a4cc247.png)
    ・モーダルで作成したアプリケーションを選択後[Continue]ボタンをクリック⇒次のモーダルで[Save]ボタンをクリックする。


# アプリケーションの設定
1. build.sbtに`play-pac4j`を追加する。
※`ehcache`は後述の`PlayCacheSessionStore`を使用するために必要
    ```build.sbt
    libraryDependencies ++= Seq(
        ehcache,
        "org.pac4j" %% "play-pac4j" % "11.1.0-PLAY2.8",
        "org.pac4j" % "pac4j-oidc" % "5.7.0"  excludeAll(ExclusionRule("commons-io" , "commons-io"), ExclusionRule(organization = "com.fasterxml.jackson.core")),
    ```


2. `routes`ファイルにOIDC用のURLを追加する。
    ```conf/routes
    GET     /login                      controllers.OidcController.login
    GET     /oidcIndex                  controllers.OidcController.oidcIndex
    GET     /callback                   @org.pac4j.play.CallbackController.callback(request: Request)
    ```

3. `application.conf`にOIDC用の設定値を追加する。 
clientId、secret、discoveryURIに記載する値は`Oneloginの設定＞6.`で控えた値
    - clientId：`Client ID`
    - secret：`Client Secret`
    - discoveryURI：`Issuer URL`

    ```conf/application.conf
    play.modules.enabled += "modules.SecurityModule"
    baseUrl="http://localhost:9000"
    oneLogin {
        clientId="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        secret="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        discoveryURI="https://xxxxxxxx.onelogin.com/oidc/2/.well-known/openid-configuration"
    }
    ```

4. `SecurityModule`を作成する。
    ```app/models/SecurityModule.scala
    class SecurityModule(environment: Environment, configuration: Configuration) extends AbstractModule {
      override def configure(): Unit = {
        bind(classOf[SessionStore]).to(classOf[PlayCacheSessionStore])
        bind(classOf[SecurityComponents]).to(classOf[DefaultSecurityComponents])
    
        // callback
        val callbackController = new CallbackController()
        callbackController.setDefaultUrl("/") // default url after sign out
        bind(classOf[CallbackController]).toInstance(callbackController)
      }
    
      @Provides
      def provideOidcClient: OidcClient = {
        val clientId = configuration.get[String]("oneLogin.clientId")
        val secret = configuration.get[String]("oneLogin.secret")
        val discoveryURI = configuration.get[String]("oneLogin.discoveryURI")
    
        val oidcConfiguration = new OidcConfiguration()
        oidcConfiguration.setClientId(clientId)
        oidcConfiguration.setSecret(secret)
        oidcConfiguration.setDiscoveryURI(discoveryURI)
        val oidcClient = new OidcClient(oidcConfiguration)
        oidcClient
      }
    
      @Provides
      def provideConfig(oidcClient: OidcClient): Config = {
        val baseUrl = configuration.get[String]("baseUrl")
        val clients = new Clients(baseUrl + "/callback", oidcClient)
        val config = new Config(clients)
        config
      }
    }

    }
    ```

5. controllerを作成する。
    ```app/controllers/OidcController.scala
    class OidcController @Inject()(val controllerComponents: SecurityComponents, implicit val pac4jTemplateHelper: Pac4jScalaTemplateHelper[UserProfile]) extends Security[UserProfile] {
    
      def login() = Action { request =>
        Ok(views.html.login())
      }
    
      def oidcIndex() = Secure("OidcClient") { implicit request =>
        // profilesからユーザー情報を取得する
        val username = profiles.apply(0).getAttribute("preferred_username")
        Ok(views.html.oidcIndex(username.toString))
      }
    }
    ```
    
6. viewを作成する。
    ```app/views/login.scala.html
    @()
    
      <h1>ログイン</h1>
      <a href="@routes.OidcController.oidcIndex">Oneloginでログイン</a>
    ```

    ```app/views/oidcIndex.scala.html
    @(userName: String)
    
      <h1>ログインしました！</h1>
      <h4>@userName</h4>
    ```

# 動作確認
1. Onloginでログインしておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620288/efde9d48-2502-da46-a42d-a473e1bdd614.png) 


2. ブラウザで`http://localhost:9000/login`にアクセスする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620288/b7087700-af8e-97fc-9bcd-83a9fd9ff040.png)


3. [Oneloginでログイン]リンクをクリックすると、
OIDC認証が走り、成功すると1.でログインしたユーザー名が表示される！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620288/cb694201-4241-507e-b3b6-1d9807fd7b79.png)

※Oneloginでログインしていない状態で2.をやると、Oneloginのログイン画面にリダイレクトされ、ログインが成功すると3.の画面にリダイレクトされる。

# おわりに
今回はログイン機能のみ実装しましたが、pac4jを使えば認証エラー時の処理やログアウト機能についても簡単に実装出来そうです。

明日は、[@Hideto-Kiyoshima-wc](https://qiita.com/Hideto-Kiyoshima-wc) さんの投稿になります。

# 参考文献
- [play-pac4j-scala-demo](https://github.com/pac4j/play-pac4j-scala-demo)
- [SilhouetteからPAC4Jに乗り換えてついでにOpenID Connectを使う](https://qiita.com/ocome/items/99393a30bcc40871bb50)