DEV Community

Cover image for OAuth 2.0 and OpenID Connect: What "Sign in with Google" Actually Does
Arnav Sharma
Arnav Sharma

Posted on

OAuth 2.0 and OpenID Connect: What "Sign in with Google" Actually Does

You click "Sign in with Google" on some app you've never used before. A popup flashes, you pick your account, and three seconds later the app knows your name and email. You never typed a password into that app. It never saw your Google credentials. So how does it know who you are?

That's OAuth 2.0 and OpenID Connect doing their thing. Almost every app you use either consumes or implements this flow, and the confusion between OAuth and OIDC causes real security bugs in production. Worth understanding properly.


🔑 The problem OAuth solves

Remember the old days when apps would ask "give us your Gmail password so we can find your friends"? You'd hand over your actual credentials to some random third party. They could read your email, send messages as you, change your password. Terrible.

OAuth 2.0 fixes this with delegated authorization. Instead of giving an app your password, you tell Google "hey, let this app see my basic profile info." Google gives the app a limited, revocable token. The app never touches your credentials.

Four roles are involved:

  • Resource owner - that's you, the human with the Google account
  • Client - the app wanting access to your stuff
  • Authorization server - Google's login and consent screen
  • Resource server - the API that actually holds your data (like Google's People API)

OAuth is authorization, not authentication

Here's the thing people get wrong constantly. OAuth answers "what is this app allowed to do?" It does NOT answer "who is this user?"

An access token says "the bearer of this token can read profile data." It doesn't say "the bearer of this token IS jane@gmail.com." Those are different statements.

But developers needed identity, so they hacked it. They'd take an access token, call a /userinfo endpoint, and treat whatever came back as proof of identity. This worked-ish, but it wasn't standardized, it wasn't secure against token substitution attacks, and every provider did it differently. That's why OpenID Connect had to be invented. But I'm getting ahead of myself.

🧠 The authorization code flow, step by step

This is the flow that runs when you click "Sign in with Google." Here's what actually happens:

  1. The client redirects your browser to Google's authorization endpoint with client_id, redirect_uri, scope, state, and code_challenge
  2. You authenticate with Google (type your password, do 2FA, whatever)
  3. Google shows a consent screen ("This app wants to see your email and profile")
  4. You click "Allow" and Google redirects your browser back to the client's redirect_uri with a short-lived authorization code
  5. The client's backend takes that code, sends it to Google's token endpoint along with its client_secret and code_verifier, and gets back tokens
  6. The client calls the resource server API with the access token
Browser                 Client (Backend)          Google (Auth Server)
  |                          |                           |
  |--- click "Sign in" ---->|                           |
  |                          |--- redirect to Google -->|
  |<---------- redirect to consent screen --------------|
  |--- authenticate + consent ------------------------->|
  |<---------- redirect back with ?code=abc123 ---------|
  |--- pass code to backend->|                          |
  |                          |--- POST code + secret -->|
  |                          |<-- access_token, id_token|
  |                          |--- call API with token ->|
  |<--- "Welcome, Jane!" ---|                           |
Enter fullscreen mode Exit fullscreen mode

So why the intermediate code? Why not just give the token directly?

The code travels through the browser. Browsers log URLs, extensions can read them, your history stores them. But the token exchange in step 5 is server-to-server. The access token never appears in a URL or browser history. That's the whole point of this extra step.


The three tokens people mix up

Token Who it's for What it's for Format
Access token Resource server Grants API access Opaque (treat it that way even if it looks like a JWT)
Refresh token Authorization server only Gets a fresh access token when the current one expires Opaque, long-lived, store carefully
ID token The client itself Tells the client who the user is Always a JWT, by spec

Two classic mistakes here. First: sending an ID token to an API as if it were an access token. The ID token is for YOUR app to read, not for an API to accept. Second: inspecting an access token's contents because it happens to look like a JWT. The issuer never promised you a format. They could switch to an opaque string tomorrow and your code would break. Don't do it.

âš¡ OpenID Connect is the identity layer

OIDC is a thin standard layer on top of OAuth 2.0. You add openid to your scope parameter, and now you get back an ID token alongside your access token.

That ID token is a signed JWT with standard claims:

{
  "iss": "https://accounts.google.com",
  "sub": "110248495921238986420",
  "aud": "your-client-id.apps.googleusercontent.com",
  "exp": 1735689600,
  "iat": 1735686000,
  "nonce": "abc123randomvalue",
  "email": "jane@gmail.com",
  "name": "Jane Smith",
  "picture": "https://lh3.googleusercontent.com/a/photo.jpg"
}
Enter fullscreen mode Exit fullscreen mode

Adding scopes like profile and email gets you the obvious extra claims. There's also a discovery document at /.well-known/openid-configuration that tells your app where all the endpoints are, what signing algorithms are supported, and where to find the public keys.

But here's what matters: your client MUST validate the ID token's signature against the provider's public keys, check that iss matches the expected issuer, verify aud contains your client ID, confirm exp hasn't passed, and validate the nonce matches what you sent. Just decoding the JWT and trusting the payload is a security hole. Anyone can craft a JWT. The signature is what makes it trustworthy. (The JWT post covers token internals in detail.)

Grant types and which to use

Grant type Use when Notes
Authorization Code + PKCE Web apps, SPAs, mobile apps The default answer for basically everything now
Client Credentials Machine-to-machine, no user involved Service accounts talking to APIs
Device Code TVs, smart displays, CLIs with no browser Shows a code to type on another device
Implicit Never Deprecated. Returned tokens in the URL fragment. Don't use it
Password / ROPC Never Deprecated. The app handles raw credentials. Defeats the purpose of OAuth

A quick note on PKCE (pronounced "pixy"). A public client like a mobile app or SPA can't keep a client_secret actually secret because the code is on the user's device. So PKCE adds a proof-of-possession step: the client generates a random code_verifier, sends the SHA-256 hash of it (the code_challenge) on the initial authorize request, then proves possession by sending the original verifier during token exchange. If an attacker intercepts the authorization code, they can't exchange it without the verifier.

And honestly, PKCE is now recommended for confidential clients too. Not just public ones. There's no good reason to skip it.

Ways people get this wrong

  • Skipping the state parameter. That's your CSRF protection on the redirect. Without it, an attacker can trick your callback into associating their account with the victim's session
  • Skipping nonce validation on the ID token. That's replay protection
  • Treating a decoded-but-unverified JWT as trustworthy. Verification means checking the cryptographic signature, not just running base64decode
  • Requesting scopes way broader than needed. Don't ask for https://mail.google.com/ when you only need email
  • Storing refresh tokens somewhere JavaScript can reach (like localStorage). XSS plus accessible refresh tokens equals full account takeover
  • Using the implicit flow because some tutorial from 2016 told you to. Deprecated for good reasons

📌 Takeaways

  • OAuth 2.0 handles authorization (what an app can do); OIDC adds authentication (who the user is) on top of it
  • The authorization code flow keeps tokens off the browser by using a short-lived code as an intermediate step, exchanged server-to-server
  • Access tokens go to APIs and should be treated as opaque; ID tokens are JWTs meant for your client to validate and read
  • Always use PKCE, always validate state and nonce, and never use the implicit flow
  • Scopes limit what a token can do. They aren't user roles

Keep reading

Top comments (0)