🥙

Ktor で JWT の認証を行う

2020/11/02に公開

日本語の記事がなさそうだったので Ktor 1.4.0 を使って JWT 認証を行う実装を記事にしたいと思います!
調べながらだったので、認識が異なる箇所がありましたらご指摘いただけますと幸いです。
ソースコードは実際に私が作ったコードから必要箇所を抜粋しただけなので、動作しないなどもありましたらご指摘ください<(_ _)>

参考

https://jp.ktor.work/servers/features/authentication/jwt.html
実際動かしながら「こういうことか!」と理解した内容になります。

処理内容

今回は Ktor で JWT を使うことを目的として、次のような簡素なものにしたいと思います。

エンドポイント

post /user/auth

認証して token を返す

request

内容 説明
email test@example.com Email
pass test3r2trnmthjtyjr パスワード
http://127.0.0.1:8080/user/auth
-d '{ "email":"test@example.com", "pass":"test" }'

response

内容 説明
access_token token68 有効期限 1時間の Bearer トークン
{
  "access_token" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0IiwiYXVkIjoic2hhcmVib29rbWFya3MiLCJleHAiOjE1OTg3NTg0NDd9.C-Irgo599czcbemvJ0yKywL6M8w3oh2VI0Cgala-wQA"
}

get /user/id

token を元にユーザーの ID を返す

request

内容 説明
Authorization Bearer ヘッダに設定
http://127.0.0.1:8080/user/id
-H 'Authorization: Bearer XXXX'

response

内容 説明
id 10 ユーザーの ID
{ "id":10 }

実装

バージョン

以下のバージョンを利用しています。

  • Kotlin 1.4.10
  • Ktor 1.4.0

build.gradle

必要箇所だけを抜粋したつもりです。
ktor-authktor-auth-jwt を追加するイメージです。

buildscript {
    repositories {
        jcenter()
    }
    
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin'
apply plugin: 'application'

group 'group'
version '1.0.0'
mainClassName = "group.ApplicationKt"

sourceSets {
    main.kotlin.srcDirs = main.java.srcDirs = ['src']
    test.kotlin.srcDirs = test.java.srcDirs = ['test']
    main.resources.srcDirs = ['resources']
    test.resources.srcDirs = ['testresources']
}

repositories {
    mavenLocal()
    jcenter()
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation "io.ktor:ktor-server-netty:$ktor_version"
    implementation "io.ktor:ktor-locations:$ktor_version"
    implementation "io.ktor:ktor-jackson:$ktor_version"
    implementation "io.ktor:ktor-auth:$ktor_version"
    implementation "io.ktor:ktor-auth-jwt:$ktor_version"
    implementation "ch.qos.logback:logback-classic:$logback_version"
    implementation "org.koin:koin-ktor:$koin_version"

    testImplementation "io.ktor:ktor-server-tests:$ktor_version"
}

kotlin.experimental.coroutines = 'enable'

task stage {
    dependsOn assemble
}

Application.kt

最終的には定数や Router など各ファイルに分かれると思いますが、1ファイルで全体を見通せた方が分かりやすいかと思ったので 1ファイルにまとめています。

val algorithm = Algorithm.HMAC256("secret 的なもの")
val issuer = "ISSUER"
val audience = "audience"
val userId = "user_id"

fun Application.module() {
  install(Authentication) {
    jwt {
      realm = javaClass.packageName
      verifier(
        JWT.require(algorithm)
          .withAudience(audience)
          .withIssuer(issuer)
          .build()
      )
      
      validate {
        it.payload.getClaim(userId).let { claim ->
          if (!claim.isNull) {
            AuthUser(claim.asInt())
          } else {
            null
          }
        }
      }
    }
  }

  routing {
    route("/user") {
      post("/auth") {
        call.receive<UserRequest>().let {
          call.respond(transaction {
            
            // DB の ID と pass を確認
    	    val id = 10 // と取れたと仮定
            
            AuthResponse(
              JWT.create()
                .withAudience(audience)
		.withExpiresAt(Date.from(LocalDateTime.now().plusHours(1).toInstant(ZoneOffset.UTC))) // 有効期限
                .withClaim(userId, id)
                .withIssuer(issuer)
                .sign(algorithm)
            )
          })
        }
      }
      
      authenticate {
        get("/id") {
          call.respond(UserResponse(call.principal<AuthUser>()!!.id))
        }
      }
    }

  }
}

data class AuthUser(val id: Int): Principal

data class UserRequest(
  val email: String,
  val pass: String
)

data class AuthResponse(
  @JsonProperty("access_token") val accessToken: String
)

data class UserResponse(
  val id: Int
)

fun main(args: Array<String>) {
  embeddedServer(Netty, commandLineEnvironment(args)).start()
}

ポイント

Application.kt の中で、個人的にここを抑えておけばよさそう…と思うポイントを上げていきます。

install(Authentication) {

Ktor らしい「認証機能を使うよ」としている箇所です。
その中で jwt をどう扱うかを設定してあげます。
ktor-auth を dependencies に含めてあげることで扱えるようになります。
今回は jwt なので、その中で必要情報を設定していきます。

validate {

認証(ヘッダに設定した情報を解析)した結果はこの validate 内で payload から取得できます。
ここではその結果を Principal型に変換して返してあげることで router内の call.principal で取得できるようになります。
参考ページにもあるように、実際は audience 等もチェックした方がよいかと思います。

authenticate {

この中に設定された router では、上記の Principal型に変換された値を call.principal にて取得できます。
/user/auth のように認証しなくてよいページは authenticate の外側に配置することで処理を通らなくなります。

data class AuthUser(val id: Int): Principal

上記で出てきている Principal型 です。
継承することで利用できるようになります。

全体を通して…

全体のイメージは掴めたでしょうか?
validateauthenticate のあたりの動きが掴めるとすぐに理解できるかと思います。
以前は intercept を使い自身で認証機能を割り込ませる必要がありましたが、機能が提供されたことで簡単に利用できるようになっています。
また、今回は JWT ですが OAuth なども同様に実装者が内部を意識することなく手軽に使えそうに感じました!

Ktor について

PHP の Slim や Ruby の Sinatra といったような軽量系が好きな私としては、とても使いやすい FW に感じています!
ちょっとした API サーバーなどにいかがでしょうか??
Springboot もですが、Kotlin でサーバーが書けますよ〜

Discussion