IITD OAuth documentation

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.

OpenID Connect discovery

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.

Environment variables

The sample loads a .env file next to server.mjs if present (only sets variables that are not already defined in the process environment).

VariableRequiredDescription
OIDC_CLIENT_IDYesClient ID from the developer portal.
OIDC_CLIENT_SECRETYesClient secret from the developer portal. This sample uses client_secret_post.
OIDC_REDIRECT_URIYesRedirect URI; must exactly match a URI registered for this client (for example http://localhost:3000/callback).
OIDC_SCOPEYesSpace-separated OAuth scopes (for example openid profile email).
OIDC_DISCOVERY_URLOne ofFull URL to the OpenID configuration JSON (recommended). Example: https://auth.devclub.in/api/oauth/.well-known/openid-configuration
OIDC_ISSUEROne ofIssuer base URL, if you are not using OIDC_DISCOVERY_URL.
OIDC_APP_NAMENoLabel on the sign-in page. Default: OIDC client.
PORTNoHTTP listen port. Default: 3000.

Required: OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, OIDC_SCOPE, and either OIDC_DISCOVERY_URL or OIDC_ISSUER.

Run the sample app

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.

  1. Get the latest sample code from GitHub (first time: clone; later updates: pull).
  2. Create your env file: cp .env.example .env, then edit .env and set all required values (see the table above).
  3. Install dependencies: npm install.
  4. Start the server: npm start (runs node server.mjs). The process prints a URL such as http://localhost:3000 unless you set PORT.
  5. In your browser, open that URL, click Sign in, complete login at the provider, and you should land on /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.

Important snippets

Full source code is maintained in 3x3cu73/oidc-client. Below are the key pieces for quick integration.

.env.example

# 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

package.json

{
  "name": "tmp1-oidc-client",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node server.mjs"
  },
  "dependencies": {
    "openid-client": "^6.8.2"
  }
}

server.mjs - config and env parsing

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)
}

server.mjs - /auth route (PKCE + redirect)

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
    }

server.mjs - /callback route (token exchange)

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
    }

frontend/index.html

<!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>

frontend/welcome.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>