Open5
Nimbus JWT + JOSE の使い方
Nimbus JWT + JOSE
Spring Security の Resource Server サポート等の OAuth 2.0, OpenID Connect のクライアントを利用する際に内部で利用されている実装。普段はあまり見ないが、JWTや JWKSet 等の具体的な実装コードがあるので見ると色々勉強になる
JWKSet の構成方法
通常 JWKSet は外部プロパティでURLが渡されるなどして、自動的に取得し構成される。しかし諸事情があって内部のファイルで持ちたい場合などは、以下のようなコードで対応できる。
// Load JWK set from JSON file, etc.
JWKSet jwkSet = JWKSet.load(new File("/var/server/jwkset.json");
自分はこれに気づかず、以下を実装。
val json = Files.readString(Paths.get(ClassLoader.getSystemResource("jwk.json").toURI()));
return JWKSet.parse(json)
Immutable にする
JWKSet を Immutable にする
// Create JWK source backed by a JWK set
JWKSource keySource = new ImmutableJWKSet(jwkSet);
署名付きJWT
val jwkSource: JWKSource<SecurityContext> = ImmutableJWKSet(jwkSet())
val minter: ConfigurableJWSMinter<SecurityContext> = DefaultJWSMinter()
minter.jwkSource = jwkSource
val header = JWSHeader.Builder(JWSAlgorithm.RS256)
.type(JOSEObjectType.JWT)
.build()
// Create JWT
val nowInstant = Instant.now()
val jwtClaims = JWTClaimsSet.Builder()
.issuer("https://openid.net")
.subject("alice")
.audience(Arrays.asList("https://app-one.com", "https://app-two.com"))
.expirationTime(Date(nowInstant.plusSeconds(600).toEpochMilli())) // expires in 10 minutes
.notBeforeTime(Date(nowInstant.toEpochMilli()))
.issueTime(Date(nowInstant.toEpochMilli()))
.jwtID(UUID.randomUUID().toString())
.build()
val jwsObject = minter.mint(header, jwtClaims.toPayload(), null)
return jwsObject.serialize()
-
SecurityContext
はcom.nimbusds.jose.proc.SecurityContext
であるので、spring.security
パッケージのものと混同しないように
JSON Web Encryption
暗号化したJSONオブジェクト。当然ながら普通にパースしても何のことやらな内容
val jwk = jwkSet()
// Create JWT
val nowInstant = Instant.now()
val jwtClaims = JWTClaimsSet.Builder()
.issuer("https://openid.net")
.subject("alice")
.audience(Arrays.asList("https://app-one.com", "https://app-two.com"))
.expirationTime(Date(nowInstant.plusSeconds(600).toEpochMilli())) // expires in 10 minutes
.notBeforeTime(Date(nowInstant.toEpochMilli()))
.issueTime(Date(nowInstant.toEpochMilli()))
.jwtID(UUID.randomUUID().toString())
.build()
// Request JWT encrypted with RSA-OAEP-256 and 128-bit AES/GCM
val header = JWEHeader(
JWEAlgorithm.RSA_OAEP_256,
EncryptionMethod.A128GCM
)
val jwt = EncryptedJWT(header, jwtClaims)
// Create an encrypter with the specified public RSA key
val encrypter = RSAEncrypter(jwk.getKeyByKeyId("1").toRSAKey())
jwt.encrypt(encrypter)
return jwt.serialize()
Verify JWT
署名付き JWT の検証
try {
val jwkSource = ImmutableJWKSet<SecurityContext>(jwkSet())
// JwtProcessor
val jwtProcessor = DefaultJWTProcessor<SecurityContext>()
jwtProcessor.jwsTypeVerifier = DefaultJOSEObjectTypeVerifier(JOSEObjectType.JWT)
// jwtProcessor.jweTypeVerifier // JWE の Verifier もいる
val keySelector = JWSVerificationKeySelector(JWSAlgorithm.RS256, jwkSource)
jwtProcessor.jwsKeySelector = keySelector
jwtProcessor.jwtClaimsSetVerifier =
DefaultJWTClaimsVerifier(
JWTClaimsSet.Builder().issuer(ISSUER).build(),
setOf("sub", "iat", "exp", "jti", "KEY1", "KEY2"))
val claimsSet: JWTClaimsSet = jwtProcessor.process(token, null)
logger.debug("Verified Claimset: $claimsSet")
// Claims から各種カスタム情報を取得
val hoge = claimsSet.claims.get("KEY1") as String
val fuga = claimsSet.claims.get("KEY2") as String
return Pairs(hoge, fuga)
} catch (e: Exception) {
when (e) {
is ParseException, is BadJOSEException, is JOSEException -> {
// Verify 失敗
throw FailedVerifyJwtException("Failed verify Jwt Token", e)
}
else -> {
throw UnexpectedVerifyJwtException("Unexpected Error", e)
}
}
}
- JWT の正しさや有効期限を確認し、カスタム Claims の値を取得して返してる
- Verify した時点で Claims 等の取得ができてるので、そのまま利用した
-
jwtProcessor.jweTypeVerifier
もあるため、 JWE も検証できそう