OAuth 2.0 is an authorization protocol: it gets your server an access token for somebody else's API. OpenID Connect is a thin layer on top that also answers "who is this person" by returning an ID token, a JWT of verified identity claims. "Sign in with Google" is OpenID Connect; reading a user's repositories is OAuth.

Only the authorization code flow is current. The implicit flow returned tokens in the URL fragment and is retired; so is the password grant. PKCE (RFC 7636) — a random code_verifier, its SHA-256 sent ahead as code_challenge — was once a mobile-only add-on, and the OAuth 2.0 Security Best Current Practice now requires it for every client.
import * as client from 'openid-client'; // npm i openid-client@6.8.8
const config = await client.discovery(new URL(ISSUER + '/.well-known/openid-configuration'),
process.env.CLIENT_ID, process.env.CLIENT_SECRET);
app.get('/auth/login', async (req, res) => {
const verifier = client.randomPKCECodeVerifier();
req.session.pkce = verifier; // never send this to the browser
req.session.state = client.randomState();
res.redirect(client.buildAuthorizationUrl(config, {
redirect_uri: 'https://app.example.com/auth/callback',
scope: 'openid email profile', state: req.session.state,
code_challenge: await client.calculatePKCECodeChallenge(verifier),
code_challenge_method: 'S256'
}).href);
});
app.get('/auth/callback', async (req, res) => {
const tokens = await client.authorizationCodeGrant(config, new URL(req.url, BASE),
{ pkceCodeVerifier: req.session.pkce, expectedState: req.session.state });
const claims = tokens.claims(); // signature, iss, aud, exp checked
const user = await users.upsertByIssuer(claims.iss, claims.sub, claims.email);
req.session.regenerate(() => { req.session.userId = user.id; res.redirect('/'); });
}); // a failed state or PKCE check throws here, and Express 5 sends it to your handlerThe state parameter is the CSRF defense for the redirect: without comparing it, an attacker can feed your callback a code from their account and silently link it to your user's session. Never treat claims.email as a primary key either — the stable identity is the pair (iss, sub).
Prefer a maintained library to hand-rolled requests: openid-client handles discovery, JWKS fetching and key rotation, and ID token validation. Either way, the session you create at the end is an ordinary Express 24,430 session.