Skip to content
Episode 87 · Sign in without a password

OAuth 2.0 and OpenID Connect: “Sign in with Google”, properly

Monday, 9:00: 34% of new guests abandon StayHub at the password form, and the 120-hotel chain wants its 2,400 staff to log in with their company accounts. Can StayHub do both without ever seeing their passwords?

How can an app use your Google Calendar without ever seeing your Google password? That's OAuth, and "Sign in with Google" builds on it.

30 min intermediateOAuth 2.0 roles and the authorization-code flowPKCE and state: binding an OAuth login to its initiatorOpenID Connect: id_token (identity) vs access token (permission)Scopes and consent: delegated, least-privilege accessAccess/refresh tokens, expiry and revocationAuthentication (who you are) vs authorization (what you may do)
  1. Problem

    A hotel app (StayHub) wants guests to sign in with their Google account, and later to add bookings to their Google Calendar, without StayHub ever handling Google passwords.

  2. Why the simple system fails

    Asking guests for their Google password gives StayHub total, permanent access and trains users to be phished. Ad-hoc redirect schemes that pass tokens in URLs, skip state, or trust any email claim let attackers log in as someone else.

  3. Engineering concept

    Delegation: the user authenticates at the provider and consents to specific scopes; the app receives a one-time code, redeems it over a back channel (proved by PKCE) for short-lived tokens, and verifies a signed id_token to learn who the user is.

  4. Technology

    OAuth 2.0 authorization-code flow + PKCE, state and nonce, scopes, access tokens, refresh tokens, OpenID Connect id_token (JWT) verified with the provider's JWKS.

  5. Trade-offs

    Social login removes password storage and signup friction but adds a dependency on the provider, redirect complexity, and account-linking edge cases. Running your own authorization server is a large, security-critical project; most teams use a provider or a hosted identity service.

01

The problem

2 min
Monday, 9:00. Product review.
  1. Maya · Product manager
    34% of new guests quit at the email + password form. They want "Sign in with Google" or Apple.
  2. Karim · CTO
    And the 120-hotel chain wants its 2,400 staff to log in with their company accounts.
  3. Lina · Senior engineer, your mentor
    Same idea for both: someone else checks the password.
02

The big idea

4 min
A valet key, not your master key
The big idea

OAuth gives an app limited access to an API without the password; OpenID Connect adds a signed statement of who logged in.

Like a hotel valet: you never hand over the master key. The valet gets a valet key that only starts the car, today. You get a signed guest card that proves who you are, and it doesn't start the car.

03

How it works

5 min
Sign in with Google: the authorization code flow with PKCE
  1. 1
    Send the guest to Google

    With our client_id, an exact redirect_uri, scopes openid email profile (what we may ask for), a random , a nonce and a challenge: the hash of a secret only our server keeps.

  2. 2
    Google checks the guest

    Password, two-factor, consent screen. StayHub never sees any of it.

  3. 3
    Back with a code

    Google redirects the browser to our callback with a one-time code and the same state. We compare state with this browser's session: that stops someone pushing their login into a guest's browser.

  4. 4
    Swap the code, server to server

    We send code + code_verifier + client secret straight to Google. A thief who grabbed the code from the URL lacks the verifier, so PKCE stops them.

  5. 5
    Verify the id_token

    The is a signed about the guest. Check signature, issuer, audience = our client_id, expiry, nonce. The access token is different: it's for calling an API, never proof of identity.

  6. 6
    Find the account, start a session

    Key the user on (issuer, sub), the provider's stable id. Email is just a label. Then issue our own HttpOnly session cookie.

04

Watch a decision being made

4 min
Worked example

You write the callback, Lina reviews

Write GET /auth/callback for "Sign in with Google". The redirect already stored state, nonce and the PKCE verifier in the guest's session.

  1. 1Check state first

    Compare the state in the URL with the session's, in constant time. Mismatch → 400.

    Why: Lina: "Without it, an attacker pushes their login into a guest's browser, and the guest saves a card into the attacker's account."

  2. 2Swap the code, then verify the id_token

    Send code + code_verifier to Google's token endpoint. Verify the id_token, then the nonce.

    ts
    const claims = await verifyJwt(tokens.id_token, {
      jwksUrl: GOOGLE_JWKS_URL, issuer: "https://accounts.google.com", audience: CLIENT_ID,
    });
    if (claims.nonce !== session.oauthNonce) throw new Error("nonce mismatch");

    Why: A valid Google token minted for another app must not log anyone into StayHub. The audience check stops that.

  3. 3Key the account on (iss, sub)

    Find or create the user by issuer + subject. Store the email as a contact detail, with email_verified.

    Why: Emails change, may be unverified or hidden behind a relay. sub is stable.

  4. 4Ask for more only when needed, then hand over

    No calendar access now; request calendar.events when a guest taps "Add to calendar". Regenerate the session id, set userId, discard the id_token.

    Why: Big consent screens scare guests, and a broad leaked token does broad damage. A fresh session id prevents session fixation.

The whole thing in one line: State, PKCE, audience, nonce, (issuer, sub): five checks, each blocking one attack. Then StayHub's own session takes over.
05

In real life

3 min
Two identity providers, two privacy choices

Sign in with Google: the real address

  • Shares the account's email (with consent) and whether it's verified
  • Tempting to use the email as the account key

Easy to integrate; don't mistake the email for the identity.

Sign in with Apple: the option to hide it

  • Publicly offers "Hide My Email": each app gets a unique relay address
  • Forces apps to rely on the stable user id

More private; breaks any login built on email.

06

What goes wrong

3 min
Keying accounts on email

Emails get reassigned, can be unverified, and differ between providers.

Instead

Key on (issuer, sub); link to an existing account only from a logged-in session with a verified email.

Using an access token as proof of identity

A token issued to another app for the same user could be replayed to log in to yours.

Instead

Authenticate with the id_token and check aud, iss, exp and nonce.

Tokens in URLs, logs or localStorage

URLs leak; localStorage is readable by any XSS; bearer tokens work for whoever holds them.

Instead

Tokens server-side, refresh tokens encrypted, only an HttpOnly session cookie in the browser.

07

Trade-offs

3 min
OptionFits whenYou pay
Own email + passwordGuests without Google/Apple; offered alongsideStoring hashes, building resets and two-factor
Social login via OIDC, built with a libraryConsumer signup friction mattersYou own the callback code; provider outages block logins
Hosted identity brokerMany providers, enterprise SSO, no auth teamPer-user fees, lock-in, one more critical dependency
Wednesday, 14:00. How to build it.
  1. Karim · CTO
    Buy a hosted identity service: Google, Apple, the chain's SSO, one integration. About €50,000 a month at ~9 million monthly active guests.
  2. Lina · Senior engineer, your mentor
    Google and Apple are two well-documented integrations. Build those with a mature library; use a broker only for enterprise SSO.
  3. Karim · CTO
    Then we own security-critical code.
08

Your job as tech lead

4 min

Claude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.

Decisions you own
  • Which flow: authorization code with , state and nonce. An agent may copy an older tutorial with the implicit flow; you decide, and you check.
  • What identifies an account: issuer plus sub, never the email.
  • Which and when: openid email profile at sign-in; calendar access only when the guest asks for it.
Tell Claude Code
Add Sign in with Google to the guest web app. Plan first, wait for my OK. Use code flow with PKCE, state and nonce through a maintained OIDC library. Scopes: openid email profile. On the callback: check state, exchange the code server-side, verify the id_token's signature, issuer, audience equal to our client id, expiry and nonce. Key users on (issuer, sub); never link accounts by email. Then set our own HttpOnly session cookie. Add tests for a wrong state, a wrong audience and an expired token.
Check before you accept
  • The redirect URL Claude shows contains code_challenge and state, and no secret.
  • The callback has all five id_token checks; a missing audience is the classic hole.
  • The user lookup searches by issuer and sub, not email.
Red flags
  • It uses response_type=token (implicit flow) or drops PKCE because "we have a client secret".
  • It logs in or merges accounts by matching the email claim.
  • It stores tokens in localStorage or prints them in logs "for debugging".
09

Check yourself

2 min
Q1/3Reasoning
If a request carries a valid Google access token for user X, StayHub can safely treat the caller as user X and log them in.
How confident are you?

Next episode: a review on a hotel's page starts quietly changing the email address of every guest who opens it.

Finished?

Mark it complete to update your skill map.

Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Interactive simulation, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
10

Technical deep dive

Every parameter has a job
Worked example

You write the callback, Lina reviews

Write GET /auth/callback for "Sign in with Google". The redirect already sent state, nonce and a PKCE challenge, stored in the guest's session.

  1. 1Check state first

    Compare the state in the URL with the one in this browser's session, in constant time. Mismatch → 400.

    Why: Lina's review: "Without it, an attacker can push their own login into a guest's browser (login CSRF), and the guest saves a card into the attacker's account."

  2. 2Swap the code on the back channel

    POST the code, the code_verifier, our client id and secret to Google's token endpoint.

    ts
    const tokens = await fetch(GOOGLE_TOKEN_URL, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code", code, redirect_uri: REDIRECT_URI,
        client_id: CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET!,
        code_verifier: session.pkceVerifier,
      }),
    }).then((r) => r.json());

    Why: The verifier proves we started this login. A stolen code is useless without it.

  3. 3Verify the id_token

    Check the signature with Google's published keys (JWKS), then iss, aud = our client id, exp, and the nonce from the session.

    ts
    const claims = await verifyJwt(tokens.id_token, {
      jwksUrl: GOOGLE_JWKS_URL, issuer: "https://accounts.google.com", audience: CLIENT_ID,
    });
    if (claims.nonce !== session.oauthNonce) throw new Error("nonce mismatch");

    Why: A valid Google token minted for another app must not log anyone into StayHub. The audience check is what stops that.

  4. 4Key the account on (iss, sub)

    Find or create the user by issuer + subject. Store the email as a contact detail, with email_verified.

    Why: Emails change, get reassigned, may be unverified or hidden behind a relay. sub is the provider's stable id for this person.

  5. 5Ask for more only when needed

    Don't request calendar access now. When a guest taps "Add to calendar", request calendar.events then, and store that refresh token encrypted, server-side.

    Why: Big consent screens scare guests away, and a leaked token with broad scopes does broad damage.

  6. 6Start our own session

    Regenerate the session id, set userId, and redirect to /my-bookings. Discard the id_token.

    Why: A fresh session id prevents session fixation. After login, StayHub's own HttpOnly cookie is what keeps the guest signed in.

The whole thing in one line: Your callback checks state, swaps the code with the PKCE verifier on the back channel, verifies the id_token's signature, audience and nonce, keys the guest on (issuer, sub), and hands over to StayHub's own session.
ArtifactLifetime (typical)AudienceWhere it lives
Authorization codeSeconds to ~10 min, single useOur token requestRedirect URL (public)
Access tokenMinutes to ~1 hThe API (e.g. Google Calendar)Server-side only
Refresh tokenDays to months, often rotatedThe authorization serverEncrypted, server-side only
id_tokenMinutes to ~1 hStayHub (aud = client_id)Verified once, then discarded
StayHub session cookieOur choiceStayHubHttpOnly, Secure cookie
Lifetimes vary by provider and configuration.
11

Interactive simulation

12

Visual diagram

Click a component

Authorization-code flow with PKCE and OIDC. Solid lines through the browser are the front channel; the token exchange is a direct back channel.

13

Analogy

  • A hotel valet keyYou keep your master keyOAuth and OIDCYou never share your password
  • A hotel valet keyValet key starts the car, can't open the bootOAuth and OIDCAccess token with limited scopes
  • A hotel valet keyValet key only works todayOAuth and OIDCToken expires quickly
  • A hotel valet keyFront desk checks your ID firstOAuth and OIDCThe login server
  • A hotel valet keyA signed guest card proving who you areOAuth and OIDCThe OIDC id token

Where the analogy breaks: a valet key is physical, so only one person holds it. A token is just text, so anyone who copies it can use it. That's why tokens must stay short-lived and out of URLs and logs.

14

Check your understanding

Q1/4Recall
StayHub adds a guest's booking to her Google Calendar. Match each OAuth role:
Resource owner
Client
Authorization server
Resource server
How confident are you?
15

Practice ladder

Step 1 · Trace

Which check stops which attack?

Scenario Warm-up

StayHub's login uses code + PKCE + state + nonce, verifies the id_token's signature, iss, aud and exp, and keys accounts on (iss, sub). Trace each attack and name the check that stops it:

  1. An attacker gets a valid Google id_token for their own account that was issued to a different app, and sends it to StayHub.
  2. An attacker starts a login with their own Google account, stops at the callback, and tricks a guest into opening /auth/callback?code=ATTACKER_CODE&state=….
  3. A malicious app on a guest's phone intercepts the code from the redirect and calls the token endpoint itself.
Step 2 · Complete

Fix the account lookup

Code Practice

Lina's review left two gaps in the login handler after the id_token is verified.

// claims = verified id_token claims
let user = await /* gap 1: find the existing account */;
if (!user) {
  user = await db.user.create({
    data: { iss: claims.iss, sub: claims.sub, email: claims.email, emailVerified: claims.email_verified },
  });
}
// A guest who is ALREADY logged in wants to link this provider to their account:
if (session.userId && session.userId !== user.id) {
  if (/* gap 2 */) return badRequest("Verify your email with the provider first");
  await linkIdentity(session.userId, claims.iss, claims.sub);
}
Step 3 · Solve

Design "Add to Google Calendar"

Design Practice

Maya wants a button on each booking: "Add to Google Calendar". Guests log in with Google, email or Apple. When a booking's dates change, the calendar event should update too, even if the guest isn't using the app.

Design it: which scope, when to ask for consent, which tokens you store and where, and what happens when the guest disconnects.

Step 4 · Decide

API keys or client credentials for partners?

Decision Stretch

After last episode's leak, channel-manager partners now have scoped API keys (their hotels only, rates:write, availability:read). The "short-lived" part of that fix is still open: today the keys never expire. Two options:

  • A. Keep scoped API keys, add yearly rotation reminders.
  • B. OAuth client credentials: each partner gets a client id + secret, exchanges them for 1-hour access tokens with scopes. ~3 weeks of work; 60 partners must change their integration over 3 months.

Write a short decision: "We chose X over Y because…, accepting the risk of…".

16

Explain it yourself

Explain to a junior developer what happens when a guest clicks “Sign in with Google” on StayHub, and why each of state, PKCE and the id_token checks exists.

0 words