Git Product home page Git Product logo

kjwt's Introduction

Kotlin version MavenCentral

kJWT

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

Usage

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

Android

The minimum level of support for Android is 26 as Base64 is being used.

Creating a JWT

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
}

Signing a JWT

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

Decoding a JWT

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.

Validating a JWT

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).

ClaimValidators 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

Verifying a Signature

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.

Claim Validation and Verifying together

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.

kjwt's People

Contributors

janseeger avatar nefilim avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

kjwt's Issues

Expiration validation failure due to TimeZone differences

Hello

I am receiving TokenExpired failures as the claim validation (see snippet below) doesn't consider the timezone in the comparison.

val expired: ClaimsValidator = requiredOptionClaim("expired", { expiresAt() }, { it.isAfter(LocalDateTime.now()) }, KJWTValidationError.TokenExpired)

From the extension function which is used to deserialize the expiration field ("exp") I can see it uses UTC timezone (see snippet below)

internal fun Long.fromJWTNumericDate(): LocalDateTime = LocalDateTime.ofEpochSecond(this, 0, ZoneOffset.UTC)

The difference in time zones is enough to cause all new tokens to fail this validation even though the token is valid. Any help would be appreciated, thank you!

JWT Header "typ" is required, causes AlgorithmMismatch

Hi

I noticed that as part of JWT.kt the typ field is required (see snippet below).

@Serializable
data class JOSEHeader<T: JWSAlgorithm>(
    @SerialName("alg") @Serializable(JWSAlgorithmSerializer::class) val algorithm: T,
    @SerialName("typ") @Serializable(JOSETypeSerializer::class) val type: JOSEType,
    @SerialName("kid") val keyID: JWTKeyID? = null,
) {
    fun toJSON(): String {
        return if (keyID != null && keyID.id.isNotBlank())
            """{"alg":"${algorithm.headerID}","typ":"${JOSEType.JWT}","kid":"${keyID.id}"}"""
        else
            """{"alg":"${algorithm.headerID}","typ":"${JOSEType.JWT}"}"""
    }
}

Is this intentional? As part of https://www.rfc-editor.org/rfc/rfc7519#section-5.1 it's not required. I am receiving an error AlgorithmMismatch which I imagine is coming from the deserialization process as part of JWT.decode(...) as there is no "typ" field in my header. I can't make changes to this payload as it's coming from a third-party authorisation server (Okta):

fun decode(jwt: String): Either<KJWTVerificationError, DecodedJWT<out JWSAlgorithm>> {
    return either.eager {
        val parts = jwt.split(".")
        Either.conditionally (!(parts.size < 2 || parts.size > 3), { KJWTVerificationError.InvalidJWT }, {}).bind()

        val h = Either.catch {
            format.decodeFromString(JOSEHeader.serializer(PolymorphicSerializer(JWSAlgorithm::class)), jwtDecodeString(parts[0]))
        }.mapLeft { KJWTVerificationError.AlgorithmMismatch }.bind()
        val claims = Either.catch { format.parseToJsonElement(jwtDecodeString(parts[1])) }.mapLeft { KJWTVerificationError.InvalidJWT }.bind()
        val claimsMap = Either.catch { (claims as JsonObject) }.mapLeft { KJWTVerificationError.EmptyClaims }.bind()

        DecodedJWT(JWT(h, claimsMap), parts)
    }
}

get JsonArray from claims

I have a claimSet in the JWT that is seen as a JsonArray (even though in the end its just a list of Strings).

However, there is no claimValue function for that since they all check for jsonPrimitive?

This leads to the exception:

java.lang.IllegalArgumentException: Element class kotlinx.serialization.json.JsonArray is not a JsonPrimitive

Is there any way to get that? In the end it is just a StringList but even claimValueAsList throws that Exception.

kjwt without arrow

Hi,

I'm developing in a project that doesn't uses "arrow", so I wonder if you could send a sample of generating JWT hs256 without arrow

Android support only API 26+

And maybe a heads up for everyone who wants to use this for an Android Project.

Since the Base64 class is used, it allows the usage of this library only from Android API 26+.

https://developer.android.com/reference/java/util/Base64

Versions below will get trouble with the call
fun jwtDecodeString(data: String): String = String(Base64.getUrlDecoder().decode(data))
stating a NoClassDefFoundError: Failed resolution of: Ljava/util/Base64;

Maybe it would be good to mention this in case someone is interested in this.

Getting all Claims

Sorry for bothering but is there a way to get all claims from a JWT?

There is sadly only an option for getting single fields from it but is there access to the claimSet directly? It is set to private but it could come handy to get the whole map.

Cannot access class 'arrow.core.Either'.

In my Android project, the gradle configuration I use looks like that:

implementation 'io.github.nefilim.kjwt:kjwt-core:0.4.0'

but somehow the decode function doesn't seem to work for me. I always get the message:

Cannot access class 'arrow.core.Either'. Check your module classpath for missing or conflicting dependencies

No matter whether I use decode or decodeT and it's not like I have any other jwt library added or such. Am I missing something? Is this probably not compatible with Android?

How to add claim with nested structure?

Hello!

I'd love to generate a JWT with a nested structure, e.g.:

{
  "realm_access": {
    "roles": [
      "default-roles-my-realm",
      "offline_access",
      "uma_authorization"
    ]
  }
}

Could you please give me guidance on how to achieve this?

Cheers!
Michal

Instant should be used instead of LocalDateTime

The times should be in a time zone independent format and so they could use Instant instead. LocalDateTime loses the time zone information and the user needs to know that the time zone is implicitly UTC. The API becomes confusing because the user cannot use a LocalDateTime in their own local time zone. Instant, being independent of time zones, does not have this problem.

Exception thrown when updated Arrow to 2.0.0 in main app module

I updated Arrow to 2.0.0-alpha.2 in my main app and after that I have this exception thrown at runtime. This is strange because I already had the Arrow version of this lib and the one I use in the project differs.
Some suggestions? Thanks

java.lang.NoClassDefFoundError: Failed resolution of: Larrow/core/computations/either; at io.github.nefilim.kjwt.JWT$Companion.decode(JWT.kt:129) ... Caused by: java.lang.ClassNotFoundException: Didn't find class "arrow.core.computations.either" on path

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.