🧪

KtorでOAuthプラグインを利用する方法

2022/04/18に公開
3

Ktorのv.2.0.0が正式にリリースされそうなので本格的に触ってみようと思い、このところKtor Serverの調査などをしていました。(実際、4/11にリリースされました🎉)

https://zenn.dev/yamachoo/scraps/4a1f8177812968

https://blog.jetbrains.com/ktor/2022/04/11/ktor-2-0-released/

その際に、KtorのOAuth周りは調べただけではイメージがわかなかったので、一度手を動かして実装してみました。
そして実装してみたところ、意外と詰まる部分があったので自分の備忘録も兼ねて記事にしました。

前提

記事内に出てこない開発環境は以下の通りです。

Gradle:7.1.1
Kotlin:1.6.20
JVM:17.0.1 (Amazon.com Inc. 17.0.1+12-LTS)
Ktor:2.0.0

ちなみにKtorはKtor Project Generatorでプラグインの追加なしで作成している前提になります。

OAuthの下準備

今回の実装ではGoogleのOAuthを利用するので、事前にGoogle Cloud Platformから「OAuth 同意画面」と「認証情報」を設定し、クライアントIDとクライアントシークレットを取得しておきます。

また自分の場合は.envを作成し、環境変数にクライアントIDとクライアントシークレットを設定して、direnvで読み込んでいます。

.env
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

必要なKtorプラグインの導入

まずは必要なKtorプラグインをbuild.gradle.ktsに追加します。

build.gradle.kts
dependencies {
    implementation("io.ktor:ktor-server-auth:$ktor_version")
    implementation("io.ktor:ktor-server-sessions:$ktor_version")
    implementation("io.ktor:ktor-client-core:$ktor_version")
    implementation("io.ktor:ktor-client-apache:$ktor_version")
}

GoogleのOAuth 2.0の利用

まずは追加でplugins配下にSecurity.ktを作成します。
以下のようにコードを記述することで、GoogleのOAuth 2.0を利用しています。

plugins/Security.kt
fun Application.configureSecurity() {
    install(Sessions) {
        cookie<UserSession>("user_session")
    }

    install(Authentication) {
        oauth("auth-oauth-google") {
            urlProvider = { "http://localhost:8080/callback" }
            providerLookup = {
                OAuthServerSettings.OAuth2ServerSettings(
                    name = "google",
                    authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
                    accessTokenUrl = "https://accounts.google.com/o/oauth2/token",
                    requestMethod = HttpMethod.Post,
                    clientId = System.getenv("GOOGLE_CLIENT_ID"),
                    clientSecret = System.getenv("GOOGLE_CLIENT_SECRET"),
                    defaultScopes = listOf("https://www.googleapis.com/auth/userinfo.profile")
                )
            }
            client = HttpClient(Apache)
        }
    }

    routing {
        authenticate("auth-oauth-google") {
            get("/login") {}

            get("/callback") {
                val principal: OAuthAccessTokenResponse.OAuth2? = call.authentication.principal()
                call.sessions.set(UserSession(principal?.accessToken.toString()))
                call.respondRedirect("/hello")
            }
        }
    }
}

data class UserSession(val accessToken: String)

しかし全体のコードだけでは理解がしにくいので、3つの重要な箇所に分けて説明をします。

OAuthの実装

まずはOAuthをプラグインを用いて実装している部分になります。

install(Authentication)はAuthenticationプラグインの読み込み、oauth("auth-oauth-google")ではauth-oauth-googleという名前のOAuthプロバイダーを作成しています。

oauthの中身のurlProviderプロパティでは、承認が完了したあとにリダイレクトさせる遷移先のURLを、providerLookupプロパティでは、必要なプロバイダーのOAuth設定を指定しています。そしてclientプロパティでは、OAuthサーバーにリクエストを送信するために使用するHttpClientを指定しています。

install(Authentication) {
    oauth("auth-oauth-google") {
        urlProvider = { "http://localhost:8080/callback" }
        providerLookup = {
            OAuthServerSettings.OAuth2ServerSettings(
                name = "google",
                authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
                accessTokenUrl = "https://accounts.google.com/o/oauth2/token",
                requestMethod = HttpMethod.Post,
                clientId = System.getenv("GOOGLE_CLIENT_ID"),
                clientSecret = System.getenv("GOOGLE_CLIENT_SECRET"),
                defaultScopes = listOf("https://www.googleapis.com/auth/userinfo.profile")
            )
        }
        client = HttpClient(Apache)
    }
}

セッションの実装

次にセッションの設定をしている部分を説明します。

この部分では、data classを使いUserSessionというセッションを定義し、セッションをCookieにuser_sessionという名前で保存しています。
これによりcall.sessions.set()call.sessions.get<UserSession>()を使うことができるようになります。

fun Application.configureSecurity() {
    install(Sessions) {
        cookie<UserSession>("user_session")
    }

    ... // 省略
}

data class UserSession(val accessToken: String)

ルーティングの実装

最後はroutingで、この部分で実際のルーティングを行っています。

/loginにアクセスするとOAuthが行われ、認証に成功すると/callbackにリダイレクトされ続きの処理が行われます。
今回は/callbackで認証の情報を取り出し、セッションにaccessTokenを保存してから/helloへリダイレクトしています。
(※この実装は確認のための仮実装のため、本番ではセッションに直接accessTokenを保存しないでください。)

またget("/login") {}は若干奇妙ですが、実はauthenticate("auth-oauth-google")が自動でurlProviderにリダイレクトしてくれるため空でも動きます。

routing {
    authenticate("auth-oauth-google") {
        get("/login") {}

        get("/callback") {
            val principal: OAuthAccessTokenResponse.OAuth2? = call.authentication.principal()
            call.sessions.set(UserSession(principal?.accessToken.toString()))
            call.respondRedirect("/hello")
        }
    }
}

その他の実装と起動

Routing.ktにはSecurity.ktで実装したリダイレクト先を新しく追加しています。

plugins/Routing.kt
fun Application.configureRouting() {
    routing {
        get("/") {
            call.respondText("Hello World!")
        }
+
+       get("/hello") {
+           call.respondText("Hello World!\n${call.sessions.get<UserSession>()?.accessToken}")
+       }
    }
}

またApplication.ktでは、Security.ktで作成したconfigureSecurityを新しく読み込みます。

Application.kt
fun main(args: Array<String>): Unit =
    io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused") // application.conf references the main function. This annotation prevents the IDE from marking it as unused.
fun Application.module() {
    configureRouting()
+   configureSecurity()
}

あとは./gradlew runで起動し、http://localhost:8080/loginにアクセスすることで、OAuthを利用できることが確認できます。
認証に成功すると、http://localhost:8080/helloにリダイレクトされ、Hello World!と自分のaccessTokenを確認できるはずです。

さいごに

これで一通りのGoogleのOAuth 2.0のみの利用の確認ができました。
しかし、本来であればユーザー登録のための実装や複数OAuthのためのルーティング設計などもしなくてはいけません。

また、ritou様からご指摘をいただいた通り、OAuthを使ったログイン認証を実装する際にはOpenID Connectという仕組みを使う方が良さそうなので追記させていただきます。

https://developers.google.com/identity/protocols/oauth2/openid-connect

そこで今後、もう少し実践的なアプリの実装することでログイン周りの実装を更に詰めていきたいと思っています。

参考

https://ktor.io/docs/oauth.html

https://github.com/ktorio/ktor-documentation/tree/main/codeSnippets/snippets/auth-oauth-google

ちなみに自分が作成した実装サンプルも一応参考までに置いておきます。

https://github.com/yamachoo/ktor-oauth-sample

Discussion

ritouritou

こちらの目的であれば、GoogleはOAuth 2.0にユーザー情報などを受け渡しする仕組みを追加したOpenID Connect(≠OAuth認証)という仕様に沿った仕組みが提供されていますのでそれを利用すべきです。

参考URL : https://developers.google.com/identity/protocols/oauth2/openid-connect

今回の記事の内容では

  • セッションにアクセストークンを保存
  • 今後はそのセッションに含まれたアクセストークンから対象ユーザーを把握できる

と言う意味合いになっているかと思いますが、ID連携のベストプラクティスとしては

  • callback時にGoogleからユーザー情報を取得する
  • そのユーザー情報のユーザー識別子を用いてアプリケーション内部のユーザーDBなどを参照し、紐付けがあるユーザーのログインセッションを発行する。場合によってはアクセストークンを内部のユーザーIDと紐付けて保存しておく
  • その後はアプリケーション内のセッション機構を利用する

と言うものです。
上記Googleのドキュメントにもこの辺りの流れが記載されていると思いますので、ぜひお時間のあるときにご覧いただいて今後の参考にしていただければと思います。

ヤマチューヤマチュー

ご指摘ありがとうございます!

一応、今回の記事の内容としては

  • セッションにアクセストークンを保存
  • 今後はそのセッションに含まれたアクセストークンから対象ユーザーを把握できる

というよりはKtorのOAuthプラグインの使い方を解説し、セッションに保存したアクセストークンはデータが取れていることの確認とその後の表示の確認くらいの認識の実装で済ましてしまっていたので、「OAuth認証」というタイトルをつけてしまったことなども良くなかったかもです💦
(「KtorでOAuthプラグインを利用する方法」が正しいかもしれないと考え中です)

そのため、今後、記事を読まれる方が誤解しないようタイトルや記述の一部を修正しようと思います。
また差し支えなければ、ritou様のお名前とOpenID Connectについてのご指摘を「さいごに」に付け加えさせていただきたいと思っています。

また教えていただいたOpenID Connectの実装は、今後のログインの実装の際に参考にさせていただきます!

自分としても間違った情報を拡散してしまうことは本意ではないので、今後とも間違いなどありましたら、ご指摘いただけるとありがたいです!

ヤマチューヤマチュー

修正をさせていただきました。
もし気になる箇所がありましたら、ご指摘いただけると幸いです。