Level: Beginner
In the previous lesson we learned how to implement basic auth in our server, in this lesson we’re going to learn how to implement JWT auth and bearer token.
JSON Web Tokens (JWT)
A JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The token is digitally signed and contains a payload that can be verified and trusted. JWTs are commonly used for authentication and authorization in web applications.
A JWT typically consists of three parts:
- Header: The header contains the algorithm used for signing the token, such as HMAC SHA256 or RSA.
- Payload: The payload contains the claims or data that the token is intended to convey, such as user information or permissions.
- Signature: The signature is generated by signing the header and payload with a secret key.
Bearer Tokens
A Bearer Token is a type of token that can be used to authenticate and authorize a user or client. When a client presents a Bearer Token to a server, the server verifies the token and grants access to the requested resources if the token is valid.
In the context of JWT, a Bearer Token is simply a JWT that is sent in the Authorization
header of an HTTP request. The token is prefixed with the string “Bearer” to indicate that it is a Bearer Token.
Here’s an example of a Bearer Token in an HTTP request:
GET /protected-resource HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaGFuIjoiMjMwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
How it works
Here’s a high-level overview of how JWT and Bearer Tokens work:
- A client requests access to a protected resource.
- The server authenticates the client and generates a JWT containing the client’s claims.
- The server returns the JWT to the client.
- The client stores the JWT and includes it in the
Authorization
header of subsequent requests to the server. - The server verifies the JWT and grants access to the requested resources if the token is valid.
Setup
add the following dependency to your build.gralde file:
// JWT auth
implementation("io.ktor:ktor-server-auth-jwt-jvm")
add the following to your applicatio configuration file, you might have a Hocon or YAML file so use whatever you have
- application.conf
- application.yaml
jwt {
domain: "http://localhost/"
audience: "fruit-admins"
realm: "fruit server"
secret: "test-secret"
}
jwt:
domain: "http://localhost/"
audience: "fruit-admins"
realm: "fruit server"
secret: "test-secret"
TO BE UPDATED: The secret should not be hard coded in your config file and should be added to an enviroment variable file instead. also its super important to keep it a secret and never expose it. (I will update the article on how to do this later)
Preparing JWT
Open our Security.kt file and comment out the basic authentication for now to avoid any errors.
Read the jwt config values from your configuration file and initalize JWT auth like below
// Access to 'enviroment.config' should be above the 'authentication' block
val jwtConfig = environment.config.config("jwt")
val jwtAudience = jwtConfig.property("audience").getString()
val jwtDomain = jwtConfig.property("domain").getString()
val jwtRealm = jwtConfig.property("realm").getString()
val jwtSecret = jwtConfig.property("secret").getString()
authentication {
jwt {
realm = jwtRealm
verifier(
JWT
.require(Algorithm.HMAC256(jwtSecret))
.withAudience(jwtAudience)
.withIssuer(jwtDomain)
.build()
)
validate { credential ->
if (credential.payload.audience.contains(jwtAudience)) JWTPrincipal(credential.payload) else null
}
}
}
Create a response data class for when the user logs in successfully the idea is that we would like to respond with a token and some other details that we could represent in this data class.
@Serializable
data class LoginResponse(
val username: String,
val token: String,
@SerialName("expires_at")
val expiresAt: String
)
Update your login end point to generate a JWT when login is successfull and respond with LoginResponse
// if password is correct generate a new JWT token
val expiresAt = Clock.System.now().plus(30.days)
val token = JWT.create()
.withAudience(jwtAudience)
.withIssuer(jwtDomain)
.withClaim("username", user.username)
.withExpiresAt(expiresAt.toJavaInstant())
.sign(Algorithm.HMAC256(jwtSecret))
val response = LoginResponse(user.username, token, expiresAt.toLocalDateTime(TimeZone.UTC).toString())
call.respond(HttpStatusCode.OK, response)
This should generate a bearer token and respond with it on successful login!.
Update add-fruit end point
Update the add-fruit end point to read the username from the JWTPrincipal
// get the username for the authenticated user
val username = call.principal<JWTPrincipal>()?.get("username") ?: throw Exception("Can't get username")
And that’s it!. the article will be updated soon untill then you can find the current source code here