In Ktor, how does session-based authentication work with the Sessions plugin?
answer
- two plugins, not one
- one carries state, one authenticates it
- an unsigned cookie is user-editable text
- cookie holds payload, or just an id
basics
~10 sTwo plugins cooperate: Sessions defines a typed session carried in a cookie, and Authentication's session provider validates it per request. A login route calls call.sessions.set(...); protected routes wrap in authenticate and read the principal.
solid answer
~40 s`install(Sessions) { cookie<UserSession>("user_session") { ... } }` declares a typed session and how it travels — cookie name, `path`, `httpOnly`, `secure`, `maxAgeInSeconds` — and, critically, a `transform(...)` such as `SessionTransportTransformerMessageAuthentication(signKey)` so the cookie is signed and cannot be forged, or `SessionTransportTransformerEncrypt(...)` when the contents must also be opaque. Then `install(Authentication) { session<UserSession>("auth-session") { validate { session -> if (accounts.exists(session.userId)) session else null }; challenge { call.respondRedirect("/login") } } }` turns a present, intact session into a principal. A login handler verifies the password itself and calls `call.sessions.set(UserSession(id))`; logout calls `call.sessions.clear<UserSession>()`. By default the whole session object lives in the cookie; passing a `storage` such as `SessionStorageMemory()` or a directory-backed store keeps state server-side with only an id in the cookie, which is what makes real invalidation possible.
code
kotlin · 19 lines@Serializable
data class UserSession(val userId: String)
install(Sessions) {
cookie<UserSession>("user_session") {
cookie.path = "/"
cookie.httpOnly = true
cookie.secure = true
cookie.maxAgeInSeconds = 3600
transform(SessionTransportTransformerMessageAuthentication(signKey))
}
}
install(Authentication) {
session<UserSession>("auth-session") {
validate { session -> if (accounts.isActive(session.userId)) session else null }
challenge { call.respondRedirect("/login") }
}
}go deeper
Know that Sessions and the session authentication provider are two installs, that login sets the session via call.sessions.set, and that protected routes still need an authenticate block.
Explain the pieces: typed cookie configuration, the transform that signs or encrypts it, validate turning a session into a principal, and clear() on logout.
Argue cookie-side versus server-side storage in terms of revocation, cookie size and multi-instance deployment, and get the cookie attributes right for a browser-facing service.
Own the session strategy across the platform — cookies for browsers versus bearer tokens for machine clients, where session state lives, and how fast a compromised session must be killable.
## Two plugins, two jobs Beginners conflate them, and the question is often asked to see whether you separate them. **`Sessions`** is transport and serialization. It knows nothing about authentication. `install(Sessions) { cookie<UserSession>("user_session") { ... } }` says: there is a session type `UserSession`, it rides in a cookie of that name, here is how it is protected, here is how long it lives. Inside any handler you can `call.sessions.set(value)`, `call.sessions.get<UserSession>()` and `call.sessions.clear<UserSession>()` — even with no auth plugin installed at all. A shopping cart or a flash message uses the same machinery. **The `session<T>` authentication provider** is the bridge. `session<UserSession>("auth-session") { validate { ... }; challenge { ... } }` says: when a route is wrapped in `authenticate("auth-session")`, read the session of that type; if it is absent or `validate` returns null, run the challenge; otherwise the returned object becomes the principal. ## The login and logout routes are yours Nothing in Ktor logs anyone in. A login route reads the submitted credentials (via `call.receiveParameters()` for a form post, or `receive<T>()` for JSON), verifies the password against your store, and on success establishes the session: `call.sessions.set(UserSession(userId = user.id))` Logout is `call.sessions.clear<UserSession>()`, which instructs the client to drop the cookie. Note what "logout" means in the default, cookie-only mode: you ask the browser to discard a credential it still physically holds. A copy captured earlier remains valid until it expires. That is the central tradeoff of client-side sessions. ## Cookie-side versus server-side By default the serialized session object is the cookie value. That is stateless and scales trivially across instances, but it means the payload travels to the client, it is size-limited, and it cannot be revoked before expiry. Passing a storage — `cookie<UserSession>("user_session", storage = SessionStorageMemory())`, or a directory-backed store, or your own `SessionStorage` implementation over Redis — flips this: the cookie carries only an opaque identifier and the payload stays server-side. Now you can invalidate a session on demand and keep large or sensitive state out of the browser, at the cost of shared storage that every instance must reach. `SessionStorageMemory()` is fine for tests and single-instance development and wrong for a multi-instance deployment, where each node would hold different sessions. ## Integrity is not optional A plain cookie value is client-controlled text. Without a transform, a user can edit their session and become someone else. `transform(SessionTransportTransformerMessageAuthentication(signKey))` attaches an authentication code so tampering is detected and the session rejected. `SessionTransportTransformerEncrypt(encryptKey, signKey)` both hides and authenticates the contents; use it when the payload itself should not be readable. Even with server-side storage, signing the identifier is worthwhile. The cookie attributes carry the rest of the defence: `httpOnly = true` keeps scripts away from it, `secure = true` keeps it off plaintext connections, `path` narrows where it is sent, and `maxAgeInSeconds` bounds its life. Combined with a same-site policy and anti-forgery measures on state-changing routes, this is the standard cookie hygiene a browser-facing service needs. ## Validate is still your decision function `validate { session -> ... }` runs on every request to a protected route. Returning the session object makes it the principal; returning null rejects. It is the natural place to confirm the referenced account still exists and is enabled — cheap if you can answer from a cache, a per-request database round trip if you cannot, which is a real cost to weigh. ## The challenge distinguishes browsers from APIs For a server-rendered application, `challenge { call.respondRedirect("/login") }` is the expected behaviour: an unauthenticated visitor lands on the login page. For an API surface, redirecting is wrong — return 401 with your error body so a client can react. This is exactly why Ktor makes the challenge a block you write instead of a fixed behaviour, and why a mixed application usually declares two providers with two challenges. ## Sessions versus bearer tokens Both end at a principal. Sessions suit browser applications: cookies are sent automatically, invalidation is possible with server-side storage, and nothing needs to be stored in JavaScript. Bearer tokens suit machine clients and cross-origin callers where a cookie is inconvenient or undesirable. Many services run both — two named providers, and routes wrapped in whichever they serve.
- What does adding a storage to the cookie session change?The cookie then carries only an opaque identifier while the payload lives server-side. That enables real invalidation and keeps sensitive or bulky state out of the browser, but it introduces shared state every instance must reach — an in-memory store is single-instance only, so multi-node deployments need an external one.
- Why must a Ktor session cookie be signed or encrypted?Because the cookie value is client-controlled. Without `transform(...)`, a user can rewrite the serialized session and impersonate another account. A message-authentication transform detects tampering; the encrypting transform additionally makes the contents unreadable when the payload itself should not be exposed.
- What does call.sessions.clear<UserSession>() actually accomplish?It tells the browser to drop the cookie, so subsequent requests carry no session. In default cookie-only mode a previously captured copy stays valid until it expires, because nothing server-side records the revocation. Genuine logout across stolen copies requires server-side session storage you can delete from.
saying these in an interview costs you the question
- Installing the session auth provider without the Sessions plugin
- Leaving the session cookie unsigned and unencrypted
- Believing clear() invalidates a stolen cookie-only session
- Using in-memory session storage across multiple instances
- Redirecting to a login page from an API endpoint's challenge