{"id":49865938,"url":"https://github.com/lukasniessen/oauth-explained","last_synced_at":"2026-05-15T03:00:22.746Z","repository":{"id":289168493,"uuid":"970190992","full_name":"LukasNiessen/oauth-explained","owner":"LukasNiessen","description":"OAuth explained with code snippet","archived":false,"fork":false,"pushed_at":"2025-05-07T13:34:43.000Z","size":8,"stargazers_count":66,"open_issues_count":0,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-05-15T03:00:16.494Z","etag":null,"topics":["authorization","iam","oauth","oauth2"],"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-21T16:14:42.000Z","updated_at":"2026-04-27T22:56:27.000Z","dependencies_parsed_at":"2026-05-15T03:00:11.684Z","dependency_job_id":null,"html_url":"https://github.com/LukasNiessen/oauth-explained","commit_stats":null,"previous_names":["lukasniessen/oauth-explained"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/LukasNiessen/oauth-explained","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LukasNiessen%2Foauth-explained","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LukasNiessen%2Foauth-explained/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LukasNiessen%2Foauth-explained/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LukasNiessen%2Foauth-explained/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LukasNiessen","download_url":"https://codeload.github.com/LukasNiessen/oauth-explained/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LukasNiessen%2Foauth-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":["authorization","iam","oauth","oauth2"],"created_at":"2026-05-15T03:00:07.021Z","updated_at":"2026-05-15T03:00:22.739Z","avatar_url":"https://github.com/LukasNiessen.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# OAuth Explained\n\n## The Basic Idea\n\nLet's say LinkedIn wants to let users import their Google contacts.\n\nOne obvious (but terrible) option would be to just ask users to enter their Gmail email and password directly into LinkedIn. But giving away your actual login credentials to another app is a huge security risk.\n\nOAuth was designed to solve exactly this kind of problem.\n\nNote: So OAuth solves an authorization problem! Not an authentication problem. See [here][ref1] for the difference. OAuth is an abbreviation for Open Authorization.\n\n## Super Short Summary\n\n- User clicks “Import Google Contacts” on LinkedIn\n- LinkedIn redirects user to Google's OAuth consent page\n- User logs in and approves access\n- Google redirects back to LinkedIn with a one-time code\n- LinkedIn uses that code to get an access token from Google\n- LinkedIn uses the access token to call Google's API and fetch contacts\n\n## More Detailed Summary\n\nSuppose LinkedIn wants to import a user's contacts from their Google account.\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. A user visits LinkedIn and clicks \"Import Google Contacts\"\n3. LinkedIn redirects the user to Google's authorization endpoint:\n   https://accounts.google.com/o/oauth2/auth?client_id=12345\u0026redirect_uri=https://linkedin.com/oauth/callback\u0026scope=contacts\n\n- client_id is the before mentioned client id, so Google knows it's LinkedIn\n- redirect_uri is very important. It's used in step 6\n- in scope LinkedIn tells Google how much it wants to have access to, in this case the contacts of the user\n\n4. The user will have to log in at Google\n5. Google displays a consent screen: \"LinkedIn wants to access your Google contacts. Allow?\" The user clicks \"Allow\"\n6. Google generates a one-time authorization code and redirects to the URI we specified: redirect_uri. **It appends the one-time code as a URL parameter**.\n   - So the URL could be https://linkedin.com/oauth/callback?code=one_time_code_xyz\n7. Now, LinkedIn makes a server-to-server request (not a redirect) to Google's token endpoint and receive an access token (and ideally a refresh token)\n8. **Finished**. Now LinkedIn can use this access token to access the user's Google contacts via Google's API\n\n---\n\n**Question:**\n_Why not already send the 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## Security Note: Encryption\n\nOAuth 2.0 does **not** handle encryption itself. It relies on HTTPS (SSL/TLS) to secure sensitive data like the client_secret and access tokens during transmission.\n\n## Security Addendum: The state Parameter\n\nThe state parameter is critical to prevent cross-site request forgery (CSRF) attacks. It's a unique, random value generated by the third-party app (e.g., LinkedIn) and included in the authorization request. Google returns it unchanged in the callback. LinkedIn verifies the state matches the original to ensure the request came from the user, not an attacker.\n\nFor an example of how a CSRF attack would work without the state param, see [here][csrf-ref].\n\n## OAuth 1.0 vs OAuth 2.0 Addendum:\n\nOAuth 1.0 required clients to cryptographically sign every request, which was more secure but also much more complicated. OAuth 2.0 made things simpler by relying on HTTPS to protect data in transit, and using bearer tokens instead of signed requests.\n\n## OIDC\n\nIf you want to use OAuth for authentication, you should use OIDC. It's a protocol that builds on top of OAuth 2.0. I wrote a very similar guide about it here: https://github.com/LukasNiessen/oidc-explained\n\n## Code Example: OAuth 2.0 Login Implementation\n\nBelow is a standalone Node.js example using Express to handle OAuth 2.0 login with Google, storing user data in a SQLite database.\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 jwksClient = require(\"jwks-rsa\");\n\nconst app = express();\nconst db = new sqlite3.Database(\":memory:\");\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.GOOGLE_CLIENT_ID;\nconst CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;\nconst REDIRECT_URI = \"https://example.com/oauth2/callback\";\nconst SCOPE = \"openid profile email\";\n\n// JWKS client to fetch Google's public keys\nconst jwks = jwksClient({\n  jwksUri: \"https://www.googleapis.com/oauth2/v3/certs\",\n});\n\n// Function to verify JWT\nasync function verifyIdToken(idToken) {\n  return new Promise((resolve, reject) =\u003e {\n    jwt.verify(\n      idToken,\n      (header, callback) =\u003e {\n        jwks.getSigningKey(header.kid, (err, key) =\u003e {\n          callback(null, key.getPublicKey());\n        });\n      },\n      {\n        audience: CLIENT_ID,\n        issuer: \"https://accounts.google.com\",\n      },\n      (err, decoded) =\u003e {\n        if (err) return reject(err);\n        resolve(decoded);\n      }\n    );\n  });\n}\n\n// Generate a random state for CSRF protection\napp.get(\"/login\", (req, res) =\u003e {\n  const state = crypto.randomBytes(16).toString(\"hex\");\n  req.session.state = state; // Store state in session\n  const authUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${CLIENT_ID}\u0026redirect_uri=${REDIRECT_URI}\u0026scope=${SCOPE}\u0026response_type=code\u0026state=${state}`;\n  res.redirect(authUrl);\n});\n\n// OAuth callback\napp.get(\"/oauth2/callback\", async (req, res) =\u003e {\n  const { code, state } = req.query;\n\n  // Verify state to prevent CSRF\n  if (state !== req.session.state) {\n    return res.status(403).send(\"Invalid state parameter\");\n  }\n\n  try {\n    // Exchange code for tokens\n    const tokenResponse = await axios.post(\n      \"https://oauth2.googleapis.com/token\",\n      {\n        code,\n        client_id: CLIENT_ID,\n        client_secret: CLIENT_SECRET,\n        redirect_uri: REDIRECT_URI,\n        grant_type: \"authorization_code\",\n      }\n    );\n\n    const { id_token } = tokenResponse.data;\n\n    // Verify ID token (JWT)\n    const decoded = await verifyIdToken(id_token);\n    const { sub: subject, name, email } = decoded;\n\n    // Check if user exists in federated_credentials\n    db.get(\n      \"SELECT * FROM federated_credentials WHERE provider = ? AND subject = ?\",\n      [\"https://accounts.google.com\", 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, \"https://accounts.google.com\", subject],\n                (err) =\u003e {\n                  if (err) return res.status(500).send(\"Database error\");\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              res.send(`Logged in as ${user.name} (${user.email})`);\n            }\n          );\n        }\n      }\n    );\n  } catch (error) {\n    res.status(500).send(\"OAuth or JWT verification error\");\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[ref1]: https://stackoverflow.com/questions/6556522/authentication-versus-authorization\n[csrf-ref]: https://stackoverflow.com/questions/35985551/how-does-csrf-work-without-state-parameter-in-oauth2-0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukasniessen%2Foauth-explained","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flukasniessen%2Foauth-explained","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukasniessen%2Foauth-explained/lists"}