Kotlin
oauth2
Ktor

KtorでOAuth2認証をしてみる

More than 1 year has passed since last update.

だいぶ遅くなってしまいましたが、この記事はリクルートライフスタイル Advent Calendar 2017の2日目の記事です。

はじめに

Ktorとは

Ktor

KtorとはJetBrains社が提供する、kotlinのwebフレームワークです。
"Easy to use, fun and asynchronous."を標榜しており、とても軽量で簡単に使えるのが特徴です。

この記事を書こうと思った11月末にはQiitaにktorのtagすら無い状態だったのですが、モタモタしている内にいくつかQiitaにも記事が投稿されていました。

kotlinをサポートしたWeb FrameworkといえばSpring Framwork 5.0が有名ですが、kotlinの提供元であるJetBrains製ということでKtorは安心感も高く、シンプルなものを作る時には選択肢になりうるのかなと思っております。

本記事の目的・モチベーション

公式ページでも説明が色々されているのですが、まだ未整備の部分も多く認証に関するページもまだだったため、自分で試してみたいと思いたち、Githubの該当部分の実装sampleを色々調べたため記事としてまとめました。
Ktor authentication page screenshot

今回は、GitHubアカウントでOAuth2認証してログインし、ユーザ情報を表示するだけの簡単なアプリをサンプルとして作りました。
記事内のソースコードはGitHubにあげてあります。
サンプル実装のgif

なおkotlin, Ktorの使用したバージョンは下記となっております。

  • kotlin: 1.2.0
  • Ktor: 0.9.0

Feature

今回使用する主要なFeatureのそれぞれの簡単な説明です。

  • Routing
  • Sessions
  • Authentication

まずは、使用するFeatureをinstallします。

App.kt
class App {
    fun Application.install() {
        install(Sessions)
        install(Routing)
    }
}

Applicationに直接 Authenticationをinstallしていないかというと、特定のパスでのみOAuth2認証をするようにするためです。
詳しくは下記で説明します。

Routing

まずは、Routingの設定をおこないます。
このFeatureはその名の通りrequetのルーティングを管理するFeatureです。

こちらは本記事では詳しくは触れませんが、Routingをタイプセーフに行うためにLocation Featureもinstallし、各エンドポイントをclassとして宣言します。

それを用いて、Routeの拡張関数として各エンドポイント毎の設定をすると、以下のようになります。
これでルーティングの設定ができている状態となります。

App.kt
@location("/") class Index()
@location("/login") class Login()
@location("/login/github") class LoginWithGitHub()

fun Route.index() {
    get<Index> {
        call.respondText { "TOPページ" }
    }
}

fun Route.login() {
    get<Login> {
        call.respondText { "ログインページ" }
    }
}

fun Route.loginWithGitHub() {
    get<LoginWithGitHub> {
        call.respondText { "ここからGitHubの認証ページにリダイレクトします" }
    }
}

class App {
    fun Application.install() {
        install(Sessions)
        install(Locations)
        install(Routing) {
            index()
            login()
            loginWithGitHub()
        }
    }
}

Sessions

このFeatureを使うことでSessionへの値の保存・読み出しが可能となります。
値をCookieで渡すか、session idを発行するかセッションのストア先はどうするかなどを設定できます。
今回は、Cookieにsession idを発行して渡しインメモリで保存するように設定しました。

App.kt
install(Sessions) {
    cookie<GitHubSession>("GitHubSession", SessionStorageMemory())
}

またここではSessionに保存する型も指定しています。
今回はaccess_tokenだけを持つようにしています。

App.kt
data class GitHubSession(val accessToken: String)

Authentication

やっと本題の認証の設定についてです。

  • 認証済みであれば、Indexページへリダイレクト
  • 未認証であれば、Loginページへリダイレクト
  • Loginページ内の「GitHubでログイン」ボタンを押すとLoginWithGitHubページへ遷移、OAuth2認証開始

という動きにしたいと思います。

Sessionから認証情報を読み出す。

まずは、Sessionから認証情報を読み出して判定するように、Routeの各拡張関数を修正します。

App.kt
fun Route.index() {
    get<Index> {
        if (call.sessions.get<GitHubSession>() != null) {
            call.respondText { "TOPページ" }
        } else {
            call.respondRedirect(application.locations.href(Login()))
        }
    }
}

fun Route.login() {
    get<Login> {
        if (call.sessions.get<GitHubSession>() != null) {
            call.respondRedirect(application.locations.href(Index()))
        } else {
            call.respondHtml {
                head { title { +"Login" } }
                body { a(href = locations.href(LoginWithGitHub())) { +"GitHubでログイン" } }
            }
        }
    }
}

このように、PipelineContext<*, ApplicationCall>内でcall.sessionsとすることでSessionへとアクセスできます。

OAuth2認証設定

次に、LoginWithGitHubページでOAuth2認証を行うための設定をします。

サービスプロバイダや登録したクライアントの情報

まずはサービスプロバイダであるGithubの情報と、そこへ登録したサンプルアプリの情報OAuth2ServerSettingsとして宣言します。

App.kt
// OAuth2の設定
private val gitHubOAuth2Settings = OAuthServerSettings.OAuth2ServerSettings(
        name = "github",
        authorizeUrl = "https://github.com/login/oauth/authorize",
        accessTokenUrl = "https://github.com/login/oauth/access_token",
        clientId = "xxxx",
        clientSecret = "xxxx",
        defaultScopes = listOf("read:user")
)

OAuth2認証のinstall

RouteAuthenticationをinstallすることで、該当のパスで認証のチェックが行われます。
Authenticationの詳細として、先ほど宣言したOAuth2ServerSettingsを用いてOAuth2認証をおこないます。
oauthAtLocation関数自体は標準のAPIで、OAuth1用の設定を渡せばOAuth1認証をおこなってくれます。
また、GitHubとのHTTP通信にHttpClientが必要になるためメソッド引数として受け取っています。

App.kt
private val exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 4)

fun Route.loginWithGitHub(client: HttpClient) {
    location<LoginWithGitHub> {
        // Authentication Featureをinstall
        install(Authentication) {
            // OAuth2認証
            oauthAtLocation<LoginWithGitHub>(
                    client,
                    exec.asCoroutineDispatcher(),
                    providerLookup = { gitHubOAuth2Settings },
                    urlProvider = { location, _ -> "http://localhost:8080${application.locations.href(location)}" })
        }

        // 認証後(OAuth2フローでcallbackとして戻ってきてから)の処理)
        handle { TODO("認証後の処理") }
    }
}

また、今までget<LoginWithGitHub>となっていた部分がlocation<LoginWithGitHub>となっていることに気づいたかもしれませんが、実は元々のget関数はlocation関数のラッパーなのですが、これを使うとRouteに認証処理を挟み込めないためlocation関数を使うように変更しています。

Location.kt
inline fun <reified T : Any> Route.get(noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit): Route {
    return location(T::class) {
        method(HttpMethod.Get) {
            handle(body)
        }
    }
}

認証後の処理

先ほど省略していた認証後の処理の中身ですが、取得した認証情報をSessionに格納してTOPページへリダイレクトします。
取得した認証情報はcall.principal<OAuthAccessTokenResponse.OAuth2>()で取得できます。

App.kt
        // 認証後(OAuth2フローでcallbackとして戻ってきてから)の処理)
        handle {
            val principal = call.principal<OAuthAccessTokenResponse.OAuth2>()
            if (principal != null) {
                // セッションへ認証情報を格納
                call.sessions.set(GitHubSession(principal.accessToken))
                call.respondRedirect(application.locations.href(Index()))
            } else {
                call.respondText { "Login Error" }
            }
        }

これでOAuth2を用いた認証の一連のフローが完成しました!!
Pipeline/ApplicationCall/PipelineContext周りの動きを理解するのが最初は大変でしたが、
とてもシンプルに書けて楽しいので、もう少しKtorを色々触ってみたいと思いました。

全体のコードが気になる方は、GitHubをご覧ください!