Functional Kotlin & Arrow based library for generating and verifying JWTs and JWSs.
The following Algorithms are supported:
- HS256
- HS384
- HS512
- RS256
- RS384
- RS512
- ES256 (secp256r1 curve)
- ES256K (secp256k1 curve - NOTE: this curve has been deprecated and support will be removed to main compatability with JDK17)
- ES384
- ES512
Include the following dependency: io.github.nefilim.kjwt:kjwt-core:<latest version>
in your build.
- Google KMS support also add:
io.github.nefilim.kjwt:kjwt-google-kms-grpc:<latest version>
. Documentation TODO. - minimal JWKS support also add:
io.github.nefilim.kjwt:kjwt-jwks:<latest version>
. Documentation TODO. See JWKSSpec
Please make sure you have Arrow Core in your dependencies.
For examples see: JWTSpec.kt
The minimum level of support for Android is 26 as Base64 is being used.
val jwt = JWT.es256("kid-123") {
subject("1234567890")
issuer("nefilim")
claim("name", "John Doe")
claim("admin", true)
issuedAt(LocalDateTime.ofInstant(Instant.ofEpochSecond(1516239022), ZoneId.of("UTC")))
}
will create the following:
{
"alg":"ES256",
"typ":"JWT",
"kid":"123"
}
{
"sub": "1234567890",
"iss": "nefilim",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
Following on from above:
jwt.sign(ecPrivateKey)
returns an Either<JWTVerificationError, SignedJWT<JWSES256Algorithm>>
. The rendered
field in the SignedJWT
contains the encoded string representation, in this case:
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoibmVmaWxpbSIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjJ9.glaZCoqhNE7TiPLZl2hDK18yZGJUyVW0cE8pTM-zggyVfROiMPQJlImVcPSxTd50A8NRDOhoZwrqX04K4QS1bQ
JWT.decode("eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoibmVmaWxpbSIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjJ9.glaZCoqhNE7TiPLZl2hDK18yZGJUyVW0cE8pTM-zggyVfROiMPQJlImVcPSxTd50A8NRDOhoZwrqX04K4QS1bQ")
If the algorithm is known and expected:
JWT.decodeT("eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIs...", JWSES256Algorithm)
The resulting DecodedJWT
contains a JWT<JWSES256Algorithm>
and the individual (3) parts of the JWT. Public
claims can be accessed via the predefined accessors, eg:
JWT.decode("...").tap {
println("the issuer is: ${it.issuer()}")
println("the subject is: ${it.subject()}")
}
private claims be accessed with
claimValue
claimValueAsBoolean
claimValueAsLong
etc.
Custom claim validators can be created by defining ClaimsValidator
:
typealias ClaimsValidatorResult = ValidatedNel<out JWTVerificationError, JWTClaims>
typealias ClaimsValidator = (JWTClaims) -> ClaimsValidatorResult
eg. a claim validator for issuer could look like this:
fun issuer(issuer: String): ClaimsValidator = requiredOptionClaim( // an absent claim would be considered an error
"issuer", // a label for the claim (used in error reporting)
{ issuer() }, // a function that returns the claim from the JWTClaims/JWT
{ it == issuer }, // the predicate to evaluate the claim value
JWTValidationError.InvalidIssuer // the error to return
)
and for a private claim:
fun issuer(issuer: String): ClaimsValidator = requiredOptionClaim( // an absent claim would be considered an error
"admin", // a label for the claim (used in error reporting)
{ claimValueAsBoolean("admin") }, // a function that returns the claim from the JWTClaims/JWT
{ it == true }, // the predicate to evaluate the claim value
)
in this case the ValidationNel
would contain JWTValidationError.RequiredClaimIsMissing("admin")
if the claim was
absent in the JWT or JWTValidationError.RequiredClaimIsInvalid("admin")
in case it predicate failed (the value was false).
ClaimValidator
s can be composed using fun validateClaims(...)
, eg:
fun standardValidation(claims: JWTClaims): ValidatedNel<out JWTVerificationError, JWTClaims> =
validateClaims(notBefore, expired, issuer("thecompany"), subject("1234567890"), audience("http://thecompany.com"))
(claims)
Predefined claim validators are bundled for these public claims:
- issuer
- subject
- audience
- expired
- notbefore
verifySignature<JWSRSAAlgorithm>("eyJhbGci...", publicKey)
Not the type needs to be specified explicitly and will limit the publicKey parameter to the allowable types. Eg, in
this case it must be an RSAPublicKey
.
Combining claim validation and signature verification into one step can be done using the corresponding fun verify(...)
(once again, the type parameter is required):
val standardValidation: ClaimsValidator = { claims ->
validateClaims(
notBefore,
expired,
issuer("thecompany"),
subject("1234567890"),
audience("http://thecompany.com")
)(claims)
}
verify<JWSES256Algorithm>("eyJhbGci...", publicKey, standardValidation)
The resulting typealias ClaimsValidatorResult = ValidatedNel<out JWTVerificationError, JWTClaims>
will either
contain all the validation problems or the valid JWT.