OAuth 2.0 authorization code flow with mandatory PKCE (S256). Register your app in the developer portal, then run the sample Node client below against the OpenID Provider.
Use this document for endpoints, scopes, and supported features. Set OIDC_DISCOVERY_URL to this URL in your .env.
OIDC_DISCOVERY_URL=https://auth.devclub.in/api/oauth/.well-known/openid-configuration
Full source code is maintained in 3x3cu73/oidc-client. Below are the key pieces for quick integration.
The sample loads a .env file next to server.mjs if present (only sets variables that are not already defined in the process environment).
| Variable | Required | Description |
|---|---|---|
| OIDC_CLIENT_ID | Yes | Client ID from the developer portal. |
| OIDC_CLIENT_SECRET | Yes | Client secret from the developer portal. This sample uses client_secret_post. |
| OIDC_REDIRECT_URI | Yes | Redirect URI; must exactly match a URI registered for this client (for example http://localhost:3000/callback). |
| OIDC_SCOPE | Yes | Space-separated OAuth scopes (for example openid profile email). |
| OIDC_DISCOVERY_URL | One of | Full URL to the OpenID configuration JSON (recommended). Example: https://auth.devclub.in/api/oauth/.well-known/openid-configuration |
| OIDC_ISSUER | One of | Issuer base URL, if you are not using OIDC_DISCOVERY_URL. |
| OIDC_APP_NAME | No | Label on the sign-in page. Default: OIDC client. |
| PORT | No | HTTP listen port. Default: 3000. |
Required: OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, OIDC_SCOPE, and either OIDC_DISCOVERY_URL or OIDC_ISSUER.
The sample is published in 3x3cu73/oidc-client. You need Node.js 18+ (20 LTS recommended) and an OAuth client registered with a redirect URI that exactly matches OIDC_REDIRECT_URI in your .env.
cp .env.example .env, then edit .env and set all required values (see the table above).npm install.npm start (runs node server.mjs). The process prints a URL such as http://localhost:3000 unless you set PORT./callback and see a welcome page.# First time setup
git clone https://github.com/3x3cu73/oidc-client
cd oidc-client
# If already cloned, get latest code
git pull
# Configure environment
cp .env.example .env
# Edit .env: OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, OIDC_SCOPE, OIDC_DISCOVERY_URL (or OIDC_ISSUER)
npm install
npm start
# Then open http://localhost:3000 in a browser (or http://localhost:${PORT} if PORT is set)
If the server exits immediately, read the error: missing or invalid env vars are reported to the terminal. If the browser shows "Bad session", start again from the home page so a new PKCE session cookie is set.
Full source code is maintained in 3x3cu73/oidc-client. Below are the key pieces for quick integration.
# Required - from the developer portal
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
# Required - must match a registered redirect URI exactly
OIDC_REDIRECT_URI=http://localhost:3000/callback
# Required - space-separated OAuth scopes
OIDC_SCOPE=openid profile email
# Required: set either OIDC_DISCOVERY_URL or OIDC_ISSUER (discovery is recommended)
OIDC_DISCOVERY_URL=https://auth.devclub.in/api/oauth/.well-known/openid-configuration
# OIDC_ISSUER=https://auth.devclub.in
# Optional
OIDC_APP_NAME=OIDC client
PORT=3000
{
"name": "tmp1-oidc-client",
"private": true,
"type": "module",
"scripts": {
"start": "node server.mjs"
},
"dependencies": {
"openid-client": "^6.8.2"
}
}
const discoveryUrl = process.env.OIDC_DISCOVERY_URL?.trim()
const issuer = process.env.OIDC_ISSUER?.trim()
const clientId = (process.env.OIDC_CLIENT_ID ?? '').trim()
const clientSecret = (process.env.OIDC_CLIENT_SECRET ?? '').trim()
const redirectUri = (process.env.OIDC_REDIRECT_URI ?? '').trim()
const scope = (process.env.OIDC_SCOPE ?? '').trim()
const appName = process.env.OIDC_APP_NAME ?? 'OIDC client'
const port = Number(process.env.PORT ?? 3000)
if (!clientId || (!discoveryUrl && !issuer)) {
console.error('Missing OIDC_CLIENT_ID and either OIDC_DISCOVERY_URL or OIDC_ISSUER (.env)')
process.exit(1)
}
if (!clientSecret) {
console.error('Missing OIDC_CLIENT_SECRET (.env)')
process.exit(1)
}
if (!redirectUri) {
console.error('Missing OIDC_REDIRECT_URI (.env)')
process.exit(1)
}
if (!scope) {
console.error('Missing OIDC_SCOPE (.env)')
process.exit(1)
}
if (req.method === 'GET' && url.pathname === '/auth') {
const cfg = await getConfig()
// PKCE: verifier stays server-side; only the challenge goes to the IdP.
const codeVerifier = client.randomPKCECodeVerifier()
const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier)
// State: CSRF protection for the redirect round-trip.
const state = client.randomState()
const sid = randomBytes(24).toString('hex')
sessions.set(sid, { code_verifier: codeVerifier, state })
const redirectTo = client.buildAuthorizationUrl(cfg, {
redirect_uri: redirectUri,
scope,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state,
})
res.writeHead(302, {
location: redirectTo.href,
'set-cookie': setSessionCookie(sid),
})
res.end()
return
}
if (req.method === 'GET' && url.pathname === '/callback') {
const sid = getCookie(req, 'oidc_session')
const sess = sid ? sessions.get(sid) : undefined
if (!sess) {
sendHtml(res, 400, readFileSync(resolve(frontendDir, 'bad-session.html'), 'utf8'), {
'set-cookie': clearSessionCookie(),
})
return
}
sessions.delete(sid)
const cfg = await getConfig()
const tokens = await client.authorizationCodeGrant(cfg, url, {
pkceCodeVerifier: sess.code_verifier,
expectedState: sess.state,
})
const claims = tokens.claims()
let userinfo = null
if (claims?.sub) {
try {
userinfo = await client.fetchUserInfo(cfg, tokens.access_token, claims.sub)
} catch {
userinfo = { _error: true }
}
}
const name = pickName(claims, userinfo)
sendHtml(
res,
200,
renderTemplate('welcome.html', { USER_NAME: name, APP_NAME: appName }),
{ 'set-cookie': clearSessionCookie() },
)
return
}
<!DOCTYPE html>
<!--
Sign-in landing page (frontend only).
{{APP_NAME}} is replaced by server.mjs -> renderTemplate('index.html', { APP_NAME }).
"Sign in" goes to GET /auth (backend starts the OIDC redirect).
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign in</title>
<link rel="stylesheet" href="/static/styles.css">
</head>
<body class="page-center">
<h1>{{APP_NAME}}</h1>
<a class="btn" href="/auth">Sign in</a>
</body>
</html>
<!DOCTYPE html>
<!--
Shown after successful OIDC callback (frontend only).
{{USER_NAME}}, {{APP_NAME}} <- server.mjs after token + optional UserInfo.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Welcome</title>
<link rel="stylesheet" href="/static/styles.css">
</head>
<body class="page-center">
<h1>Welcome, {{USER_NAME}}</h1>
<p>Login successful</p>
<p>{{APP_NAME}}</p>
</body>
</html>