Theory

Two mechanisms: a JWT in a header, a session in a cookie

You have now built authentication twice. In Pica (the Android course) a mobile client proves who it is with a JWT in an Authorization header. In SignFlow a browser proves who it is with a session cookie backed by a database. This lesson is the reason the whole four-track curriculum was worth finishing: the same problem — prove who this request is from — has two correct answers, and you can now feel exactly why.

What Pica does — a JWT in a header. On login the Pica backend signs a JWT and hands it back. The client stores it and attaches it to every subsequent request through an OkHttp interceptor:

// Pica: AuthInterceptor
val token = runBlocking { tokenStorage.getToken() }
val newRequest = originalRequest.newBuilder()
    .header("Authorization", "Bearer $token")
    .build()

The server verifies that token by its signature alone — it recomputes the signature with its secret and checks it matches. There is no database lookup: everything the server needs (the user id, an expiry) is inside the token. That is what "stateless" means, and it is the JWT's great strength — any instance can verify any token with no shared session store.

It is also the JWT's defining limitation: you cannot revoke it. The server keeps no record of the token, so it has nothing to delete. A stolen JWT is valid until it expires, full stop. "Log this user out everywhere, now" is not something a pure JWT can do.

What SignFlow does — a session cookie. On login SignFlow generates a random opaque token, stores its SHA-256 hash in a sessions table, and sends the raw token to the browser in an HttpOnly cookie. Every request resolves it against the database:

// SignFlow: LoadUser middleware → session.Manager.User
row, err := m.q.GetUserBySessionToken(ctx, hashToken(c.Value))

The cookie carries no information — it is just a lookup key. The authoritative session lives on the server. That costs a database read per request, but it buys the thing the JWT can't: revocation is a single DELETE.

// SignFlow: Logout → session.Manager.Destroy
m.q.DeleteSession(ctx, hashToken(c.Value))   // the session is gone; the cookie is now dead

Log out, force-logout after a password reset, an admin killing a compromised session — all of it is one row deleted. The next request finds no session and is anonymous.

Two mechanisms, opposite in shape: claims-in-the-token, no server state, no revocation versus key-in-the-cookie, server state, instant revocation. Next: why neither is the "right" one.