Skip to content
Server Plugin

JSON Webトークン

必須の依存関係: 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トークンの署名を検証します。
  • JWTペイロードに対して追加の検証を実行します。

Ktorにおける認証と認可に関する一般的な情報は、Ktor Serverの認証と認可セクションを参照してください。

依存関係の追加

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

Kotlin
Groovy
XML

JWT認証フロー

KtorにおけるJWT認証フローは次のようになります。

  1. クライアントは、サーバーアプリケーションの特定の認証ルートに対して、認証情報を含むPOSTリクエストを送信します。以下の例は、JSONで認証情報を渡すHTTPクライアントPOSTリクエストを示しています。
    HTTP
    POST http://localhost:8080/login
    Content-Type: application/json
    
    {
      "username": "jetbrains",
      "password": "foobar"
    }
  2. 認証情報が有効な場合、サーバーはJSON Webトークンを生成し、指定されたアルゴリズムで署名します。たとえば、これは特定の共有シークレットを使用するHS256、または公開/秘密鍵ペアを使用するRS256である場合があります。
  3. サーバーは生成されたJWTをクライアントに送信します。
  4. クライアントは、Bearerスキーマを使用してAuthorizationヘッダーにJSON Webトークンを渡すことで、保護されたリソースにリクエストを送信できるようになります。
    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 {
        // Configure jwt authentication
    }
}

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

JWTの設定

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

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

完全なプロジェクトはこちらで確認できます: auth-jwt-hs256auth-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トークンを生成するには、JWTCreator.Builderを使用できます。以下のコードスニペットは、HS256およびRS256アルゴリズムの両方でこれを行う方法を示しています。

kotlin
post("/login") {
    val user = call.receive<User>()
    // Check username and password
    // ...
    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>()
    // Check username and password
    // ...
    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プロパティを使用すると、保護されたルートにアクセスする際にWWW-Authenticateヘッダーで渡されるレルムを設定できます。

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

ステップ4: トークン検証器の構成

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

  • HS256の場合、トークンを検証するためにJWTVerifierインスタンスを渡す必要があります。
  • RS256の場合、トークンを検証するために使用される公開鍵にアクセスするためのJWKSエンドポイントを指定するJwkProviderを渡す必要があります。この場合、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ペイロードに対して追加の検証を実行できます。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.")
        }
    }
}