JWT can be a tricky thing to implement yourself, fortunately you don't really have to do that, it's much easier, safer and faster to use one of many existing libraries.
Most frameworks have such libraries, and so does Ktor. In this post I would like to save the knowledge on how to quickly setup basic JWT flow in your app.
0. Add gradle import and install feature
implementation "io.ktor:ktor-auth-jwt:$ktor_version"
install(Authentication) { jwt { } }
1. Setup secret and config
Preferably you should use standard application.conf
file to store configuration info. If so, just add your cypher's secret and other jwt related configs there (don't push it to control version system)
jwt {
"SECRET" = "123"
"VALIDITY_MS" = "36000000" // 10 Hours
"ISSUER" = "softwaret"
"REALM" = "softwaret.kpdf"
}
2. Prepare token generation method
Pick user-related data that you want to store in token and then prepare token generation method (I use just login in my example)
fun generateToken(login: Login): String = JWT.create()
.withSubject("Authentication")
.withIssuer(issuer)
.withClaim("login", login.value)
.withExpiresAt(obtainExpirationDate())
.sign(algorithm)
private fun obtainExpirationDate() = Date(System.currentTimeMillis() + tokenExpirationPeriodMs) # expiration period from config file
3. Pass token to user after login
Return the new token in any way you like and let client set it as its Authorization
header with value Bearer $token
(eg Authorization: Bearer 123
).
4. Setup token validation
Add body to installed JWT application's feature
jwt {
realm = "jwt.REALM" #realm from config file
verifier(buildJwtVerifier())
validate { jwtCredential -> validateCredential(jwtCredential) }
}
}
Verifier should check whether token is correct and can be trusted, fortunately the framework takes care of this for us, just create a function which build the verifier:
fun buildJwtVerifier() =
JWT.require(Algorithm.HMAC512("jwt.SECRET")) # secret from config file
.withIssuer(issuer) # issuer from config file
.build()
Validator should check whether any user (or any other entity in your app) matches the passed data (login in my example) and sets it as the call's principle. This principle with be later available for any call handling method which requires authentication, so that one can easily check wich user asked about data.
fun validateCredential(jwtCredential: JWTCredential) =
if (userByLoginExists(jwtCredential.payload.getClaim("login")) {
JWTPrincipal(jwtCredential.payload)
} else {
null
}
5. Put any route which requires authentication inside authenticate
block
Just wrap any route in authenticate
lambda and it will easily handle JWT authentication and return 401 if token is invalid.
authenticate {
get("/authentication") {
call.respond(message = "Authentication is valid!")
}
}
And that's it! Happy JWT-ing in the future!
Real life code example of JWT implementation can by found here: github.com/Softwaret/kpdf
Top comments (4)
Hi, good read well defined and on point, I dig it!
Have you done something with refresh token logic? I'm working on a REST api app using ktor and I need to implement refresh token logic since the standard JWT library does not support this out of the box do you have any recommendation?
Nice and clear article.
Is it possible to implement user roles and authorize each user to a specific route?
Can you please explain what is issuer and realm here. Also about refreshing tokens?
Hi, issuer and realm are just standard fields added to the claim set in JWT, you can find more about them on wikipedia en.wikipedia.org/wiki/JSON_Web_Token.
Refreshing token is a completely different story and can be implemented in many ways, I use short-lived access token + refresh token but it's up to your application really.