JSON Web Tokens
必須の依存関係: io.ktor:ktor-server-auth, io.ktor:ktor-server-auth-jwt
コード例: auth-jwt-hs256, auth-jwt-rs256
JSON Web Token (JWT) は、情報を JSON オブジェクトとして関係者間で安全に送信するための方法を定義したオープン標準です。この情報は、共有シークレット(HS256 アルゴリズムを使用)または公開鍵/秘密鍵のペア(例:RS256)を使用して署名されているため、検証および信頼することができます。
Ktor は、Bearer スキーマを使用して Authorization ヘッダーで渡される JWT を処理し、以下のことを可能にします。
- JSON Web Token の署名を検証する。
- JWT ペイロードに対して追加の検証を実行する。
Ktor における認証と認可に関する一般的な情報は、Ktor サーバーにおける認証と認可 セクションで確認できます。
依存関係の追加
JWT 認証を有効にするには、ビルドスクリプトに ktor-server-auth と ktor-server-auth-jwt アーティファクトを含める必要があります。
JWT 認可フロー
Ktor における JWT 認可フローは以下のようになります。
- クライアントは、サーバーアプリケーション内の特定の認証 ルート (route) に対して、資格情報を含む
POSTリクエストを送信します。以下の例は、JSON で渡された資格情報を使用した HTTP クライアント のPOSTリクエストを示しています。HTTPPOST http://localhost:8080/login Content-Type: application/json { "username": "jetbrains", "password": "foobar" } - 資格情報が有効な場合、サーバーは JSON Web Token を生成し、指定されたアルゴリズムで署名します。たとえば、これは特定の共有シークレットを使用した
HS256や、公開鍵/秘密鍵のペアを使用したRS256などです。 - サーバーは生成された JWT をクライアントに送信します。
- クライアントは、
Bearerスキーマを使用してAuthorizationヘッダーに JSON Web Token を含めることで、保護されたリソースにリクエストを送信できるようになります。HTTPGET http://localhost:8080/hello Authorization: Bearer {{auth_token}} - サーバーはリクエストを受信し、以下の検証を実行します。
- 検証後、サーバーは保護されたリソースの内容を応答として返します。
JWT のインストール
jwt 認証プロバイダーをインストールするには、install ブロック内で jwt 関数を呼び出します。
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'"
}秘密情報は設定ファイルにプレーンテキストで保存しないでください。これらのパラメータを指定するには、環境変数 の使用を検討してください。
これらの設定には、次のようにして コード内でアクセス できます。
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()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 を使用できます。以下のコードスニペットは、HS256 と RS256 の両方のアルゴリズムでこれを行う方法を示しています。
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))
}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))
}post("/login")は、POSTリクエストを受信するための認証 ルート を定義します。call.receive<User>()は、JSON オブジェクトとして送信されたユーザー資格情報を 受信 し、Userクラスオブジェクトに変換します。JWT.create()は、指定された JWT 設定でトークンを生成し、受信したユーザー名を含むカスタムクレームを追加し、指定されたアルゴリズムでトークンに署名します。HS256の場合、トークンの署名には共有シークレットが使用されます。RS256の場合、公開鍵/秘密鍵のペアが使用されます。
call.respondは、トークンを JSON オブジェクトとしてクライアントに 送信 します。
ステップ 3: realm の構成
realm プロパティを使用すると、保護されたルート にアクセスしたときに WWW-Authenticate ヘッダーで渡される realm を設定できます。
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になります。
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())
}
}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 ペイロードの検証
validate関数を使用すると、JWT ペイロードに対して検証を実行できます。この関数は必須です。構成しない場合、プロバイダーの初期化時にIllegalArgumentExceptionがスローされます。credentialパラメーターをチェックしてください。これは JWTCredential オブジェクトを表し、JWT ペイロードを含んでいます。以下の例では、カスタムクレームusernameの値をチェックしています。kotlininstall(Authentication) { jwt("auth-jwt") { validate { credential -> if (credential.payload.getClaim("username").asString() != "") { JWTPrincipal(credential.payload) } else { null } } } }認証に成功した場合は、JWTPrincipal を返します。
challenge関数を使用すると、認証に失敗した場合に送信されるレスポンスを構成できます。kotlininstall(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 の値とトークンの有効期限を取得しています。
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.")
}
}
}