{"id":49865962,"url":"https://github.com/lukasniessen/oidc-explained","last_synced_at":"2026-05-15T03:01:06.293Z","repository":{"id":289502729,"uuid":"971476092","full_name":"LukasNiessen/oidc-explained","owner":"LukasNiessen","description":"OIDC explained with code snippet","archived":false,"fork":false,"pushed_at":"2025-05-07T13:35:03.000Z","size":9,"stargazers_count":39,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-05-15T03:00:05.479Z","etag":null,"topics":["authentication","oauth","oauth2","oidc"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/LukasNiessen.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-04-23T15:21:53.000Z","updated_at":"2026-04-27T22:56:18.000Z","dependencies_parsed_at":"2025-04-23T16:44:37.403Z","dependency_job_id":"4517bd73-bade-4ac6-82f8-74869037c05b","html_url":"https://github.com/LukasNiessen/oidc-explained","commit_stats":null,"previous_names":["lukasniessen/oidc-explained"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/LukasNiessen/oidc-explained","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LukasNiessen%2Foidc-explained","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LukasNiessen%2Foidc-explained/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LukasNiessen%2Foidc-explained/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LukasNiessen%2Foidc-explained/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LukasNiessen","download_url":"https://codeload.github.com/LukasNiessen/oidc-explained/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LukasNiessen%2Foidc-explained/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33051875,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-13T13:14:54.681Z","status":"online","status_checked_at":"2026-05-15T02:00:06.351Z","response_time":103,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","oauth","oauth2","oidc"],"created_at":"2026-05-15T03:00:32.044Z","updated_at":"2026-05-15T03:01:06.277Z","avatar_url":"https://github.com/LukasNiessen.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# OIDC Explained\n\nLet's say John is on LinkedIn and clicks _'Login with Google_'. He is now logged in without that LinkedIn knows his password or any other sensitive data. Great! But how did that work?\n\nVia OpenID Connect (OIDC). This protocol builds on OAuth 2.0 and is the answer to above question.\n\nI will provide a super short and simple summary, a more detailed one and even a code snippet. You should know what OAuth and JWTs are because OIDC builds on them. If you're not familiar with OAuth, see my other guide [here][ref_oauth_repo].\n\n## Super Short Summary\n\n- John clicks _'Login with Google_'\n- Now the usual OAuth process takes place\n  - John authorizes us to get data about his Google profile\n    - E.g. his email, profile picture, name and user id\n- **Important**: Now Google not only sends LinkedIn the access token as specified in OAuth, **but also a JWT.**\n- LinkedIn uses the JWT for authentication in the usual way\n  - E.g. John's browser saves the JWT in the cookies and sends it along every request he makes\n  - LinkedIn receives the token, verifies it, and sees \"_ah, this is indeed John_\"\n\n## More Detailed Summary\n\nSuppose LinkedIn wants users to log in with their Google account to authenticate and retrieve profile info (e.g., name, email).\n\n1. LinkedIn sets up a Google API account and receives a client_id and a client_secret\n   - So Google knows this client id is LinkedIn\n2. John clicks '_Log in with Google_' on LinkedIn.\n3. LinkedIn redirects to Google’s OIDC authorization endpoint:\n   https://accounts.google.com/o/oauth2/auth?client_id=...\u0026redirect_uri=...\u0026scope=openid%20profile%20email\u0026response_type=code\n   - As you see, LinkedIn passes client_id, redirect_id, scope and response_type as URL params\n     - **Important:** scope must include openid\n     - profile and email are optional but commonly used\n   - redirect_uri is where Google sends the response.\n4. John logs into Google\n5. Google asks: '_LinkedIn wants to access your Google Account_', John clicks '_Allow_'\n6. Google redirects to the specified redirect_uri with a one-time authorization code. For example:\n   https://linkedin.com/oidc/callback?code=one_time_code_xyz\n7. LinkedIn makes a server-to-server request to Google\n   - It passes the one-time code, client_id, and client_secret in the request body\n   - Google responds with an **access token and a JWT**\n8. **Finished.** LinkedIn now uses the JWT for authentication and can use the access token to get more info about John's Google account\n\n---\n\n**Question:**\n_Why not already send the JWT and access token in step 6?_\n\n**Answer:** To make sure that the requester is actually LinkedIn. So far, all requests to Google have come from the user's browser, with only the client_id identifying LinkedIn. Since the client_id isn't secret and could be guessed by an attacker, Google can't know for sure that it's actually LinkedIn behind this.\n\nAuthorization servers (Google in this example) use predefined URIs. So LinkedIn needs to specify predefined URIs when setting up their Google API. And if the given redirect_uri is not among the predefined ones, then Google rejects the request. See here: https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2.2\n\nAdditionally, LinkedIn includes the client_secret in the server-to-server request. This, however, is mainly intended to protect against the case that somehow intercepted the one time code, so he can't use it.\n\n## Addendum\n\nIn step 8 LinkedIn also verifies the JWT's signature and claims. Usually in OIDC we use asymmetric encryption (Google does for example) to sign the JWT. The advantage of asymmetric encryption is that the JWT can be verified by anyone by using the public key, including LinkedIn.\n\nIdeally, Google also returns a refresh token. The JWT will work as long as it's valid, for example hasn't expired. After that, the user will need to redo the above process.\n\nThe public keys are usually specified at the JSON Web Key Sets (JWKS) endpoint.\n\n## Key Additions to OAuth 2.0\n\nAs we saw, OIDC extends OAuth 2.0. This guide is incomplete, so here are just a few of the additions that I consider key additions.\n\n### ID Token\n\nThe ID token is the JWT. It contains user identity data (e.g., sub for user ID, name, email). It's signed by the IdP (Identity provider, in our case Google) and verified by the client (in our case LinkedIn). The JWT is used for authentication. Hence, while OAuth is for authorization, OIDC is authentication.\n\nDon't confuse Access Token and ID Token:\n\n- Access Token: Used to call Google APIs (e.g. to get more info about the user)\n- ID Token: Used purely for authentication (so we know the user actually is John)\n\n### Discovery Document\n\nOIDC providers like Google publish a JSON configuration at a standard URL:\n\n`https://accounts.google.com/.well-known/openid-configuration`\n\nThis lists endpoints (e.g., authorization, token, UserInfo, JWKS) and supported features (e.g., scopes). LinkedIn can fetch this dynamically to set up OIDC without hardcoding URLs.\n\n### UserInfo Endpoint\n\nOIDC standardizes a UserInfo endpoint (e.g., https://openidconnect.googleapis.com/v1/userinfo). LinkedIn can use the access token to fetch additional user data (e.g., name, picture), ensuring consistency across providers.\n\n### Nonce\n\nTo prevent replay attacks, LinkedIn includes a random nonce in the authorization request. Google embeds it in the ID token, and LinkedIn checks it matches during verification.\n\n### Security Notes\n\n- HTTPS: OIDC requires HTTPS for secure token transmission.\n\n- State Parameter: Inherited from OAuth 2.0, it prevents CSRF attacks.\n\n- JWT Verification: LinkedIn must validate JWT claims (e.g., iss, aud, exp, nonce) to ensure security.\n\n## Code Example\n\nBelow is a standalone Node.js example using Express to handle OIDC login with Google, storing user data in a SQLite database.\n\nPlease note that this is just example code and some things are missing or can be improved.\n\nI also on purpose did not use the library openid-client so less things happen \"behind the scenes\" and the entire process is more visible. In production you would want to use openid-client or a similar library.\n\nLast note, I also don't enforce HTTPS here, which in production you really really should.\n\n```javascript\nconst express = require(\"express\");\nconst axios = require(\"axios\");\nconst sqlite3 = require(\"sqlite3\").verbose();\nconst crypto = require(\"crypto\");\nconst jwt = require(\"jsonwebtoken\");\nconst session = require(\"express-session\");\nconst jwkToPem = require(\"jwk-to-pem\");\n\nconst app = express();\nconst db = new sqlite3.Database(\":memory:\");\n\n// Configure session middleware\napp.use(\n  session({\n    secret: process.env.SESSION_SECRET || \"oidc-example-secret\",\n    resave: false,\n    saveUninitialized: true,\n  })\n);\n\n// Initialize database\ndb.serialize(() =\u003e {\n  db.run(\n    \"CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)\"\n  );\n  db.run(\n    \"CREATE TABLE federated_credentials (user_id INTEGER, provider TEXT, subject TEXT, PRIMARY KEY (provider, subject))\"\n  );\n});\n\n// Configuration\nconst CLIENT_ID = process.env.OIDC_CLIENT_ID;\nconst CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET;\nconst REDIRECT_URI = \"https://example.com/oidc/callback\";\nconst ISSUER_URL = \"https://accounts.google.com\";\n\n// OIDC discovery endpoints cache\nlet oidcConfig = null;\n\n// Function to fetch OIDC configuration from the discovery endpoint\nasync function fetchOIDCConfiguration() {\n  if (oidcConfig) return oidcConfig;\n\n  try {\n    const response = await axios.get(\n      `${ISSUER_URL}/.well-known/openid-configuration`\n    );\n    oidcConfig = response.data;\n    return oidcConfig;\n  } catch (error) {\n    console.error(\"Failed to fetch OIDC configuration:\", error);\n    throw error;\n  }\n}\n\n// Function to generate and verify PKCE challenge\nfunction generatePKCE() {\n  // Generate code verifier\n  const codeVerifier = crypto.randomBytes(32).toString(\"base64url\");\n\n  // Generate code challenge (SHA256 hash of verifier, base64url encoded)\n  const codeChallenge = crypto\n    .createHash(\"sha256\")\n    .update(codeVerifier)\n    .digest(\"base64\")\n    .replace(/\\+/g, \"-\")\n    .replace(/\\//g, \"_\")\n    .replace(/=/g, \"\");\n\n  return { codeVerifier, codeChallenge };\n}\n\n// Function to fetch JWKS\nasync function fetchJWKS() {\n  const config = await fetchOIDCConfiguration();\n  const response = await axios.get(config.jwks_uri);\n  return response.data.keys;\n}\n\n// Function to verify ID token\nasync function verifyIdToken(idToken) {\n  // First, decode the header without verification to get the key ID (kid)\n  const header = JSON.parse(\n    Buffer.from(idToken.split(\".\")[0], \"base64url\").toString()\n  );\n\n  // Fetch JWKS and find the correct key\n  const jwks = await fetchJWKS();\n  const signingKey = jwks.find((key) =\u003e key.kid === header.kid);\n\n  if (!signingKey) {\n    throw new Error(\"Unable to find signing key\");\n  }\n\n  // Format key for JWT verification\n  const publicKey = jwkToPem(signingKey);\n\n  return new Promise((resolve, reject) =\u003e {\n    jwt.verify(\n      idToken,\n      publicKey,\n      {\n        algorithms: [signingKey.alg],\n        audience: CLIENT_ID,\n        issuer: ISSUER_URL,\n      },\n      (err, decoded) =\u003e {\n        if (err) return reject(err);\n        resolve(decoded);\n      }\n    );\n  });\n}\n\n// OIDC login route\napp.get(\"/login\", async (req, res) =\u003e {\n  try {\n    // Fetch OIDC configuration\n    const config = await fetchOIDCConfiguration();\n\n    // Generate state for CSRF protection\n    const state = crypto.randomBytes(16).toString(\"hex\");\n    req.session.state = state;\n\n    // Generate nonce for replay protection\n    const nonce = crypto.randomBytes(16).toString(\"hex\");\n    req.session.nonce = nonce;\n\n    // Generate PKCE code verifier and challenge\n    const { codeVerifier, codeChallenge } = generatePKCE();\n    req.session.codeVerifier = codeVerifier;\n\n    // Build authorization URL\n    const authUrl = new URL(config.authorization_endpoint);\n    authUrl.searchParams.append(\"client_id\", CLIENT_ID);\n    authUrl.searchParams.append(\"redirect_uri\", REDIRECT_URI);\n    authUrl.searchParams.append(\"response_type\", \"code\");\n    authUrl.searchParams.append(\"scope\", \"openid profile email\");\n    authUrl.searchParams.append(\"state\", state);\n    authUrl.searchParams.append(\"nonce\", nonce);\n    authUrl.searchParams.append(\"code_challenge\", codeChallenge);\n    authUrl.searchParams.append(\"code_challenge_method\", \"S256\");\n\n    res.redirect(authUrl.toString());\n  } catch (error) {\n    console.error(\"Login initialization error:\", error);\n    res.status(500).send(\"Failed to initialize login\");\n  }\n});\n\n// OIDC callback route\napp.get(\"/oidc/callback\", async (req, res) =\u003e {\n  const { code, state } = req.query;\n  const { codeVerifier, state: storedState, nonce: storedNonce } = req.session;\n\n  // Verify state\n  if (state !== storedState) {\n    return res.status(403).send(\"Invalid state parameter\");\n  }\n\n  try {\n    // Fetch OIDC configuration\n    const config = await fetchOIDCConfiguration();\n\n    // Exchange code for tokens\n    const tokenResponse = await axios.post(\n      config.token_endpoint,\n      new URLSearchParams({\n        grant_type: \"authorization_code\",\n        client_id: CLIENT_ID,\n        client_secret: CLIENT_SECRET,\n        code,\n        redirect_uri: REDIRECT_URI,\n        code_verifier: codeVerifier,\n      }),\n      {\n        headers: {\n          \"Content-Type\": \"application/x-www-form-urlencoded\",\n        },\n      }\n    );\n\n    const { id_token, access_token } = tokenResponse.data;\n\n    // Verify ID token\n    const claims = await verifyIdToken(id_token);\n\n    // Verify nonce\n    if (claims.nonce !== storedNonce) {\n      return res.status(403).send(\"Invalid nonce\");\n    }\n\n    // Extract user info from ID token\n    const { sub: subject, name, email } = claims;\n\n    // If we need more user info, we can fetch it from the userinfo endpoint\n    // const userInfoResponse = await axios.get(config.userinfo_endpoint, {\n    //   headers: { Authorization: `Bearer ${access_token}` }\n    // });\n    // const userInfo = userInfoResponse.data;\n\n    // Check if user exists in federated_credentials\n    db.get(\n      \"SELECT * FROM federated_credentials WHERE provider = ? AND subject = ?\",\n      [ISSUER_URL, subject],\n      (err, cred) =\u003e {\n        if (err) return res.status(500).send(\"Database error\");\n\n        if (!cred) {\n          // New user: create account\n          db.run(\n            \"INSERT INTO users (name, email) VALUES (?, ?)\",\n            [name, email],\n            function (err) {\n              if (err) return res.status(500).send(\"Database error\");\n\n              const userId = this.lastID;\n              db.run(\n                \"INSERT INTO federated_credentials (user_id, provider, subject) VALUES (?, ?, ?)\",\n                [userId, ISSUER_URL, subject],\n                (err) =\u003e {\n                  if (err) return res.status(500).send(\"Database error\");\n\n                  // Store user info in session\n                  req.session.user = { id: userId, name, email };\n                  res.send(`Logged in as ${name} (${email})`);\n                }\n              );\n            }\n          );\n        } else {\n          // Existing user: fetch and log in\n          db.get(\n            \"SELECT * FROM users WHERE id = ?\",\n            [cred.user_id],\n            (err, user) =\u003e {\n              if (err || !user) return res.status(500).send(\"Database error\");\n\n              // Store user info in session\n              req.session.user = {\n                id: user.id,\n                name: user.name,\n                email: user.email,\n              };\n              res.send(`Logged in as ${user.name} (${user.email})`);\n            }\n          );\n        }\n      }\n    );\n  } catch (error) {\n    console.error(\"OIDC callback error:\", error);\n    res.status(500).send(\"OIDC authentication error\");\n  }\n});\n\n// User info endpoint (requires authentication)\napp.get(\"/userinfo\", (req, res) =\u003e {\n  if (!req.session.user) {\n    return res.status(401).send(\"Not authenticated\");\n  }\n  res.json(req.session.user);\n});\n\n// Logout endpoint\napp.get(\"/logout\", async (req, res) =\u003e {\n  try {\n    // Fetch OIDC configuration to get end session endpoint\n    const config = await fetchOIDCConfiguration();\n    let logoutUrl;\n\n    if (config.end_session_endpoint) {\n      logoutUrl = new URL(config.end_session_endpoint);\n      logoutUrl.searchParams.append(\"client_id\", CLIENT_ID);\n      logoutUrl.searchParams.append(\n        \"post_logout_redirect_uri\",\n        \"https://example.com\"\n      );\n    }\n\n    // Clear the session\n    req.session.destroy(() =\u003e {\n      if (logoutUrl) {\n        res.redirect(logoutUrl.toString());\n      } else {\n        res.redirect(\"/\");\n      }\n    });\n  } catch (error) {\n    console.error(\"Logout error:\", error);\n\n    // Even if there's an error fetching the config,\n    // still clear the session and redirect\n    req.session.destroy(() =\u003e {\n      res.redirect(\"/\");\n    });\n  }\n});\n\napp.listen(3000, () =\u003e console.log(\"Server running on port 3000\"));\n```\n\n# Feedback ⌨️😊\n\nFeel free to contribute by submitting a PR or creating an issue.  \n**If this was helpful, you can show support by giving this repository a star! 🌟**\n\n# License\n\nMIT\n\n[ref_oauth_repo]: https://github.com/LukasNiessen/oauth-explained\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukasniessen%2Foidc-explained","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flukasniessen%2Foidc-explained","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukasniessen%2Foidc-explained/lists"}