Skip to content
Server Plugin

JSON Web Tokens

必須の依存関係: io.ktor:ktor-server-auth, io.ktor:ktor-server-auth-jwt

コード例: auth-jwt-hs256, auth-jwt-rs256

Native サーバー
Ktor は Kotlin/Native をサポートしており、追加のランタイムや仮想マシンなしでサーバーを実行できます。
のサポート: ✖️

JSON Web Token (JWT) は、情報を JSON オブジェクトとして関係者間で安全に送信するための方法を定義したオープン標準です。この情報は、共有シークレット(HS256 アルゴリズムを使用)または公開鍵/秘密鍵のペア(例:RS256)を使用して署名されているため、検証および信頼することができます。

Ktor は、Bearer スキーマを使用して Authorization ヘッダーで渡される JWT を処理し、以下のことを可能にします。

  • JSON Web Token の署名を検証する。
  • JWT ペイロードに対して追加の検証を実行する。

Ktor における認証と認可に関する一般的な情報は、Ktor サーバーにおける認証と認可 セクションで確認できます。

依存関係の追加

JWT 認証を有効にするには、ビルドスクリプトに ktor-server-authktor-server-auth-jwt アーティファクトを含める必要があります。

Kotlin
Groovy
XML

JWT 認可フロー

Ktor における JWT 認可フローは以下のようになります。

  1. クライアントは、サーバーアプリケーション内の特定の認証 ルート (route) に対して、資格情報を含む POST リクエストを送信します。以下の例は、JSON で渡された資格情報を使用した HTTP クライアントPOST リクエストを示しています。
    HTTP
    POST http://localhost:8080/login
    Content-Type: application/json
    
    {
      "username": "jetbrains",
      "password": "foobar"
    }
  2. 資格情報が有効な場合、サーバーは JSON Web Token を生成し、指定されたアルゴリズムで署名します。たとえば、これは特定の共有シークレットを使用した HS256 や、公開鍵/秘密鍵のペアを使用した RS256 などです。
  3. サーバーは生成された JWT をクライアントに送信します。
  4. クライアントは、Bearer スキーマを使用して Authorization ヘッダーに JSON Web Token を含めることで、保護されたリソースにリクエストを送信できるようになります。
    HTTP
    GET http://localhost:8080/hello
    Authorization: Bearer {{auth_token}}
  5. サーバーはリクエストを受信し、以下の検証を実行します。
    • トークンの署名を検証します。検証方法 は、トークンの署名に使用されたアルゴリズムによって異なることに注意してください。
    • JWT ペイロードに対して 追加の検証 を実行します。
  6. 検証後、サーバーは保護されたリソースの内容を応答として返します。

JWT のインストール

jwt 認証プロバイダーをインストールするには、install ブロック内で jwt 関数を呼び出します。

kotlin
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
//...
install(Authentication) {
    jwt {
        // jwt 認証を構成する
    }
}

オプションで、特定のルートを認証する ために使用できる プロバイダー名 を指定することもできます。

JWT の構成

このセクションでは、Ktor サーバーアプリケーションで JSON Web Token を使用する方法を見ていきます。トークンの検証方法が若干異なるため、トークンの署名に関する 2 つのアプローチを示します。

  • 指定された共有シークレットを使用して HS256 を使用する。
  • 公開鍵/秘密鍵のペアを使用して RS256 を使用する。

完全なプロジェクトはこちらで見つけることができます: auth-jwt-hs256, auth-jwt-rs256

ステップ 1: JWT 設定の構成

JWT 関連の設定を構成するために、設定ファイル にカスタムの jwt グループを作成できます。たとえば、application.conf ファイルは以下のようになります。

jwt {
    secret = "secret"
    issuer = "http://0.0.0.0:8080/"
    audience = "http://0.0.0.0:8080/hello"
    realm = "Access to 'hello'"
}
jwt {
    privateKey = "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtfJaLrzXILUg1U3N1KV8yJr92GHn5OtYZR7qWk1Mc4cy4JGjklYup7weMjBD9f3bBVoIsiUVX6xNcYIr0Ie0AQIDAQABAkEAg+FBquToDeYcAWBe1EaLVyC45HG60zwfG1S4S3IB+y4INz1FHuZppDjBh09jptQNd+kSMlG1LkAc/3znKTPJ7QIhANpyB0OfTK44lpH4ScJmCxjZV52mIrQcmnS3QzkxWQCDAiEA1Tn7qyoh+0rOO/9vJHP8U/beo51SiQMw0880a1UaiisCIQDNwY46EbhGeiLJR1cidr+JHl86rRwPDsolmeEF5AdzRQIgK3KXL3d0WSoS//K6iOkBX3KMRzaFXNnDl0U/XyeGMuUCIHaXv+n+Brz5BDnRbWS+2vkgIe9bUNlkiArpjWvX+2we"
    issuer = "http://0.0.0.0:8080/"
    audience = "http://0.0.0.0:8080/hello"
    realm = "Access to 'hello'"
}

秘密情報は設定ファイルにプレーンテキストで保存しないでください。これらのパラメータを指定するには、環境変数 の使用を検討してください。

これらの設定には、次のようにして コード内でアクセス できます。

kotlin
val secret = environment.config.property("jwt.secret").getString()
val issuer = environment.config.property("jwt.issuer").getString()
val audience = environment.config.property("jwt.audience").getString()
val myRealm = environment.config.property("jwt.realm").getString()
kotlin
val privateKeyString = environment.config.property("jwt.privateKey").getString()
val issuer = environment.config.property("jwt.issuer").getString()
val audience = environment.config.property("jwt.audience").getString()
val myRealm = environment.config.property("jwt.realm").getString()

ステップ 2: トークンの生成

JSON Web Token を生成するには、JWTCreator.Builder を使用できます。以下のコードスニペットは、HS256RS256 の両方のアルゴリズムでこれを行う方法を示しています。

kotlin
post("/login") {
    val user = call.receive<User>()
    // ユーザー名とパスワードをチェック
    // ...
    val token = JWT.create()
        .withAudience(audience)
        .withIssuer(issuer)
        .withClaim("username", user.username)
        .withExpiresAt(Date(System.currentTimeMillis() + 60000))
        .sign(Algorithm.HMAC256(secret))
    call.respond(hashMapOf("token" to token))
}
kotlin
post("/login") {
    val user = call.receive<User>()
    // ユーザー名とパスワードをチェック
    // ...
    val publicKey = jwkProvider.get("6f8856ed-9189-488f-9011-0ff4b6c08edc").publicKey
    val keySpecPKCS8 = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyString))
    val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpecPKCS8)
    val token = JWT.create()
        .withAudience(audience)
        .withIssuer(issuer)
        .withClaim("username", user.username)
        .withExpiresAt(Date(System.currentTimeMillis() + 60000))
        .sign(Algorithm.RSA256(publicKey as RSAPublicKey, privateKey as RSAPrivateKey))
    call.respond(hashMapOf("token" to token))
}
  1. post("/login") は、POST リクエストを受信するための認証 ルート を定義します。
  2. call.receive<User>() は、JSON オブジェクトとして送信されたユーザー資格情報を 受信 し、User クラスオブジェクトに変換します。
  3. JWT.create() は、指定された JWT 設定でトークンを生成し、受信したユーザー名を含むカスタムクレームを追加し、指定されたアルゴリズムでトークンに署名します。
    • HS256 の場合、トークンの署名には共有シークレットが使用されます。
    • RS256 の場合、公開鍵/秘密鍵のペアが使用されます。
  4. call.respond は、トークンを JSON オブジェクトとしてクライアントに 送信 します。

ステップ 3: realm の構成

realm プロパティを使用すると、保護されたルート にアクセスしたときに WWW-Authenticate ヘッダーで渡される realm を設定できます。

kotlin
val myRealm = environment.config.property("jwt.realm").getString()
install(Authentication) {
    jwt("auth-jwt") {
        realm = myRealm
    }
}

ステップ 4: トークン検証機能(verifier)の構成

verifier 関数を使用すると、トークンの形式とその署名を検証できます。

  • HS256 の場合、トークンを検証するために JWTVerifier インスタンスを渡す必要があります。
  • RS256 の場合、JwkProvider を渡す必要があります。これは、トークンの検証に使用される公開鍵にアクセスするための JWKS エンドポイントを指定します。今回の例では、issuer が http://0.0.0.0:8080 であるため、JWKS エンドポイントのアドレスは http://0.0.0.0:8080/.well-known/jwks.json になります。
kotlin
val secret = environment.config.property("jwt.secret").getString()
val issuer = environment.config.property("jwt.issuer").getString()
val audience = environment.config.property("jwt.audience").getString()
val myRealm = environment.config.property("jwt.realm").getString()
install(Authentication) {
    jwt("auth-jwt") {
        realm = myRealm
        verifier(JWT
                .require(Algorithm.HMAC256(secret))
                .withAudience(audience)
                .withIssuer(issuer)
                .build())
    }
}
kotlin
val issuer = environment.config.property("jwt.issuer").getString()
val audience = environment.config.property("jwt.audience").getString()
val myRealm = environment.config.property("jwt.realm").getString()
val jwkProvider = JwkProviderBuilder(issuer)
    .cached(10, 24, TimeUnit.HOURS)
    .rateLimited(10, 1, TimeUnit.MINUTES)
    .build()
install(Authentication) {
    jwt("auth-jwt") {
        realm = myRealm
        verifier(jwkProvider, issuer) {
            acceptLeeway(3)
        }
    }
}

ステップ 5: JWT ペイロードの検証

  1. validate 関数を使用すると、JWT ペイロードに対して検証を実行できます。この関数は必須です。構成しない場合、プロバイダーの初期化時に IllegalArgumentException がスローされます。credential パラメーターをチェックしてください。これは JWTCredential オブジェクトを表し、JWT ペイロードを含んでいます。以下の例では、カスタムクレーム username の値をチェックしています。

    kotlin
    install(Authentication) {
        jwt("auth-jwt") {
            validate { credential ->
                if (credential.payload.getClaim("username").asString() != "") {
                    JWTPrincipal(credential.payload)
                } else {
                    null
                }
            }
        }
    }

    認証に成功した場合は、JWTPrincipal を返します。

  2. challenge 関数を使用すると、認証に失敗した場合に送信されるレスポンスを構成できます。

    kotlin
    install(Authentication) {
        jwt("auth-jwt") {
            challenge { defaultScheme, realm ->
                call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
            }
        }
    }

ステップ 6: 特定のリソースの保護

jwt プロバイダーを構成した後、authenticate 関数を使用してアプリケーション内の特定のリソースを保護できます。認証に成功した場合は、ルートハンドラー内で call.principal 関数を使用して認証済みの JWTPrincipal を取得し、JWT ペイロードを取得できます。以下の例では、カスタムクレーム username の値とトークンの有効期限を取得しています。

kotlin
routing {
    authenticate("auth-jwt") {
        get("/hello") {
            val principal = call.principal<JWTPrincipal>()
            val username = principal!!.payload.getClaim("username").asString()
            val expiresAt = principal.expiresAt?.time?.minus(System.currentTimeMillis())
            call.respondText("Hello, $username! Token is expired at $expiresAt ms.")
        }
    }
}