KtorでOAuthプラグインを利用する方法
Ktorのv.2.0.0が正式にリリースされそうなので本格的に触ってみようと思い、このところKtor Serverの調査などをしていました。(実際、4/11にリリースされました🎉)
その際に、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で読み込んでいます。
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
必要なKtorプラグインの導入
まずは必要なKtorプラグインを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を利用しています。
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
で実装したリダイレクト先を新しく追加しています。
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
を新しく読み込みます。
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という仕組みを使う方が良さそうなので追記させていただきます。
そこで今後、もう少し実践的なアプリの実装することでログイン周りの実装を更に詰めていきたいと思っています。
参考
ちなみに自分が作成した実装サンプルも一応参考までに置いておきます。
Discussion
こちらの目的であれば、GoogleはOAuth 2.0にユーザー情報などを受け渡しする仕組みを追加したOpenID Connect(≠OAuth認証)という仕様に沿った仕組みが提供されていますのでそれを利用すべきです。
参考URL : https://developers.google.com/identity/protocols/oauth2/openid-connect
今回の記事の内容では
と言う意味合いになっているかと思いますが、ID連携のベストプラクティスとしては
と言うものです。
上記Googleのドキュメントにもこの辺りの流れが記載されていると思いますので、ぜひお時間のあるときにご覧いただいて今後の参考にしていただければと思います。
ご指摘ありがとうございます!
一応、今回の記事の内容としては
というよりはKtorのOAuthプラグインの使い方を解説し、セッションに保存したアクセストークンはデータが取れていることの確認とその後の表示の確認くらいの認識の実装で済ましてしまっていたので、「OAuth認証」というタイトルをつけてしまったことなども良くなかったかもです💦
(「KtorでOAuthプラグインを利用する方法」が正しいかもしれないと考え中です)
そのため、今後、記事を読まれる方が誤解しないようタイトルや記述の一部を修正しようと思います。
また差し支えなければ、ritou様のお名前とOpenID Connectについてのご指摘を「さいごに」に付け加えさせていただきたいと思っています。
また教えていただいたOpenID Connectの実装は、今後のログインの実装の際に参考にさせていただきます!
自分としても間違った情報を拡散してしまうことは本意ではないので、今後とも間違いなどありましたら、ご指摘いただけるとありがたいです!
修正をさせていただきました。
もし気になる箇所がありましたら、ご指摘いただけると幸いです。