A redirect is a bad place for a secret.
URLs end up in browser history, server logs, analytics, and Referer headers sent to other sites. So when the DUBTEL AI backend needed SAML single sign-on and working password resets, I designed both around one rule: a long-lived session token never appears in a URL.
The SSO flow
SAML's final step is the identity provider posting an assertion to the service provider's ACS endpoint. The backend validates it (I used python3-saml), then has to get the user logged in on the frontend, which is a separate app on another origin. The tempting shortcut is to redirect to the frontend with the access token in the URL.
Instead, the ACS endpoint:
- validates the assertion and finds or creates the user
- mints an exchange code: a JWT that expires in 2 minutes and carries
purpose: "sso_exchange" - redirects to the frontend with the code in the URL fragment (
#code=…)
The frontend reads the fragment and immediately POSTs the code to /auth/saml/exchange. The server checks the purpose claim and the expiry, confirms the user is still active, and returns a normal session.
Why these details:
- Fragments aren't sent to servers. Everything after
#stays in the browser. It isn't in request logs orRefererheaders. - The purpose claim stops any other JWT the system issues from being used as an exchange code.
- The short expiry limits the damage if a code leaks anyway.
- One login response. Password login and SSO both go through a shared
build_login_response(), so an SSO session is exactly the same as a password session. No second code path to secure.
The first SSO login provisions the user just in time into a configurable default tenant. The whole feature stays inert until its SAML_* settings are configured, so it shipped safely behind configuration.
The gap I'd close next
The exchange code isn't truly single-use. It's a stateless JWT, so within its two-minute window the same code could be exchanged twice. The fix is small: store each code's ID (jti) when it's redeemed and refuse repeats, or keep codes in a short-lived table and delete them on use. The fragment and the short expiry make exploitation unlikely, but "unlikely" isn't the standard for authentication. That's the next change.
Password resets
The forgot-password endpoint existed, but it only logged the reset token with a TODO. Nobody could actually reset a password. Making it real needed three things.
Tokens that die after use. Each reset token embeds the user's current password_changed_at. When the password changes (through a reset or a normal change), that timestamp moves, and every outstanding reset token stops matching. The token is effectively single-use without storing it anywhere.
Links that point at the right site. Reset emails were built from one hard-coded frontend URL that defaulted to production, so QA reset emails linked to prod. The link now uses the request's Origin when it matches a trusted domain pattern, falling back to the configured URL otherwise. QA and prod get correct links with no per-environment settings.
Rate limiting that actually works. The first per-email limiter kept its counts in memory. With four uvicorn workers, each worker had its own counts, so the limit barely applied. It moved to a small database table that every worker shares. (I wrote more about this class of bug in Count the Processes.)
Checklist
- No long-lived token in any URL. Use a short-lived, purpose-scoped code, sent in a fragment and exchanged by
POST. - Make exchange codes truly single-use by recording redemption.
- Tie reset tokens to something that changes on use, like a password-change timestamp.
- Build links from a trusted
Origin, never an untrusted one. - Keep rate-limit state where every worker can see it.

Henry Iddirisu
AI product engineer · Accra, Ghana · Remote