Authorization code flow, PKCE, refresh tokens, JWT, scopes, and security best practices.
Welcome everyone! Today we're diving into OAuth 2.0, the protocol that powers authorization across the modern web. Whether you're implementing social login, building an API, or integrating with third-party services, understanding OAuth is essential. We'll explore how it works, why it exists, and most importantly, how to implement it securely. By the end of this talk, you'll understand the flows, the terminology, and the security pitfalls to avoid. Let's get started.
So why does OAuth exist? Before OAuth, if you wanted a third-party app to access your Google Calendar, you had to give it your actual Google password. This was a security nightmare. Looking at the cards here, the first one shows the old way: the third-party app stores your password and has unlimited access forever. You can't revoke just one app without changing your password everywhere. The second card shows OAuth's solution: the app gets a scoped, time-limited token instead. You grant only the specific permissions it needs, and you can revoke access to one app without affecting others. The third card explains OAuth as a delegation model. You're authorizing an app to act on your behalf with specific permissions, without ever sharing your credentials. And as the fourth card shows, this is now the industry standard. Google, GitHub, Microsoft, Apple, and virtually every modern API uses OAuth for third-party access.
Understanding OAuth requires knowing the four roles in every flow. Looking at the first table, we have the Resource Owner, which is you, the user clicking allow. The Client is the app requesting access, like a to-do app wanting your Google Calendar. The Authorization Server is the service that issues tokens after you consent, like accounts dot google dot com. And the Resource Server hosts the actual protected data, like calendar dot googleapis dot com. The second table covers essential terms. Grant Type is the flow used to obtain tokens, like authorization code or client credentials. Scope is a permission string like read calendar. Access Token is the short-lived credential sent with API requests. Refresh Token is a long-lived credential used to get new access tokens. And Redirect URI is where the auth server sends the user after they grant consent. These terms will come up repeatedly, so keep them in mind.
Now let's look at the Authorization Code Flow with PKCE, which is the recommended flow for all applications, whether web, mobile, or single-page apps. Looking at the sequence diagram, the flow starts with the app generating a code verifier and code challenge. The app then sends the user to the authorization server with the code challenge in the URL. The auth server shows a consent screen, and when the user grants permission, it redirects back with an authorization code. Here's the crucial part: the app exchanges that code for tokens by sending it along with the code verifier. The auth server verifies that the SHA-256 hash of the verifier matches the original challenge. Only then does it issue the access token and refresh token. Finally, the app uses the access token to make API requests. The callout box explains why PKCE matters. Without it, a malicious app on the same device can intercept the authorization code from the redirect URI. With PKCE, the code is useless without the verifier, which never leaves the legitimate client. This prevents a whole class of attacks.
Let's see what PKCE looks like in code. Looking at this TypeScript implementation, step one generates the cryptographic verifier and challenge. The verifier is a random string between 43 and 128 characters. We then create the challenge by taking the SHA-256 hash of that verifier and base64url-encoding it. Notice the specific character replacements at the end to make it URL-safe. In step two, we build the authorization URL. We store the verifier in session storage because we'll need it later when the callback happens. Then we construct the authorization URL with all the required parameters: response type code, our client ID, the redirect URI, the requested scopes, and critically, the code challenge and code challenge method S256. When the user is redirected to this URL, they'll see the consent screen. After they approve, the authorization server will redirect back with a code, and we'll exchange that code along with our stored verifier for the actual tokens.
Not every scenario involves a human user clicking allow. OAuth defines different grant types for different situations. The first card shows Client Credentials, which is machine-to-machine authentication with no user involved. The app authenticates with its own client ID and secret to access its own resources. This is used for backend service-to-service calls. The second card covers Device Code Flow for devices without browsers, like smart TVs or command-line tools. The device shows a code, the user enters it on their phone, and the device polls until authorized. The third card explains the Refresh Token Grant, which exchanges a refresh token for a new access token without re-prompting the user. Importantly, refresh tokens should be rotated on every use so you can detect theft by monitoring for reuse. The last card mentions Implicit Flow, which is deprecated. It returned tokens directly in the URL fragment with no code exchange and no PKCE. This is deprecated in OAuth 2.1 because tokens in URLs leak via browser history, referrer headers, and server logs. Never use implicit flow in new applications.
Scopes are how you limit what an access token can do. The client requests scopes, the user approves them, and the authorization server includes the granted scopes in the token. Looking at this table, we see common examples. The openid scope gives access to the user's identity through an ID token. Profile gives you name, picture, and locale. Email provides the email address and verification status. For GitHub, read repos lets you read repository data, while write repos allows pushing commits. Google Calendar uses calendar dot readonly to view events. And offline access is the scope that grants you a refresh token for long-lived apps. The callout box emphasizes the principle of least privilege. Request only the scopes your app actually needs. Users abandon consent screens that ask for too much. If your app says it wants to read email, manage contacts, and access files when all it needs is calendar access, that's a major red flag that will hurt your conversion rate.
Most modern authorization servers issue JWTs, or JSON Web Tokens, as access tokens. Looking at this code, a JWT has three base64url-encoded parts: header, payload, and signature. The header specifies the algorithm, in this case RS256 for asymmetric signing, and includes a key ID for rotation. The payload contains the claims. We see iss for the issuer, sub for the subject or user ID, aud for the intended audience, exp for expiration, iat for issued at time, scope for granted permissions, and client ID for which app requested it. The verification comments at the bottom show what the resource server must do. First, decode the header and use the key ID to fetch the public key from the JWKS endpoint. Then verify the signature using RS256 and that public key. Check that the expiration is in the future. Verify the issuer matches what you expect. Confirm the audience includes your API's identifier. And critically, check that the scope claim includes the specific permission required for the endpoint being accessed. Skip any of these checks and you open yourself to token misuse.
Let's talk security best practices. Looking at these six cards, first: always use PKCE, even for server-side apps. PKCE is required in OAuth 2.1 for all grant types that use authorization codes. There's simply no reason to skip it. Second, use short-lived access tokens, fifteen to sixty minutes maximum. If a token leaks, the damage window is small. Use refresh tokens for longer sessions. Third, rotate refresh tokens with every use. Issue a new refresh token each time, and if the old one is reused, you know a theft has occurred and you can revoke the entire token family. Fourth, validate redirect URIs with exact matching only. No wildcards, no subdomain matching. Open redirectors allow attackers to steal codes through URL manipulation. Fifth, store tokens securely. Never in localStorage, which is accessible to any JavaScript including XSS attacks. Use httpOnly, Secure, SameSite strict cookies for web apps, or keychain for mobile. And sixth, use the state parameter with a random value to prevent CSRF attacks on your redirect URI. Verify it matches when you receive the callback.
Now let's look at common vulnerabilities and how to prevent them. The table shows six major attack vectors. Authorization code interception is when an attacker steals the code from the redirect, prevented by PKCE since the code is useless without the verifier. Token leakage via referrer happens when access tokens in URLs leak to third-party sites. Prevention: never put tokens in URLs or query parameters. CSRF on the redirect URI is when an attacker crafts a link that logs the victim into the attacker's account. The state parameter with a random nonce prevents this. Open redirect vulnerabilities arise from loose redirect URI matching. Use exact-match validation only. Insufficient scope validation is when your API checks that a token is valid but doesn't verify it has the required permissions. Always check the scope claim. And refresh token theft grants indefinite access, prevented by token rotation, reuse detection, and binding tokens to specific clients. The callout box emphasizes that the implicit flow is dead. OAuth 2.1 formally removes it. If your app still returns tokens in the URL fragment, migrate to authorization code with PKCE immediately.
Let's clarify the difference between OAuth and OpenID Connect. OAuth 2.0 is an authorization protocol. It answers the question: can this app access my data? OpenID Connect is an identity layer built on top of OAuth that adds authentication, answering the question: who is this user? Looking at the comparison table, OAuth's purpose is authorization, while OIDC handles authentication. OAuth uses access tokens which can be opaque or JWT format. OIDC always uses a JWT ID token plus an access token. OAuth doesn't standardize how to get user info, while OIDC provides standardized claims like sub, name, and email. OAuth has no standard discovery mechanism, but OIDC provides the well-known openid-configuration endpoint. And for scopes, OAuth uses app-defined values like read repos, while OIDC defines standard scopes like openid, profile, and email. The callout explains when you need OIDC. If your app has a login with Google button, you need OpenID Connect, not just OAuth. OAuth alone tells you the user granted access but not who they are. The ID token from OIDC gives you verified identity claims to actually identify the user.
Let's look at a complete, production-ready OAuth callback handler with all the security checks in place. This TypeScript function handles the redirect after the user grants consent. Step one verifies the state parameter for CSRF protection. We compare the state from the URL with what we saved in a cookie. If they don't match, we throw an error because this could be a CSRF attack. Step two exchanges the authorization code for tokens. Notice we're sending the PKCE verifier from our cookie along with the code. The authorization server will verify these match before issuing tokens. Step three verifies the ID token if we're using OpenID Connect. We check the issuer and audience claims match our expected values. Step four creates the session. This is critical: we store tokens in httpOnly cookies, never in localStorage. The cookie is set with httpOnly, secure, sameSite strict, and a max age. This prevents JavaScript access and cross-site attacks. Finally, we redirect to the dashboard. This pattern ensures every security best practice we've discussed is actually implemented in your callback handler.
Let's wrap up with the key takeaways. First, remember that OAuth is authorization, not authentication. Use OpenID Connect when you need to actually identify users. Second, Authorization Code flow with PKCE is the only recommended flow for modern applications. Implicit flow is deprecated, and you should use PKCE even for server-side apps. Third, treat tokens as credentials. Use short-lived access tokens, rotate refresh tokens on every use, store everything in httpOnly cookies, and never put tokens in URLs. And fourth, validate everything. Use the state parameter for CSRF protection, enforce exact redirect URI matching, check scopes on every API call, and verify JWT signatures plus all the standard claims. OAuth is powerful when implemented correctly, but security requires diligence at every step. Thank you for your attention, and I hope this gives you the foundation to implement OAuth securely in your applications.
Hands-on implementation guides with detailed code examples, step-by-step instructions, and expanded explanations for each topic.