Ktor 1.6.7 Help

JSON Web Tokens

JSON Web Token is an open standard that defines a way for securely transmitting information between parties as a JSON object. This information can be verified and trusted since it is signed using a shared secret (with the HS256 algorithm) or a public/private key pair (for example, RS256).

Ktor handles JWTs passed in the Authorization header using the Bearer schema and allows you to:

  • verify the signature of a JSON web token;

  • perform additional validations on the JWT payload.

Add dependencies

To enable JWT authentication, you need to include the ktor-auth and ktor-auth-jwt artifacts in the build script:

implementation "io.ktor:ktor-auth:$ktor_version" implementation "io.ktor:ktor-auth-jwt:$ktor_version"
implementation("io.ktor:ktor-auth:$ktor_version") implementation("io.ktor:ktor-auth-jwt:$ktor_version")
<dependency> <groupId>io.ktor</groupId> <artifactId>ktor-auth</artifactId> <version>${ktor_version}</version> </dependency> <dependency> <groupId>io.ktor</groupId> <artifactId>ktor-auth-jwt</artifactId> <version>${ktor_version}</version> </dependency>

JWT authorization flow

The JWT authorization flow in Ktor might look as follows:

  1. A client makes a POST request with the credentials to a specific authentication route in a server application. The example below shows an HTTP client POST request with the credentials passed in JSON:

    POST http://localhost:8080/login Content-Type: application/json { "username": "jetbrains", "password": "foobar" }
  2. If the credentials are valid, a server generates a JSON web token and signs it with the specified algorithm. For example, this might be HS256 with a specific shared secret or RS256 with a public/private key pair.

  3. A server sends a generated JWT to a client.

  4. A client can now make a request to a protected resource with a JSON web token passed in the Authorization header using the Bearer schema.

    GET http://localhost:8080/hello Authorization: Bearer {{auth_token}}
  5. A server receives a request and performs the following validations:

  6. After validation, a server responds with the contents of a protected resource.

Install JWT

To install the jwt authentication provider, call the jwt function inside the install block:

install(Authentication) { jwt { // Configure jwt authentication } }

You can optionally specify a provider name that can be used to authenticate a specified route.

Configure JWT

In this section, we'll see how to use JSON web tokens in a server Ktor application. We'll demonstrate two approaches to signing tokens since they require slightly different ways to verify tokens:

  • Using HS256 with a specified shared secret.

  • Using RS256 with a public/private key pair.

You can find full runnable projects here: auth-jwt-hs256, auth-jwt-rs256.

Step 1: Configure JWT settings

To configure JWT-related settings, you can create a custom jwt group in the application.conf configuration file. This file might look as follows:

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'" }

You can access these settings in code in the following way:

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

Step 2: Generate a token

To generate a JSON web token, you can use JWTCreator.Builder. Code snippets below show how to do this for both HS256 and RS256 algorithms:

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)) }
  1. post("/login") defines an authentication route for receiving POST requests.

  2. call.receive<User>() receives user credentials sent as a JSON object and converts it to a User class object.

  3. JWT.create() generates a token with the specified JWT settings, adds a custom claim with a received username, and signs a token with the specified algorithm:

    • For HS256, a shared secret is used to sign a token.

    • For RS256, a public/private key pair is used.

  4. call.respond sends a token to a client as a JSON object.

Step 3: Configure realm

The realm property allows you to set the realm to be passed in WWW-Authenticate header when accessing a protected route.

val myRealm = environment.config.property("jwt.realm").getString() install(Authentication) { jwt("auth-jwt") { realm = myRealm } }

Step 4: Configure a token verifier

The verifier function allows you to verify a token format and its signature:

  • For HS256, you need to pass a JWTVerifier instance to verify a token.

  • For RS256, you need to pass JwkProvider, which specifies a JWKS endpoint for accessing a public key used to verify a token. In our case, an issuer is http://0.0.0.0:8080, so a JWKS endpoint address will be 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) } } }

Step 5: Validate JWT payload

The validate function allows you to perform additional validations on the JWT payload in the following way:

  1. Check the credential parameter, which represents a JWTCredential object and contains the JWT payload. In the example below, the value of a custom username claim is checked.

  2. In a case of successful authentication, return JWTPrincipal. If authentication fails, return null.

install(Authentication) { jwt("auth-jwt") { validate { credential -> if (credential.payload.getClaim("username").asString() != "") { JWTPrincipal(credential.payload) } else { null } } } }

Step 6: Define authorization scope

After configuring the jwt provider, you can define the authorization for the different resources in our application using the authenticate function. In a case of successful authentication, you can retrieve an authenticated JWTPrincipal inside a route handler using the call.principal function and get the JWT payload. In the example below, the value of a custom username claim and a token expiration time are retrieved.

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.") } } }
Last modified: 06 September 2021