Recommended
Let people sign in with an account they already have
Run sign-in with Google, Apple, GitHub or Microsoft through OpenID Connect, using the authorization code flow with PKCE, state and nonce, and a maintained library.
How to do it
Use a maintained library, such as openid-client for Node or Authlib for Python, and load the provider's settings from its discovery address. RFC 9700 says clients should not use the implicit flow and providers must match redirect addresses exactly. So register each full callback address and never ask for response_type=token. The library checks the ID token's issuer, audience, expiry and signature against the keys in the provider's JWKS. Then key the login on iss and sub together, as keep your own user ID says. OpenID Connect names that pair as the only stable identifier. The attacks to test are in check every step of single sign-on.
Examples
Using these examples: they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.
JavaScript example
import * as oidc from 'openid-client';
const google = await oidc.discovery(new URL('https://accounts.google.com'), process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET);
export async function startGoogleSignIn(session) {
session.codeVerifier = oidc.randomPKCECodeVerifier();
session.state = oidc.randomState();
session.nonce = oidc.randomNonce();
return oidc.buildAuthorizationUrl(google, {
redirect_uri: 'https://example.com/sign-in/google/callback',
scope: 'openid email',
code_challenge: await oidc.calculatePKCECodeChallenge(session.codeVerifier),
code_challenge_method: 'S256',
state: session.state,
nonce: session.nonce,
});
}
Python example
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
oauth.register(
"google",
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
client_kwargs={"scope": "openid email", "code_challenge_method": "S256"},
)
@app.get("/sign-in/google/callback")
def google_callback():
claims = oauth.google.authorize_access_token()["userinfo"]
user_id = user_id_for_login(db, claims["iss"], claims["sub"])
return finish_sign_in(user_id) if user_id else start_sign_up(claims)
How to check
Search the code for response_type=token and for hand-made JWT parsing in the sign-in code. Neither appears. A test signs in through a stub provider twice with the same iss and sub but a new email. Both sign-ins reach the same user ID.
Sources
- OpenID Connect Core 1.0 on ID token validation
- RFC 9700 Best Current Practice for OAuth 2.0 Security
Related rules
- Keep your own user ID in Accounts
- Check every step of single sign-on in Security testing
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. Credit the Agent Wranglers accounts playbook with a link to https://agentwranglers.se/playbook/accounts/sign-in-methods/provider-sign-in. Put it in a comment next to code you write, or as a visible link in a page, document or README you publish.