JSON Webトークン
必須の依存関係: 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トークンの署名を検証します。
- JWTペイロードに対して追加の検証を実行します。
Ktorにおける認証と認可に関する一般的な情報は、Ktor Serverの認証と認可セクションを参照してください。
依存関係の追加
JWT
認証を有効にするには、ビルドスクリプトにktor-server-auth
およびktor-server-auth-jwt
アーティファクトを含める必要があります。
JWT認証フロー
KtorにおけるJWT認証フローは次のようになります。
- クライアントは、サーバーアプリケーションの特定の認証ルートに対して、認証情報を含む
POST
リクエストを送信します。以下の例は、JSONで認証情報を渡すHTTPクライアントのPOST
リクエストを示しています。HTTPPOST http://localhost:8080/login Content-Type: application/json { "username": "jetbrains", "password": "foobar" }
- 認証情報が有効な場合、サーバーはJSON Webトークンを生成し、指定されたアルゴリズムで署名します。たとえば、これは特定の共有シークレットを使用する
HS256
、または公開/秘密鍵ペアを使用するRS256
である場合があります。 - サーバーは生成されたJWTをクライアントに送信します。
- クライアントは、
Bearer
スキーマを使用してAuthorization
ヘッダーにJSON Webトークンを渡すことで、保護されたリソースにリクエストを送信できるようになります。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 {
// Configure jwt authentication
}
}
オプションで、プロバイダー名を指定できます。これは、指定されたルートを認証するために使用できます。
JWTの設定
このセクションでは、サーバーKtorアプリケーションでJSON Webトークンを使用する方法を見ていきます。トークンを検証するためにわずかに異なる方法が必要となるため、トークンを署名する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トークンを生成するには、JWTCreator.Builderを使用できます。以下のコードスニペットは、HS256
およびRS256
アルゴリズムの両方でこれを行う方法を示しています。
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))
}
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))
}
post("/login")
は、POST
リクエストを受信するための認証ルートを定義します。call.receive<User>()
は、JSONオブジェクトとして送信されたユーザー認証情報を受信し、それをUser
クラスオブジェクトに変換します。JWT.create()
は、指定されたJWT設定でトークンを生成し、受信したユーザー名を含むカスタムクレームを追加し、指定されたアルゴリズムでトークンに署名します。HS256
の場合、共有シークレットがトークンの署名に使用されます。RS256
の場合、公開/秘密鍵ペアが使用されます。
call.respond
は、トークンをJSONオブジェクトとしてクライアントに送信します。
ステップ3: レルムの構成
realm
プロパティを使用すると、保護されたルートにアクセスする際にWWW-Authenticate
ヘッダーで渡されるレルムを設定できます。
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
になります。
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ペイロードに対して追加の検証を実行できます。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.")
}
}
}