In Ktor's jwt auth provider, what do the verifier and validate blocks each do?
answer
- two gates, different owners
- one runs before your code, one after
- keys can come from a cached JWKS provider
- return a principal or null, never throw
basics
~20 sIn Ktor's jwt provider, verifier supplies the token verifier that checks the signature and standard claims before your code runs; validate then receives the already-verified JWTCredential and decides, in application terms, whether to return a principal or null.
solid answer
~50 sThey are two stages with different owners. `verifier(...)` gives the provider the object that cryptographically verifies an incoming bearer token — either a fixed verifier built from a shared secret, or `verifier(jwkProvider, issuer)` where a `JwkProviderBuilder` fetches and caches the issuer's public keys so rotation works without redeploying. If verification fails, your code never runs and the challenge answers 401. `validate { credential -> ... }` runs only for tokens that already passed: it receives a `JWTCredential` exposing the verified payload, and returns a principal — typically `JWTPrincipal(credential.payload)` or your own user type — or `null` to reject. Use it for application-level decisions the token itself cannot make: is this subject still an active account, does the token carry the claim this service requires, is the tenant enabled. Cryptographic and standard-claim checking belongs in the verifier; business rules belong in validate.
code
kotlin · 18 linesinstall(Authentication) {
jwt("auth-jwt") {
realm = "katajob-api"
verifier(
JWT.require(Algorithm.HMAC256(secret))
.withIssuer(issuer)
.withAudience(audience)
.build()
)
validate { credential ->
val subject = credential.payload.subject
if (subject != null && accounts.isActive(subject)) JWTPrincipal(credential.payload) else null
}
challenge { _, _ ->
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "invalid_token"))
}
}
}go deeper
Know the provider needs a verifier and a validate block, and that validate returns a principal such as JWTPrincipal or null.
Explain the ordering: the verifier gates the token first, validate sees only verified tokens, null triggers the challenge. Name both verifier shapes — fixed key and JWKS provider.
Discuss the operational side — key rotation through a cached, rate-limited key provider, clock leeway, and the cost of doing a revocation lookup inside validate on every request.
Own the trust model: which issuers this platform accepts, whether validation is local or delegated, and how quickly access must be revocable given tokens are snapshots.
## Two stages, and the boundary between them A Ktor `jwt("auth-jwt") { }` provider processes a bearer token in a fixed order: 1. Extract the token from the `Authorization: Bearer ...` header. 2. Hand it to the **verifier** you configured. This is the cryptographic and standard-claims gate. If it rejects, the provider fails immediately — your `validate` block is never entered. 3. Wrap the verified payload in a `JWTCredential` and pass it to **`validate`**, your application-level gate. 4. If `validate` returns a principal, the route handler runs with it; if it returns null, the **challenge** runs. The interview value of the question is whether you place responsibilities on the correct side of step 2/3. ## Configuring the verifier Two shapes appear in real services. **Shared secret.** `verifier(JWT.require(Algorithm.HMAC256(secret)).withIssuer(issuer).withAudience(audience).build())` builds a fixed verifier. Everything you attach to the builder — issuer, audience, expiry handling — is enforced before your code sees the token. Simple, and appropriate when the same service mints and consumes the token. **JWKS.** `verifier(jwkProvider, issuer)` where `jwkProvider = JwkProviderBuilder(URL(jwksUrl)).cached(10, 24, TimeUnit.HOURS).rateLimited(10, 1, TimeUnit.MINUTES).build()`. The provider fetches the issuer's published keys and selects the right one for the token, with caching so every request does not hit the network and rate limiting so an unknown key identifier cannot be used to hammer the issuer. This is the shape for tokens minted by an external identity provider, and it is what makes key rotation a non-event for your service. The verifier configuration also carries clock tolerance: `verifier(jwkProvider, issuer) { acceptLeeway(3) }` allows a few seconds of skew on time-based claims, because distributed clocks are never exactly aligned. ## What belongs in validate `validate` receives `JWTCredential`, whose `payload` exposes the verified claims — `payload.subject`, `payload.audience`, `payload.getClaim("...")`. Good uses: - **Liveness of the subject.** A token is a snapshot from when it was minted; the account may since have been disabled. If your product requires immediate revocation, this is where a lookup or a deny-list check goes — with the latency and coupling cost that implies, which is a legitimate architectural tradeoff to discuss. - **Required claim present.** Reject tokens minted for a different purpose, e.g. lacking the claim this API is built around. - **Building a richer principal.** Rather than `JWTPrincipal(credential.payload)`, return your own type with user id, tenant and roles already extracted, so handlers stop re-parsing claims. Things that do **not** belong: re-implementing signature checks, re-checking expiry that the verifier already enforces, or throwing exceptions. Return null and let the challenge produce the 401. ## The challenge `challenge { defaultScheme, realm -> call.respond(HttpStatusCode.Unauthorized, ...) }` decides what a rejected caller sees. Without it you get Ktor's default 401. Configuring it is how an API returns its own error envelope instead of a bare status, and it fires for every failure path — missing header, failed verification, null from validate — so it cannot distinguish *why* on its own. ## A distinction worth stating Authentication ends here. `validate` returning a principal means "this caller is who the token says". Whether that caller may perform the operation is a separate check your routes perform, because the Ktor plugin has no permission model. Candidates who blur the two often try to return null for an authorization failure, which produces a 401 where a 403 is the correct answer. ## Dependencies The provider lives in `ktor-server-auth-jwt`, which builds on the java-jwt library — hence the `JWT.require(...)`/`Algorithm` builder API and `JwkProviderBuilder`. Knowing that the verifier is that library's object, not a Ktor invention, explains why its configuration surface looks different from the rest of the Ktor DSL.
- Why configure the verifier with a JwkProviderBuilder instead of a hard-coded key?Because the issuer can rotate signing keys without coordinating a redeploy: the provider fetches the published key set and picks the key the token points at. `cached(...)` avoids a network call per request and `rateLimited(...)` stops an unknown key identifier from turning into a flood of fetches against the issuer.
- Should an authorization failure be expressed by returning null from validate?No. Null means authentication failed and produces a 401 challenge. A caller who is authenticated but lacks permission should reach a separate check that responds 403. Collapsing them tells the client to re-authenticate, which will not help and hides a real permission problem behind a login loop.
- What does acceptLeeway on the verifier configuration change?It allows a small tolerance on time-based claim checks so that a modest clock difference between the issuer and your service does not reject otherwise valid tokens. Keep it to a few seconds: it is a fudge factor for skew, not a way to extend how long a token remains usable.
saying these in an interview costs you the question
- Re-checking the signature manually inside validate
- Throwing an exception from validate instead of returning null
- Returning null for a permission failure, producing 401 not 403
- Hard-coding a signing key when the issuer rotates keys
- Assuming validate runs even when verification fails