Short answer: use a short-lived, server-tracked session for the browser, and verify every identity token with the issuer's published public keys before creating that session. For a developer portal with Google and GitHub sign-in, account recovery is the deciding constraint: a login method is only useful if a locked-out developer can regain control without bypassing the same checks.
The before-and-after model
Before, many portals treat an OAuth callback as proof of identity. The callback arrives, a cookie is set, and the rest of the system trusts that cookie for days. Recovery becomes an afterthought. A changed email, a lost device, or a revoked provider account can leave support choosing between a risky manual reset and a frustrated customer.
After, the callback is only an input to a verification pipeline. The portal checks state and redirect handling, validates the token signature and claims, maps the provider identity to an internal account, then issues a session with a deliberately short lifetime. The session is the portal's record of authorization; the provider token is not.
That separation makes incidents legible. A session lookup can be revoked centrally. A public-key check can reject a forged or expired token before it reaches account linking. The pieces are boring. Good.
Tokens expire.
How should developer portal sessions use public-key verification?
Think of two clocks. The identity token's clock is controlled by the issuer and its claims. The session clock is controlled by your portal and should be shorter than the time you are willing to leave a browser trusted. A practical starting point is a 15-minute access window with rotation on activity, plus server-side revocation for sensitive events; your mileage may vary with the portal's risk and support model.
Public-key verification answers, “Was this token signed by the expected issuer, and is it still valid?” It does not answer, “Should this browser remain trusted?” Session policy answers the second question. Mixing those jobs creates long-lived bearer tokens that are hard to revoke and easy to leak through logs, browser storage, or copied URLs.
The verification order matters. Fetch the issuer's JSON Web Key Set (JWKS) over TLS, select the key by the token's kid, verify the algorithm you explicitly allow, and then check issuer, audience, expiry, nonce, and the provider subject. Never accept an algorithm selected by the token itself. OWASP also recommends generic authentication error messages and controls against automated attacks, because detailed failures help attackers enumerate accounts.
Here is the shape of the boundary in TypeScript. The cryptographic primitive is intentionally abstract; use a maintained JOSE implementation rather than writing RSA or ECDSA code yourself.
type Claims = {
iss: string;
aud: string | string[];
sub: string;
exp: number;
nonce?: string;
};
async function acceptIdentityToken(token: string, expectedNonce: string): Promise<Claims> {
const claims = await verifyWithIssuerKeys(token, {
allowedAlgorithms: ["RS256", "ES256"],
issuer: "https://issuer.example",
audience: "developer-portal",
});
if (claims.exp <= Math.floor(Date.now() / 1000)) throw new Error("invalid credentials");
if (claims.nonce !== expectedNonce) throw new Error("invalid credentials");
return claims;
}
The callback handler should exchange a verified sub for an internal account ID, not use an email address as the primary key. Email can change or be unverified at one provider. Keep the provider name and subject together as a unique identity record, and require an explicit, recent session before linking a second provider.
What does a recovery-first design change?
Recovery is a state machine, not a “forgot password” link. For social sign-in, document at least three paths: the developer still controls one linked provider; the developer has a verified, separately protected recovery factor; or neither is available and support must perform a high-assurance review. Each path needs a distinct audit event and rate limit. My test matrix starts with two identities in staging: one with both providers linked and one with only GitHub. I remove Google access from the second identity, wait through the normal cooldown, and verify that the resulting support path asks for evidence rather than silently creating a new account. That check is small, but it exposes a dangerous assumption: a matching email is not proof that two provider subjects belong to the same person. The portal must preserve the original subject record, record who approved the link, and make the delay visible to support. If your team cannot explain each transition in a log review, the flow is too implicit.
The catch is that convenience and recoverability pull in opposite directions. A portal that auto-links accounts on a matching email feels smooth, but it can join two identities during an email migration. A portal that requires a second factor for every link is safer, yet adds friction for small teams. Not suitable when your users have no durable recovery factor: in that case, keep the account scoped to one provider and publish a support review policy instead of inventing a magic bypass.
| Situation | Allow | Require |
|---|---|---|
| Existing session, adding Google or GitHub | Link provider subject | Recent re-authentication and CSRF protection |
| Provider account lost, recovery factor available | Start recovery | Factor verification, cooldown, audit log |
| All factors lost | Support review | Identity evidence and a delayed reset |
| Suspicious repeated attempts | Deny and slow down | Generic message, alert, and investigation |
I'm not sure a single recovery factor fits every developer organization. Team-owned accounts, contractors, and bots have different ownership models. Write the policy with those roles in mind, then test it with a staging account whose provider access is intentionally removed. Don't promise an instant reset when the evidence review is deliberately delayed.
Operating the flow in production
Instrument the boundaries, not the secrets. Count callback starts, verification failures by reason class, session creation, revocation, recovery starts, and recovery completions. Redact tokens, authorization codes, raw email addresses, and full subject identifiers. A useful alert is a sudden increase in 401 responses after a key rotation or a spike in recovery requests for one tenant.
Run failure drills. Rotate a provider key and confirm JWKS refresh works without dropping valid sessions. Revoke a session and confirm API calls stop immediately. Replay a callback and confirm the stored state or nonce blocks it. These tests catch wiring errors that a happy-path login never shows.
Keep browser cookies Secure, HttpOnly, and appropriate SameSite values. Set a narrow callback redirect allowlist. Apply throttling to sign-in and recovery endpoints, and return the same public error for an unknown account as for a bad credential. OWASP's guidance is clear on this point: authentication defenses include the surrounding transport, session, and abuse controls, not just the token check.
Choosing the boundary
Use server sessions when you need immediate revocation, tenant-level policy, or a straightforward incident response. Use self-contained access tokens only when multiple services truly need to validate them independently, and then keep their lifetime short and rotate signing keys with a published overlap window. In both designs, public-key verification belongs at the trust boundary.
Stick with a provider-only recovery flow when your portal cannot operate a durable factor or support review. Choose an additional recovery factor when losing a Google or GitHub account would otherwise permanently lose project access. The right answer is the one you can explain, log, test, and revoke at 02:00.
References
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
Further reading
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html













