تخطَّ إلى المحتوى

Auth

هذا المحتوى غير متوفر بلغتك بعد.

Authentication is where every engineer’s estimate is wrong by a factor of three. “Add SSO” sounds like a Tuesday. It’s a fortnight, and most of the pain isn’t in the auth library — it’s in the seams around it.

The lesson that keeps re-teaching itself

Auth is not a feature you add. It’s a cross-cutting concern that touches middleware, session storage, route protection, environment config, redirect handling, and every third-party integration you have. When someone says “we need Okta SSO by next sprint,” what they’ve actually asked for is a redesign of every one of those pieces.

The estimate is wrong because the mental model is wrong. “Add OAuth” is one line of config. “Wire enterprise SSO into an app that already has session-based auth, protected routes, tenant awareness, and a mobile client hitting the same backend” is a project.

Where the time actually goes

On a Cognito Hosted UI + Okta enterprise integration I owned recently, here’s the breakdown that surprised the original estimate:

  • Redirect handling across environments. Local, preview, staging, prod — each has different callback URLs. Cognito app clients are environment-specific. Okta app registrations are environment-specific. Every one of them has to be configured, and each one is a config file, a Cognito console setting, and an Okta admin change. Miss one and the flow silently redirects to the wrong domain.
  • Application-level authorization on top of the identity token. Cognito tells you who the user is. It doesn’t tell you what they can do inside your app. Middleware still has to check “is this user a member of this tenant, do they have this role” on every request — and now that check has to work with a token from an external IdP whose claims format you don’t control.
  • Session lifecycle mismatches. Okta says the session is 8 hours. Cognito says 1 hour. Your app is issuing 24-hour refresh tokens. The user hits “log out” — which one of those actually terminates the session? All three, in the right order, or the user is still authenticated somewhere.
  • Middleware protecting routes it doesn’t know about. Next.js middleware runs before the route. Great — until you add a new route and forget to update the middleware’s public-path allowlist. The failure mode is silent: the route works locally (dev bypass) and breaks in staging. The fix is a deny-by-default middleware with an explicit allowlist, not the reverse.
  • Handling logged-out state gracefully. What happens when a user’s session expires mid-request? What about mid-form-submission with 200 lines of unsaved input? The right answer isn’t “redirect to login” — it’s “surface it non-destructively, let them re-auth in a modal, keep their state.” That’s a UX problem the auth library doesn’t solve for you.

Opinions I hold now

Deny by default, allow explicitly. Middleware should require authentication for every route unless the route is on an explicit public list. The failure mode of the opposite (allow by default, protect explicitly) is a leaked page. The failure mode of deny-by-default is a 404 you fix in one line.

Separate identity from authorization. The IdP tells you who. Your app decides what they can do. Never let the IdP’s claims be the authoritative source for application-level permissions — they’ll change, they’ll drift, and your app should be the source of truth about its own rules.

One session boundary, not three. Pick where the session lives (your app’s cookie, usually) and treat everything else — Cognito tokens, Okta sessions — as upstream state that feeds into it. When a user logs out, log them out of your app first. Upstream sessions may outlive that, and that’s fine as long as the next login re-verifies.

Test the failure modes, not just the happy path. Expired token in the middle of a request. Wrong tenant. Deactivated Okta user. Cognito outage. Half your bugs will be in these paths and your automated tests don’t touch any of them by default. Write them.

Environment parity is not optional. If your auth flow is different between local, staging, and prod, you don’t have an auth flow — you have three, and you’re testing one of them. Every environment gets a real IdP integration, even if it’s a dev tenant. Local dev with “skip auth in development” mode is how prod bugs are born.

An auth chapter isn’t complete without these, and they’re what most libraries let you forget until they burn you:

  • Cookies signed and httpOnly. Never accessible from JS. Signed with a secret that rotates. Secure in production, SameSite=Lax unless you have a real reason for None.
  • CSRF protection on every mutating request that reads from a cookie. Token in a header, matched against a server-known value. Most modern frameworks handle this; verify yours does before assuming.
  • Session fixation prevention. Regenerate the session ID on login and on any privilege escalation. Otherwise a pre-authentication session ID becomes an authenticated one, and that’s a hijack.
  • Idle and absolute timeouts, both. Idle (say 30 min of no activity) and absolute (24 hours regardless of activity). Pick both, not one.

None of these are exotic. They get skipped because “the library does that.” Verify what the library actually does before you ship.

When SSO would be overkill

For truly internal admin tooling with a small user set, I reach for Google Workspace (or Microsoft 365) SSO through a hosted auth provider — Clerk, WorkOS, Supabase, pick one. Thirty minutes of config, no password storage on my side, no reset flows, no 2FA management. The team already has Workspace accounts; the auth layer just trusts those.

Rolling your own email/password for six users means owning password hashing, reset flows, rate limiting, and eventually 2FA — more operational surface than the “enterprise IdP integration” you were trying to avoid. The shortcut isn’t “skip the IdP”; it’s “use one that already knows your users.”