When designing a web application, you often need to store some data on the client side to use it between HTTP requests. For example, you want to know the ID of the user who makes the request, the shopping cart for the store, or the movie history for the online cinema. The Sessions plugin provides such a mechanism.
Installation
To use the Sessions plugin, you need to include the ktor-server-sessions artifact in your build script:
implementation("io.ktor:ktor-server-sessions:$ktor_version")implementation "io.ktor:ktor-server-sessions:$ktor_version"To install the plugin, pass it to the install function inside your module:
fun Application.module() {
install(Sessions)
}You can also install the plugin to specific routes when you need different session configurations for different resources.
Create a data class
Since the session stores information, we need a class to interact with this information. For example, the UserSession class below will be used to store the user ID and the access level.
enum class AccessLevel { USER, ADMIN }
@Serializable
data class UserSession(val id: UUID, val level: AccessLevel)Pass session data via cookie
One of the options to transfer the session to the client is to use cookies. To do this, configure the plugin in this way:
install(Sessions) {
cookie<UserSession>("user_session")
}In the example above, the session will be passed to the client in the Set-Cookie header with the cookie name user_session.
You can configure other cookie attributes by passing them inside the cookie block. For example, the code snippet below shows how to specify a cookie's path and expiration time:
install(Sessions) {
cookie<UserSession>("user_session") {
cookie.path = "/"
cookie.maxAgeInSeconds = 10
}
}Before deploying your application to production, make sure the security property is set to true. This enables transferring cookies via a secure connection only and protects session data from HTTPS downgrade attacks.
Pass session data via header
Another way to transmit sessions is request/response headers. To use headers, call the header function with the specified name and data class inside the install(Sessions) block:
install(Sessions) {
header<UserSession>("user_session")
}In the example above, session data will be passed to the client using the user_session custom header. On the client side, you need to append this header to each request to get session data.
Get and set session content
To work with sessions, use the call.sessions in the endpoint handler. You can use the set method to create a new session instance. If you need to get the session value, you can use the get method receiving one of the registered session types as a type parameter:
import io.ktor.server.sessions.get
import io.ktor.server.sessions.set
// ...
get("/login") {
val session = UserSession(id = UUID.randomUUID(), level = AccessLevel.USER)
call.sessions.set(session)
call.respondRedirect("/me")
}
get("/me") {
val session = call.sessions.get<UserSession>() ?: throw Exception("Unauthorized")
call.respondText("Welcome, ${session.level} ${session.id}")
}After visiting the page, a session with the following content will be installed for the client:
id=#sd54e551e-e793-4050-8596-b487e78f06e0&level=#sUSERTo modify a session, for example, to update an access level, you need to call the copy method of the data class:
get("/admin") {
val session = call.sessions.get<UserSession>() ?: throw Exception("Unauthorized")
call.sessions.set(session.copy(level = AccessLevel.ADMIN))
call.respondRedirect("/me")
}When you need to clear a session for any reason, for example, when a user logs out, call the clear function:
import io.ktor.server.sessions.clear
// ...
get("/logout") {
call.sessions.clear<UserSession>()
call.respondText("Goodbye!")
}Store session payload on server
You can store session data on the server and pass only a session ID between the client and server. In such a case, you can choose where to store the payload on the server. For example, you can store session data in memory, in a specified folder, or you can implement own custom storage.
SessionStorageMemory enables storing a session's content in memory. This storage stores data while the server is running and deletes information after the server stops. To configure the storage, you can use this code:
cookie<UserSession>("user_session", SessionStorageMemory()) { ... }SessionStorageMemory is intended for development only.
If you want to store session data in a file, use directorySessionStorage. For example, to store session data in a file under the .sessions directory, create the directorySessionStorage in this way:
header<UserSession>("user_session", directorySessionStorage(File(".sessions"))) { ... }Finally, Ktor provides the SessionStorage interface that allows you to implement custom storage:
interface SessionStorage {
suspend fun invalidate(id: String)
suspend fun write(id: String, value: String)
suspend fun read(id: String): String
}Protect session data on client
One way to protect sessions from unauthorized modification is to sign the session. When you sign a session, you add a cryptographic signature, which will be invalid if the user changes the contents of the session. To sign a session, pass a sign key to the SessionTransportTransformerMessageAuthentication constructor and pass this instance to the transform function:
install(Sessions) {
val secretSignKey = "my super secret key".toByteArray()
cookie<UserSession>("user_session") {
transform(SessionTransportTransformerMessageAuthentication(secretSignKey))
}
}After visiting /login, the client will have a session where the last part is a cryptographic signature:
id=#s...&level=#sUSER/c4dd...6739If you change the value of the session without signing it with the correct key, the server will consider the signature invalid. Thus, you can edit the session only by knowing the secret key.
For greater security, the session can not only be signed but also encrypted. In this case, the user will not only be unable to edit the contents of the session, but they also will not be able to see what is contained in the session. To sign and encrypt a session, pass a sign/encrypt key to the SessionTransportTransformerEncrypt constructor and pass this instance to the transform function.
install(Sessions) {
val secretEncryptKey = hex("00112233445566778899aabbccddeeff")
val secretSignKey = "my super secret key".toByteArray()
cookie<UserSession>("user_session") {
transform(SessionTransportTransformerEncrypt(secretEncryptKey, secretSignKey))
}
}Now, after visiting /login, the client will have an encrypted session:
c000bbdf...c17e9446Do not store secret keys in the source code. Use environment variables instead.
Conclusion
Now can to store information about the user using the session mechanism. We know we can store the session data both on the client and the server. When storing sensitive information on the client, it is worth signing or encrypting it all together, so the user cannot change it.