NewFresh guides on DevOps, AI, cloud and security — read the latest
Security
Security

OAuth 2.0 Explained: How "Sign in with Google" Really Works

A deep, engineer-focused guide to OAuth 2.0, OpenID Connect, the Authorization Code Flow with PKCE, JWTs, and how "Sign in with Google" social login really works — step by step.

A secure container build pipeline with signed artifacts and verification checkpoints.

Every time you click "Sign in with Google," a precisely choreographed exchange unfolds across your browser, the app you're using, and Google's servers — all in a few hundred milliseconds, and crucially, without the app ever seeing your Google password. That choreography is OAuth 2.0, extended by OpenID Connect. This guide explains both from first principles, then traces the exact requests and responses so you can build, debug, and secure social login with confidence.

🔎 Who this is for — Engineers with beginner-to-intermediate OAuth knowledge who want a precise mental model: software engineers wiring up login, DevOps and platform engineers securing services, and cloud architects designing identity across Google Cloud, AWS, and Microsoft Entra ID.

The vocabulary, in one table

OAuth conversations collapse the moment two people use the same word for different things. Here is the entire glossary up front; the rest of the article expands each row.

TermOne-line definitionAnswers the question
Authentication (AuthN)Proving who a user is."Are you really Ashutosh?"
Authorization (AuthZ)Deciding what an identity may do."Are you allowed to read these files?"
OAuth 2.0A framework for delegated authorization — granting an app scoped access without sharing a password."May this app act on my behalf?"
OpenID Connect (OIDC)A thin identity layer on top of OAuth 2.0 that adds verified login."Who just logged in?"
SSOSingle Sign-On — one login session reused across many apps."Why didn't it ask me to log in again?"
Access TokenA short-lived key that lets a client call an API."What unlocks this resource?"
ID TokenA signed JWT asserting a user's identity (OIDC only)."What proves who the user is?"
Refresh TokenA long-lived credential used to mint new access tokens."How do we stay logged in?"
JWTJSON Web Token — a compact, signed, self-describing token format."What shape is the token?"

1. The Problem OAuth Solves

Imagine it's 2008. You sign up for a photo-printing site, and it offers to import your contacts so you can invite friends. To do that, it asks for your email address and your email password. You type them in. The site logs into your webmail as you, scrapes your address book, and — you hope — does nothing else.

This was the everyday reality before OAuth, and it has a name: the password anti-pattern. Handing your password to a third party is catastrophic on every axis that matters:

  • No scoping. A password is all-or-nothing. The photo site wanted your contacts but received the keys to your entire mailbox, drafts, and settings.
  • No expiry. Access lasted until you changed your password — and you probably never did.
  • No revocation. To cut off one app, you had to change your password, which broke every other app you'd trusted.
  • Plaintext exposure. Many apps stored the password to reuse it later. One breach leaked your real credentials, not a disposable token.
  • It trained users to be phished. Normalizing "type your Gmail password into this random site" is exactly the behavior security teams spend fortunes trying to untrain.

⛔ The core insight — Sharing a password shares your identity. What you actually want to share is a narrow, revocable, time-boxed permission. OAuth exists to make that distinction real.

A short history

The industry converged on a fix. OAuth 1.0 appeared in 2007 and was standardized as RFC 5849 (2010), but its request-signing scheme was painful to implement correctly. OAuth 2.0 (RFC 6749, October 2012) replaced signatures with bearer tokens over mandatory TLS and became the foundation beneath "Sign in with Google," "Continue with GitHub," and virtually every modern API authorization scheme.

💡 Analogy: the valet key — A premium car comes with a valet key that opens the door and starts the engine but won't unlock the glovebox or the trunk, and can be deactivated independently of your master key. OAuth issues software valet keys: scoped, revocable, and never your master password.

2. What Is OAuth 2.0?

OAuth 2.0 is an authorization framework, not an authentication protocol. Read that twice — it is the single most important and most misunderstood sentence in this entire topic. OAuth's job is to let a user (the resource owner) grant a third-party application limited, scoped access to resources hosted somewhere else, without revealing their credentials to that application.

It is a framework rather than a single rigid protocol because RFC 6749 defines several "grant types" (flows) for different client situations, plus extension points that later specs — PKCE, OpenID Connect, token introspection, and more — build upon.

Core concepts

  • Delegation. The user delegates a slice of their authority to an app, rather than impersonating themselves by handing over a password.
  • Scopes. Permissions are explicit and granular — openid, email, profile, https://www.googleapis.com/auth/calendar.readonly, and so on. The user sees and consents to exactly these.
  • Tokens. The currency of OAuth. A token is a string that represents the granted permission, separate from any password.
  • Consent. The authorization server shows the user what is being requested and lets them approve or deny it.

The four roles

Every OAuth interaction is a conversation between four parties. Map them once and the rest of the protocol clicks into place.

RoleWhat it isIn the "Sign in with Google" example
Resource OwnerThe human who owns the data and grants access.You, the Google account holder.
Client ApplicationThe app requesting access on the user's behalf.The third-party app showing the button (e.g. a notes app).
Authorization ServerAuthenticates the user, gets consent, and issues tokens.Google's OAuth/OIDC service at accounts.google.com.
Resource ServerThe API that holds protected resources and accepts access tokens.A Google API (e.g. People, Drive) — or, for pure login, the app's own backend.

A useful distinction: the authorization server hands out tokens; the resource server consumes them. At Google these are different systems behind different hostnames. Conflating the two is a frequent source of confusion when reading flow diagrams.

Figure 1 — The four OAuth roles and the high-level flow of trust between them: the user (via a browser) clicks "Sign in with Google" in the client app, which redirects to Google's authorization server; after the user authenticates and consents, Google issues tokens to the client, which then calls the resource server (Google APIs) with the access token.

💡 Analogy: the concert wristband — Buy a ticket (authenticate at the box office = authorization server) and you get a wristband (access token). The bartender (resource server) doesn't re-check your ID or call the box office — the wristband itself proves you're allowed in. A VIP band grants more (a broader scope). Lose it after the show (expiry) and it's worthless.

3. Understanding Tokens

Tokens are how OAuth replaces a password with something better. There are three that matter, and mixing them up is the root cause of a remarkable share of production security bugs. Keep one rule in mind: each token has exactly one intended audience and one intended job.

Access Token

An access token is a short-lived credential the client presents to a resource server to call an API. It is defined for HTTP use as a Bearer token in RFC 6750 — "bearer" meaning whoever holds it can use it, like cash. Its audience is the API, not the client; the client should treat it as an opaque string and never try to parse or trust its contents. Access tokens are deliberately short-lived (often 5–60 minutes) so that a leaked one expires quickly.

HTTP — using an access token

GET /v1/userinfo HTTP/1.1
Host: openidconnect.googleapis.com
Authorization: Bearer ya29.a0AfH6SM…opaque…Qx7

Refresh Token

A refresh token is a longer-lived credential used to obtain new access tokens after the current one expires — without dragging the user back through a login screen. It is highly sensitive: it never goes to a resource server, only to the authorization server's token endpoint, and it must be stored securely (server-side, or in platform-secured storage on mobile). Modern guidance strongly favors refresh token rotation, covered below.

ID Token

The ID token is the piece OAuth 2.0 by itself does not provide — it is introduced by OpenID Connect. It is always a JWT, it is signed by the identity provider, and its audience is the client. It exists to answer one question: who is the user that just authenticated? The client reads and validates it; an API generally does not. This separation is the entire reason OIDC exists, and we return to it in Section 5.

⚠️ The #1 token confusion — An access token authorizes API calls; an ID token identifies the user. Never use an access token to decide who someone is, and never send an ID token to a resource server as if it were an API key. Different audiences, different jobs.

JWT structure

A JSON Web Token (RFC 7519) is three Base64URL-encoded parts joined by dots: header.payload.signature. The header and payload are JSON; the signature lets the recipient verify the token wasn't forged or tampered with. Critically, the first two parts are encoded, not encrypted — anyone can read them, so never put secrets in a JWT payload.

A JWT, color-decoded

eyJhbGciOiJSUzI1NiIsImtpZCI6IjdkO…  ← HEADER   {"alg":"RS256","kid":"7d…","typ":"JWT"}
.
eyJpc3MiOiJodHRwczovL2FjY291bnRz…  ← PAYLOAD  {"iss":"https://accounts.google.com", …}
.
NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd…  ← SIGNATURE  (RS256 over header.payload)

The signature is what makes a JWT trustworthy. Google signs ID tokens with the RS256 algorithm using a private key; your application verifies them using Google's matching public key, which it fetches from a published JWKS (JSON Web Key Set) endpoint. The kid (key ID) in the header tells you which public key to use, so Google can rotate keys without breaking anyone.

⛔ Never trust alg: none — Early JWT libraries accepted a none algorithm (no signature) and could be tricked into accepting forged tokens. Always pin expected algorithms (e.g. RS256), verify the signature against the provider's JWKS, and reject anything unsigned.

Token expiration and rotation

Lifetimes are a security dial. Short access-token lifetimes shrink the blast radius of a leak; refresh tokens preserve user convenience. Refresh token rotation tightens this further: each time a refresh token is used, the authorization server issues a new refresh token and invalidates the old one. If an attacker steals and replays a rotated token, the server detects the reuse of an already-spent token and can revoke the entire token family — turning theft into a detectable event. RFC 9700 (the 2025 OAuth Security Best Current Practice) recommends rotation or sender-constraining for refresh tokens issued to public clients.

PropertyAccess TokenID TokenRefresh Token
PurposeAuthorize API callsProve user identityGet new access tokens
Defined byOAuth 2.0 (RFC 6749/6750)OpenID ConnectOAuth 2.0
Audience (aud)Resource server / APIThe client appAuthorization server
FormatOpaque or JWTAlways a JWTOpaque (usually)
Read by the client?No (treat as opaque)Yes (validate & read claims)No
Typical lifetimeMinutes – 1 hourMinutes (login moment)Days – months
Sent toResource serverStays at the clientToken endpoint only

4. How "Sign in with Google" Really Works

Modern web and mobile apps use the Authorization Code Flow with PKCE (pronounced "pixy," Proof Key for Code Exchange, RFC 7636). It is the flow Google, Auth0, Okta, and every serious identity provider recommend today. We'll look at the whole sequence first, then dissect each step.

Figure 2 — The complete Authorization Code Flow with PKCE and OpenID Connect, end to end: the client generates a code_verifier/code_challenge, state, and nonce, then redirects to Google's /authorize; the user authenticates and consents at Google; Google returns an authorization code; the client exchanges the code plus code_verifier at the token endpoint (back channel) for access, ID, and refresh tokens; the client validates the ID token, sets a session cookie, and calls the API with the access token.

Notice the two channels. The front channel runs through the browser via redirects and is visible to the user and any browser-resident code. The back channel is a direct, server-to-server HTTPS call that exchanges the code for tokens. Tokens never travel the front channel — only a temporary authorization code does, and it is useless without the PKCE secret. That split is the heart of the flow's security.

Step 1 — The Authorization Request

When the user clicks the button, the client first generates a random code verifier and derives a code challenge from it (code_challenge = BASE64URL(SHA256(code_verifier))). It also generates a random state and a random nonce. Then it redirects the browser to Google's authorization endpoint:

Front channel — browser redirect to Google

GET https://accounts.google.com/o/oauth2/v2/auth
  ?response_type=code
  &client_id=1234567890-abc.apps.googleusercontent.com
  &redirect_uri=https://app.example.com/auth/callback
  &scope=openid%20email%20profile
  &state=xCSRF7Yq2k
  &nonce=n-9fK3pQ1z
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256

Each parameter earns its place: response_type=code selects the Authorization Code Flow; scope=openid … requests an OIDC login plus the user's email and profile; state protects against CSRF; nonce binds the eventual ID token to this exact request; and the two code_challenge* parameters arm PKCE.

Google now takes over: it authenticates the user however it deems sufficient — session cookie, password, passkey, MFA, none of which the client ever sees — then shows a consent screen listing the requested scopes. This is the moment the password anti-pattern is designed out of existence: credentials are entered only on Google's own domain.

Step 3 — The Authorization Code

On approval, Google redirects the browser back to the client's pre-registered redirect_uri with a short-lived, single-use authorization code and the original state:

Front channel — Google redirects back to the app

HTTP/1.1 302 Found
Location: https://app.example.com/auth/callback
  ?code=4/0AeanS0b8xQk9…short-lived…
  &state=xCSRF7Yq2k

The client's first action is to confirm the returned state equals the value it generated in Step 1. A mismatch means the response may have been forged, and the request is dropped. The code itself is intentionally near-worthless on its own: it expires in seconds and, thanks to PKCE, cannot be redeemed without the matching code verifier.

Step 4 — The Token Exchange

Now the client switches to the back channel. Its server makes a direct POST to Google's token endpoint, presenting the code and — the PKCE payoff — the original code_verifier:

Back channel — server-to-server token request

POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=4/0AeanS0b8xQk9…
&redirect_uri=https://app.example.com/auth/callback
&client_id=1234567890-abc.apps.googleusercontent.com
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&client_secret=GOCSPX-…   ← confidential (server-side) clients only

Google recomputes SHA256(code_verifier) and checks it against the code_challenge it stored in Step 1. If they match, the same client that started the flow is the one finishing it, and Google responds with the tokens:

Token response

{
  "access_token": "ya29.a0AfH6SM…",
  "expires_in": 3599,
  "scope": "openid email profile",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZ…",
  "refresh_token": "1//09abc…"
}

✅ Why PKCE matters even with a client secret — PKCE defeats authorization code interception: even if malware or a malicious app grabs the code from the redirect, it cannot exchange it without the verifier, which never left the legitimate client. Originally designed for mobile and SPAs, PKCE is now recommended for all clients (and is mandatory in the OAuth 2.1 draft).

Step 5 — ID Token Validation

The client must now validate the ID token before trusting a single claim in it. This is non-negotiable. At minimum:

  • Signature — verify against Google's public keys from its JWKS endpoint, using the kid in the header and the expected algorithm (RS256).
  • iss — must be https://accounts.google.com (or accounts.google.com).
  • aud — must equal your own client_id. This stops a token minted for another app from being replayed at yours.
  • exp / iat — the token must be unexpired (allow small clock skew).
  • nonce — must equal the value sent in Step 1, defeating replay.

Step 6 — Session Creation

Validation is the end of OAuth/OIDC, not the end of login. The protocol has answered "who is this user?" exactly once. The application now establishes its own session — typically by setting a signed, HttpOnly, Secure, SameSite cookie — so the user stays logged in without re-running the flow on every request. The Google tokens have done their job; the app's session takes over from here.

🔎 Discovery makes this configuration-free — You don't hard-code Google's endpoints. OIDC providers publish a discovery document at https://accounts.google.com/.well-known/openid-configuration listing the authorization, token, userinfo, and JWKS endpoints plus supported scopes and algorithms. Good client libraries read it automatically.

5. OAuth vs OpenID Connect

Here is the question that trips up even experienced engineers: if OAuth gives me an access token after the user logs in with Google, why can't I just use that as proof of who they are?

Why OAuth 2.0 alone is not authentication

Because an access token is a statement about authorization, not identity. It means "the bearer of this token may access scope X." It says nothing reliable, verifiable, or audience-bound about who the user is. Three concrete problems follow:

  • It's opaque and unaddressed to you. A Google access token is meant for Google's APIs, not your app. It carries no signature you can verify and no field that says "this login was for your client."
  • The token-substitution / "confused deputy" attack. If an app logs users in by calling a profile API with an access token, an attacker can inject a valid token obtained from a different app — and your app logs them in as that token's owner. Nothing bound the token to a login meant for your application.
  • No standard identity contract. Plain OAuth never standardized how to learn the user's identity, so everyone improvised incompatibly — and many improvised insecurely.

How OpenID Connect fixes it

OpenID Connect (OIDC), finalized in 2014, is a thin identity layer on top of OAuth 2.0. It keeps everything OAuth does and adds the missing identity contract:

  • The ID token — a signed JWT whose aud claim is your client_id and whose iss is the provider. You can cryptographically verify it was issued by Google, for your app, and hasn't been tampered with.
  • The nonce — binds the ID token to your specific authorization request, defeating replay and injection.
  • A standardized openid scope, UserInfo endpoint, discovery document, and well-defined claims (sub, email, name, …), so every compliant provider behaves the same way.

💡 The one-liner to rememberOAuth 2.0 is for authorization ("what can this app do?"). OpenID Connect is for authentication ("who is this user?"). "Sign in with Google" is OIDC; "let this app read my Google Calendar" is OAuth. Most real flows do both at once.

OAuth 2.0OpenID Connect (OIDC)
PurposeDelegated authorization — grant scoped access to resources.Authentication — verify and convey user identity.
Authentication supportNone by itself; using it for login is unsafe.First-class, standardized, verifiable login.
Tokens usedAccess token, refresh token.Adds the ID token (JWT) on top of OAuth tokens.
User identity infoNot standardized; no guaranteed identity claims.Standard claims (sub, email, name) + UserInfo endpoint.
Key parameterscope, stateAdds openid scope + nonce
DiscoveryOptional (RFC 8414 metadata)./.well-known/openid-configuration
Typical use cases"Allow app to access my Calendar / repos / files." API authorization, machine-to-machine."Sign in with Google/Apple/Microsoft." Social login, SSO.

6. Security Best Practices

OAuth's flexibility is also its footgun: most vulnerabilities come from how a flow is implemented, not from the protocol itself. The following practices reflect the current IETF guidance consolidated in RFC 9700 (Best Current Practice for OAuth 2.0 Security, January 2025) and the OAuth 2.1 draft.

PKCE — for every client

Proof Key for Code Exchange binds the authorization code to the client that requested it, neutralizing code-interception attacks. Once a mobile-only safeguard, PKCE is now recommended for confidential clients too and is mandatory for all clients in OAuth 2.1. Always use the S256 challenge method, never plain.

The state parameter

A random, per-request state value, checked on return, protects against CSRF on the redirect — it ensures the callback your app receives corresponds to a flow this user actually started. Bind it to the user's session, not a global constant.

The nonce

An OIDC nonce is generated by the client, sent in the authorization request, and must reappear inside the ID token. Validating it prevents token replay — an old or injected ID token won't carry the nonce your current request expects.

Redirect URI validation

The authorization server must compare the incoming redirect_uri against pre-registered values using exact string matching (RFC 9700), not prefix or wildcard matching. Loose matching enables open-redirect and code-leakage attacks where a code is bounced to an attacker-controlled URL.

Token storage

Where you keep tokens decides how badly an XSS bug hurts you.

  • Browser SPAs: avoid storing tokens in localStorage/sessionStorage — any injected script can read them. Prefer the Backend-for-Frontend (BFF) pattern, where a server-side component holds the tokens and the browser only ever sees an HttpOnly, Secure session cookie.
  • Mobile/native: use platform secure storage (iOS Keychain, Android Keystore/EncryptedSharedPreferences), never plain files or logs.
  • Servers: protect refresh tokens like passwords — encrypted at rest, access-controlled, rotated.

Refresh token rotation

Issue a new refresh token on every use and invalidate the prior one. If a spent token is ever replayed, treat it as a breach signal: revoke the whole token family and force re-authentication. This converts silent theft into a detectable, containable event.

HTTPS everywhere

OAuth 2.0 relies on TLS for confidentiality — bearer tokens are only safe in transit because the channel is encrypted. Every endpoint (authorization, token, redirect, resource) must be HTTPS, with no mixed content and no "just for local dev" exceptions leaking into production.

⛔ Deprecated by RFC 9700 — do not use — The Implicit grant (response_type=token) returned tokens directly in the URL fragment and is now discouraged in favor of Authorization Code + PKCE. The Resource Owner Password Credentials (ROPC) grant — where the app collects the user's actual password — resurrects the very anti-pattern OAuth was built to kill. Both are deprecated.

Common attack vectors at a glance

AttackWhat it exploitsPrimary defense
Authorization code interceptionCode stolen from redirect (esp. mobile)PKCE (S256)
CSRF on callbackForged authorization responsestate bound to session
Token replay / injectionReusing a stolen/foreign tokennonce + aud validation
Open redirect / code leakageLoose redirect URI matchingExact-match registered URIs
XSS token theftTokens in localStorageHttpOnly cookies / BFF
Mix-up attackMultiple IdPs, ambiguous responsesValidate iss; distinct redirect URIs
Token leakage via RefererTokens in URLs logged/forwardedKeep tokens out of front channel

7. Common OAuth Mistakes Developers Make

Most OAuth incidents trace back to a short list of recurring errors. Here they are, with the symptom you'll actually see and how to fix it.

1. Treating the access token as proof of identity

Mistake: calling a profile API with an access token and logging the user in based on the result. Why it's dangerous: opens the token-substitution attack from Section 5. Fix: for login, request the openid scope and authenticate from a validated ID token whose aud equals your client_id.

2. Skipping ID token validation

Mistake: Base64-decoding the JWT payload and trusting it without verifying the signature, iss, aud, exp, and nonce. Fix: use a vetted OIDC library that validates against the provider's JWKS. Never roll your own JWT verification, and never accept alg: none.

3. Storing tokens in localStorage

Mistake: the default for many SPA tutorials. One XSS bug and every token is exfiltrated. Fix: BFF pattern with HttpOnly cookies, or at minimum keep tokens in memory only.

4. Omitting state or PKCE

Mistake: "it worked without them in testing." It will — right up until someone exploits the CSRF or code-interception gap. Fix: always send and verify both; let your library generate them.

5. Loose or wildcard redirect URIs

Mistake: registering https://app.example.com/* or reusing one URI across environments. Symptom: the dreaded redirect_uri_mismatch error — or worse, a code leak. Fix: register exact, per-environment callback URLs.

6. Putting a client secret in a public client

Mistake: shipping a client_secret inside a SPA bundle or mobile binary, where anyone can extract it. Fix: public clients use PKCE instead of a secret; only confidential (server-side) clients hold secrets.

7. Requesting too many scopes

Mistake: asking for broad scopes "just in case," scaring users and widening blast radius. Fix: request the minimum scopes you need, and add more via incremental authorization when a feature actually requires them.

8. Ignoring token expiry

Mistake: assuming an access token lives forever; users get logged out or API calls start returning 401. Fix: handle expiry gracefully with refresh tokens, and account for clock skew when checking exp (invalid_grant on the token endpoint is frequently a skew or a reused/expired code).

✅ Troubleshooting cheat sheetredirect_uri_mismatch → the URI isn't registered exactly. invalid_grant → expired/reused code, wrong code_verifier, or clock skew. invalid_client → bad client_id/secret. ID-token aud failures → you're validating against the wrong client_id. Signature failures → stale JWKS cache; refetch keys by kid.

8. OAuth in Modern Cloud Architectures

The same Authorization Code + PKCE / OIDC machinery underpins identity across every major cloud and developer platform. The vocabulary changes; the protocol doesn't.

Figure 3 — One protocol family, many providers: web/SPA, mobile, Kubernetes (kubectl/Dashboard), and CLI/CI clients authenticate against OIDC/OAuth providers (Google Identity, AWS Cognito, Microsoft Entra ID, GitHub), which in turn gate access to resource servers — Google Cloud APIs, AWS APIs via STS, Microsoft Graph/Azure, and your own microservices.

Google Cloud

Beyond consumer "Sign in with Google," Google Cloud uses OAuth/OIDC throughout: Cloud Identity and Identity Platform for end-user auth, Identity-Aware Proxy (IAP) to put an OIDC gate in front of apps, and Workload Identity Federation so external workloads exchange an OIDC token for short-lived Google credentials — eliminating long-lived service-account keys.

AWS Cognito

Amazon Cognito User Pools are a full OIDC provider: they issue ID, access, and refresh tokens, offer a Hosted UI, and federate to external IdPs (Google, Apple, or any OIDC/SAML provider such as Microsoft Entra ID). Identity Pools then exchange those tokens for temporary AWS credentials via STS AssumeRoleWithWebIdentity, mapping a logged-in user to a scoped IAM role.

Microsoft Entra ID (formerly Azure AD)

Microsoft Entra ID — renamed from Azure Active Directory in 2023 — is Microsoft's OIDC/OAuth 2.0 platform for consumer and enterprise identity. Developers register apps and use the MSAL libraries (Authorization Code + PKCE under the hood) to obtain tokens for Microsoft Graph and custom APIs. It underpins Microsoft 365 SSO and conditional access.

Kubernetes Dashboard & API authentication

Kubernetes itself authenticates users via OIDC: the kube-apiserver is configured with an --oidc-issuer-url and client ID, and kubectl presents an ID token (not an access token) in the Authorization: Bearer header. The API server validates the JWT and maps its claims to RBAC roles. The Kubernetes Dashboard follows the same model — you authenticate at your IdP and pass the resulting bearer token.

GitHub Login

"Sign in with GitHub" uses OAuth 2.0's Authorization Code Flow. GitHub distinguishes OAuth Apps (act on behalf of a user, broad scopes) from the newer GitHub Apps (fine-grained, per-repository permissions and short-lived installation tokens) — the latter being the recommended, least-privilege choice for integrations and CI.

Enterprise SSO

In the enterprise, OIDC (and its older sibling SAML) power single sign-on: employees authenticate once at a central IdP and silently access dozens of apps. Pairing OIDC login with SCIM provisioning gives security teams one place to grant and instantly revoke access — the organizational payoff of everything above.

9. End-to-End Example

Let's assemble everything into one concrete walkthrough. Our app is GravityNotes, a web app with a Node.js backend (a confidential client) that offers "Sign in with Google."

These three steps are exactly the flow from Section 4: the backend generates code_verifier/code_challenge/state/nonce and redirects to Google; the user authenticates and consents on accounts.google.com; Google returns an authorization code; the backend verifies state and exchanges the code (plus code_verifier) at oauth2.googleapis.com/token for an access_token, id_token, and refresh_token.

④ The backend validates and decodes the ID token

After verifying the signature against Google's JWKS and checking iss, aud, exp, and nonce, the decoded ID token looks like this:

Decoded ID token — header

{
  "alg": "RS256",
  "kid": "7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e",
  "typ": "JWT"
}

Decoded ID token — payload (claims)

{
  "iss": "https://accounts.google.com",
  "azp": "1234567890-abc.apps.googleusercontent.com",
  "aud": "1234567890-abc.apps.googleusercontent.com",
  "sub": "110248495921238986420",
  "email": "ashutosh@example.com",
  "email_verified": true,
  "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
  "nonce": "n-9fK3pQ1z",
  "name": "Ashutosh Gandhi",
  "given_name": "Ashutosh",
  "family_name": "Gandhi",
  "picture": "https://lh3.googleusercontent.com/a/default-user=s96-c",
  "iat": 1782259200,
  "exp": 1782262800
}

The claim that matters most for login is sub — Google's stable, unique identifier for this user. Always key your user records on sub, not on email, because email addresses can change owners over time while sub never does.

⑤ The backend establishes a session

The app looks up or creates a local user keyed on sub, then issues its own session cookie. The Google tokens have served their purpose.

Node.js / Express — callback handler (illustrative)

app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;

  // 1. CSRF check
  if (state !== req.session.oauthState) return res.status(403).send('Bad state');

  // 2. Exchange code + PKCE verifier for tokens (back channel)
  const tokens = await googleTokenExchange(code, req.session.codeVerifier);

  // 3. Validate the ID token: signature (JWKS), iss, aud, exp, nonce
  const claims = await verifyIdToken(tokens.id_token, {
    issuer: 'https://accounts.google.com',
    audience: process.env.GOOGLE_CLIENT_ID,
    nonce: req.session.nonce,
  });

  // 4. Find or create the local user, keyed on the stable 'sub'
  const user = await users.upsertByGoogleSub(claims.sub, {
    email: claims.email, name: claims.name, picture: claims.picture,
  });

  // 5. Establish our OWN session — HttpOnly, Secure, SameSite
  req.session.userId = user.id;
  res.cookie('sid', req.session.id, { httpOnly: true, secure: true, sameSite: 'lax' });
  res.redirect('/dashboard');
});

From the user's perspective, one click and a consent tap logged them in. Underneath, a code was exchanged over a back channel, an ID token was cryptographically verified, and a session was minted — and GravityNotes never saw their Google password.

10. Conclusion

OAuth 2.0 matters because it solved a real and dangerous problem: it replaced password sharing with scoped, revocable, time-boxed delegation. It let an entire ecosystem of apps interoperate without ever asking users to hand over their master credentials.

But OAuth 2.0 is about authorization, and login is about authentication — which is precisely why OpenID Connect is required for "Sign in with Google." OIDC adds the signed, audience-bound ID token and the nonce that together let your application prove who just logged in, rather than guessing from an access token never meant for that purpose.

The way modern applications implement secure social login is now well established: use the Authorization Code Flow with PKCE, request the openid scope, validate the ID token rigorously, store tokens safely (HttpOnly cookies or a BFF, never localStorage), rotate refresh tokens, enforce HTTPS, and lean on a battle-tested OIDC library or identity provider instead of hand-rolling the protocol.

Key takeaways

  • OAuth 2.0 = authorization; OpenID Connect = authentication. Use OIDC for login.
  • Access token ≠ ID token. Access tokens call APIs; ID tokens identify users. Never swap their jobs.
  • Authorization Code + PKCE is the default flow for web and mobile — PKCE for every client, per RFC 9700 and OAuth 2.1.
  • Always validate the ID token: signature (JWKS), iss, aud, exp, and nonce.
  • Protect the flow with state, exact-match redirect URIs, refresh-token rotation, and HTTPS everywhere.
  • Key users on sub, store tokens out of the browser's reach, and let a vetted library do the heavy lifting.

Frequently Asked Questions

What is the difference between OAuth 2.0 and OpenID Connect?

OAuth 2.0 is an authorization framework that grants an app scoped access to resources; OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that adds verifiable user authentication via a signed ID token. In short: OAuth answers "what can this app do?" and OIDC answers "who is this user?"

Is "Sign in with Google" OAuth or OpenID Connect?

Both. The underlying flow is OAuth 2.0's Authorization Code Flow with PKCE, and the login/identity portion is OpenID Connect. Requesting the openid scope is what turns an OAuth authorization into an authenticated login that returns an ID token.

What is PKCE, and do I still need it if I have a client secret?

PKCE (Proof Key for Code Exchange) binds the authorization code to the client that requested it, preventing code-interception attacks. Yes — modern guidance (RFC 9700 and the OAuth 2.1 draft) recommends or requires PKCE for all clients, including confidential ones with a secret, because it defends a different attack surface.

Where should I store OAuth tokens in a single-page app?

Not in localStorage or sessionStorage, which are readable by any injected script. Prefer a Backend-for-Frontend (BFF) that holds tokens server-side and exposes only an HttpOnly, Secure session cookie to the browser; if that's impossible, keep tokens in memory only.

What's the difference between an access token and an ID token?

An access token authorizes calls to a resource server (an API) and should be treated as opaque by the client. An ID token is a signed JWT issued to the client that proves the user's identity. Using an access token to determine who a user is — instead of a validated ID token — is a classic security mistake.


Standards referenced: OAuth 2.0 (RFC 6749), Bearer Token Usage (RFC 6750), PKCE (RFC 7636), JWT (RFC 7519), Authorization Server Metadata (RFC 8414), OAuth 2.0 Security Best Current Practice (RFC 9700, 2025), OpenID Connect Core 1.0, and the OAuth 2.1 draft.

Share
All articles