Ktor 3.0.2 Help

JSON Web Tokens

JSON Web Token (JWT) 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 JWTauthentication, you need to include the ktor-server-auth and ktor-server-auth-jwt artifacts in the build script:

implementation("io.ktor:ktor-server-auth:$ktor_version") implementation("io.ktor:ktor-server-auth-jwt:$ktor_version")
implementation "io.ktor:ktor-server-auth:$ktor_version" implementation "io.ktor:ktor-server-auth-jwt:$ktor_version"
<dependency> <groupId>io.ktor</groupId> <artifactId>ktor-server-auth-jvm</artifactId> <version>${ktor_version}</version> </dependency> <dependency> <groupId>io.ktor</groupId> <artifactId>ktor-server-auth-jwt-jvm</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:

import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.auth.jwt.* //... 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 complete 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 a configuration file. For example, the application.conf 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 the 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

  1. The validate function allows you to perform additional validations on the JWT payload. 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.

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

    In the case of successful authentication, return JWTPrincipal.

  2. The challenge function allows you to configure a response to be sent if authentication fails.

    install(Authentication) { jwt("auth-jwt") { challenge { defaultScheme, realm -> call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired") } } }

Step 6: Protect specific resources

After configuring the jwt provider, you can protect specific resources in our application using the authenticate function. In the 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: 11 September 2024