什么是OAuth 2.0?授权访问与谷歌登录原理
Security

什么是OAuth 2.0?授权访问与谷歌登录原理

OAuth 2.0是RFC 6749开放授权协议,允许第三方应用访问用户在其他服务上的资源,而无需知道其密码。了解授权码流程、4个角色、范围(scope),以及谷歌/GitHub登录的底层原理。

系列文章: Bảo mật
  1. 1 什么是恶意软件?分类、特征及预防方法
  2. 2 什么是DDoS?识别迹象、应对方法与有效防御指南
  3. 3 什么是网络钓鱼?识别与防范在线欺诈
  4. 4 什么是DNS Sinkhole?DNS Sinkhole技术的应用与使用方法
  5. 5 什么是OAuth 2.0?授权访问与谷歌登录原理
  6. 6 什么是木马病毒?关于Trojan恶意软件的基本知识
  7. 7 Zero Trust 是什么?'永不信任,始终验证'安全模型
  8. 8 VPN是什么?虚拟专用网络与WireGuard、OpenVPN协议
  9. 9 MFA 是什么?多因素认证与 2FA 对比详解
  10. 10 什么是防火墙?在网络安全中的角色和功能
  11. 11 什么是SQL注入?数据库攻击与防护
  12. 12 什么是XSS?跨站脚本攻击与防护
✦ 快速摘要
OAuth 2.0是RFC 6749开放授权协议,允许第三方应用访问用户在其他服务上的资源,而无需知道其密码。了解授权码流程、4个角色、范围(scope),以及谷歌/GitHub登录的底层原理。
这篇文章怎么样?

OAuth 2.0是RFC 6749开放授权协议,允许第三方应用在不知道用户密码的情况下访问其在另一服务上的资源。这是"谷歌账号登录"、"GitHub登录"等功能的技术基础,每天被数十亿用户使用。

本文将深入探讨OAuth 2.0的工作原理,区分授权与身份验证,逐步解析授权码流程,并说明为何PKCE对SPA和移动应用至关重要。读完本文,你将清楚地了解每次点击"谷歌账号登录"按钮时底层发生了什么。

什么是OAuth 2.0?授权vs身份验证

首先,我们需要区分两个常被混淆的概念:身份验证(Authentication)授权(Authorization)

  • **身份验证(Authentication)**回答"你是谁?"——验证身份。当你输入密码时,系统验证你就是账户所有者。
  • **授权(Authorization)**回答"你被允许做什么?"——在身份确认后授予或限制访问权限。

OAuth 2.0是授权协议,而非身份验证协议。它解决的问题是:如何让一个应用(如Canva)在不知道你谷歌密码的情况下访问你的谷歌云盘?

一个实际的类比:当你在高档餐厅代客泊车时,服务员得到的是代客钥匙(valet key)——一把特殊的钥匙,只能启动引擎和停车,无法打开储物箱或邮箱。这正是OAuth 2.0的工作方式:你不是把谷歌账户的全部控制权交给Canva,而是发给它一把数字"代客钥匙"——访问令牌(access token)——只授权读取云盘文件,无法读取Gmail或更改密码。

OAuth 2.0标准由IETF在RFC 6749(2012年10月发布)中定义,是OAuth 1.0的完整替代版本,简化了流程并扩展了对更多客户端类型的支持。

OAuth 2.0的核心特点:

  • 应用获得有时限的访问令牌,而非密码。
  • 用户控制scope——已授权权限的具体列表。
  • 令牌可以随时撤销,无需更改密码。
  • 支持多种客户端类型:Web应用、移动应用、SPA、物联网设备。

OAuth 2.0中的4个角色

RFC 6749定义了完整OAuth 2.0流程中的四个角色。理解这四个角色有助于明确整个过程中各方的职责。

1. 资源所有者(Resource Owner)

资源所有者是终端用户——拥有资源并可以授予访问权限的实体。在"Canva访问谷歌云盘"的例子中,资源所有者是你——拥有谷歌账户和云盘文件的人。

资源所有者不一定是人。在机器对机器流程(Client Credentials)中,资源所有者和客户端可以是同一实体。

2. 客户端(Client)

客户端是代表资源所有者请求访问资源的应用。在我们的例子中,Canva就是客户端。客户端必须提前在授权服务器注册,以获取client_idclient_secret

客户端有两种类型:

  • 机密客户端(Confidential client):运行在服务器上的Web应用,可以安全保存client_secret
  • 公开客户端(Public client):SPA(React、Vue)或移动应用——无法安全存储密钥,因为代码运行在用户设备上。必须使用PKCE。

3. 授权服务器(Authorization Server)

授权服务器是在验证资源所有者身份并获得其同意后颁发令牌的服务。谷歌、GitHub、Facebook和微软都运营各自的授权服务器。

授权服务器公开两个主要端点:

  • 授权端点(Authorization Endpoint)/auth):接收授权请求,显示同意页面,返回授权码。
  • 令牌端点(Token Endpoint)/token):接收授权码,验证客户端,返回访问令牌和刷新令牌。

4. 资源服务器(Resource Server)

资源服务器是存储客户端想要访问数据的API。它在请求头中接受访问令牌,验证令牌,如果令牌有效且具有所需权限,则返回数据。

实际上,授权服务器和资源服务器通常部署在一起(例如,accounts.google.com颁发令牌,www.googleapis.com是资源服务器),但RFC 6749在概念上将它们分开。

授权码流程(Authorization Code Flow)——逐步解析

授权码流程是最广泛使用的授权类型,推荐用于Web应用和移动应用。这是最安全的流程,因为访问令牌永远不会经过浏览器。

1. 用户在应用中点击"谷歌账号登录"
2. 应用重定向 → https://accounts.google.com/o/oauth2/auth
       ?client_id=APP_CLIENT_ID
       &redirect_uri=https://myapp.com/callback
       &response_type=code
       &scope=openid email profile
       &state=RANDOM_CSRF_TOKEN

3. 用户登录并同意 → 谷歌重定向回:
       https://myapp.com/callback?code=AUTH_CODE&state=TOKEN

4. 应用后端向谷歌发送POST请求:
       https://oauth2.googleapis.com/token
       client_id, client_secret, code, redirect_uri, grant_type=authorization_code

5. 谷歌响应:
       { access_token, expires_in, refresh_token, id_token }

6. 应用调用API:
       GET https://www.googleapis.com/oauth2/v3/userinfo
       Authorization: Bearer ACCESS_TOKEN

逐步解析:

第1-2步:授权请求 应用构造包含以下参数的重定向URL:

  • client_id:已在谷歌注册的应用标识符。
  • redirect_uri:用户同意后谷歌将重定向回的URL(必须与注册URI精确匹配)。
  • response_type=code:指定授权码流程。
  • scope:请求的权限列表。
  • state:防止CSRF的随机随机数——应用必须在收到回调时验证此值。

第3步:授权码 用户在谷歌的同意页面登录并授权后,谷歌将code(授权码)附在redirect_uri后重定向回来——授权码的有效期约10秒,且只能使用一次。这是一个关键的安全特性:短暂的、一次性的授权码可以防止重放攻击。

第4步:令牌交换 应用后端(不是浏览器!)向谷歌的令牌端点发送POST请求,包含client_secret。这次交换直接通过HTTPS在服务器之间进行,不经过浏览器——这就是访问令牌不会出现在浏览器历史记录或日志中的原因。

第5步:令牌响应 谷歌返回:

  • access_token:用于调用API的令牌,通常1小时后过期。
  • expires_in:到期前的秒数。
  • refresh_token:用于在不需要用户交互的情况下获取新访问令牌的长期令牌(仅在请求offline_access范围时颁发)。
  • id_token:包含用户身份信息的JWT(仅在使用带openid范围的OIDC时存在)。

第6步:API调用 应用在调用资源服务器时,在Authorization: Bearer <token>头中包含访问令牌。这是RFC 6750定义的标准Bearer Token语法。

授权类型(Grant Types)

OAuth 2.0为不同使用场景定义了多种授权类型。每种授权类型都针对特定类型的客户端和场景设计。

授权码(推荐)

如上所述,这是以下场景最安全的授权类型:

  • Web应用(机密客户端):在服务器上使用client_secret
  • SPA和移动应用(公开客户端):使用PKCE代替client_secret

客户端凭证(Client Credentials)

用于**机器对机器(M2M)**流程——不涉及人类用户时。例如,微服务A需要调用微服务B的API。

POST /token
grant_type=client_credentials
client_id=SERVICE_A_ID
client_secret=SERVICE_A_SECRET
scope=read:orders write:inventory

服务A用client_idclient_secret进行身份验证,直接获取访问令牌,无需显示同意页面。

设备授权(Device Code)

用于没有浏览器或输入受限的设备:智能电视、游戏机、物联网设备、命令行工具。设备显示一个短URL和验证码;用户在手机或电脑上输入该URL,登录并授权;设备轮询授权服务器,直到收到令牌。

隐式流程(已废弃)

隐式流程曾用于CORS广泛支持之前的SPA。它直接在URL片段中返回访问令牌,跳过了授权码交换步骤——方便但不安全(令牌暴露在浏览器历史记录、Referer头中)。隐式流程已不再推荐;OAuth 2.1(草案)完全删除了它。SPA请使用授权码+PKCE。

OAuth 2.0 vs OpenID Connect(OIDC)

这是关于OAuth 2.0最容易被误解的方面之一:当你"谷歌账号登录"时,你不仅仅在使用OAuth 2.0——你在使用OpenID Connect(OIDC)

纯OAuth 2.0解决授权问题:授予应用访问资源的权限。它没有提供回答"这个用户是谁?"的标准机制。OAuth 2.0访问令牌是不透明的字符串——资源服务器知道令牌有效,但没有额外信息的话,不知道令牌属于哪个用户。

**OpenID Connect(OIDC)**是构建在OAuth 2.0之上的身份验证层,由OpenID基金会规范定义。OIDC增加了:

  1. ID Token:与访问令牌一起返回的JWT,包含sub(主体/用户ID)、nameemailpicture和其他用户身份声明。客户端无需额外API调用即可读取ID Token。

  2. /userinfo端点:标准化的资源服务器端点——用访问令牌调用它以获取JSON格式的用户信息。

  3. openid Scope:激活OIDC的特殊权限范围。当授权请求包含scope=openid时,授权服务器返回ID Token。

  4. 发现文档(Discovery Document)/.well-known/openid-configuration端点返回描述所有端点、支持的范围和签名密钥的JSON——客户端可以自动配置,无需硬编码URL。

总结:

  • OAuth 2.0:"Canva被允许读取我的谷歌云盘。"
  • OIDC:"我是张伟,邮箱z@gmail.com——这是谷歌的加密签名确认。"

大多数主流身份提供商(谷歌、GitHub、微软、Apple)在同一端点上同时实现了OAuth 2.0和OIDC。在scope中添加openid即切换到OIDC模式。

常见安全漏洞

不正确地实现OAuth 2.0可能导致严重漏洞。以下是最常见的错误:

1. 未验证redirect_uri——授权码劫持

这是最危险的漏洞。如果授权服务器未严格验证redirect_uri,攻击者可以:

  1. 构造包含redirect_uri=https://attacker.com/steal的链接。
  2. 将链接发送给受害者。
  3. 受害者登录;授权码被发送到attacker.com
  4. 攻击者用授权码换取访问令牌。

防护规则:

  • 与注册URI进行精确匹配——不使用通配符,不允许新子域名。
  • 不做前缀匹配检查https://myapp.com不够安全,攻击者可以使用https://myapp.com.evil.com)。
  • 拒绝任何不在白名单中的URI。

2. 忽略state参数——CSRF攻击

state参数是客户端创建的随机数,在授权请求中发送,并在回调时验证。如果应用不使用或不验证state

  1. 攻击者发起自己的授权请求并获得授权码。
  2. 攻击者将代码嵌入页面并诱骗受害者访问。
  3. 受害者的应用自动交换攻击者的代码,将攻击者的账户绑定到受害者的会话中。

始终为每个授权请求生成新的state,存储在会话中,并在回调中交换代码前先验证它。

3. URL中的访问令牌——日志泄露

已废弃的隐式流程和一些不谨慎的实现将访问令牌放在URL片段或查询字符串中。URL中的令牌可能通过以下途径泄露:

  • 浏览器历史记录。
  • 浏览器跳转到其他页面时的Referer头。
  • 服务器访问日志。
  • 代理日志。

始终通过Authorization: Bearer头传输令牌,绝不放在URL中。

4. SPA和移动应用不使用PKCE

公开客户端(SPA、移动应用)无法保护client_secret。没有PKCE,任何知道client_id(公开信息)的应用都可以用截获的授权码换取令牌。

SPA和移动应用的PKCE

PKCE(代码交换证明密钥,RFC 7636)解决了这个问题。思路:不使用静态的client_secret,每个授权请求生成一个随机的code_verifier和作为其哈希值的code_challenge

JavaScript
 1// SPA — PKCE flow (Web Crypto API)
 2async function generatePKCE() {
 3  const array = new Uint8Array(32);
 4  crypto.getRandomValues(array);
 5  const verifier = btoa(String.fromCharCode(...array))
 6    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
 7  const encoder = new TextEncoder();
 8  const data = encoder.encode(verifier);
 9  const digest = await crypto.subtle.digest('SHA-256', data);
10  const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
11    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
12  return { verifier, challenge };
13}

PKCE流程:

  1. SPA生成code_verifier(43-128个字符的随机字符串)。
  2. SPA用SHA-256对code_verifier进行哈希 → code_challenge
  3. SPA将code_challenge(不是verifier!)与授权请求一起发送。
  4. 授权服务器存储code_challenge
  5. SPA用授权码换取令牌时,发送原始的code_verifier
  6. 授权服务器对code_verifier进行哈希并与存储的code_challenge比较——只有在请求来自同一个发起流程的客户端时才会匹配。

如果攻击者截获了授权码,他们没有code_verifier → 无法换取令牌。PKCE将每个授权请求变成绑定到特定客户端实例的一次性密钥对。

从OAuth 2.1(草案)开始,PKCE对所有授权码流程都是强制要求的,包括机密客户端。

总结

OAuth 2.0是现代API生态系统的基础。请记住以下要点:

  • OAuth 2.0是授权,而非身份验证——需要"谷歌账号登录"时请使用OIDC。
  • 4个角色:资源所有者(用户)、客户端(应用)、授权服务器(谷歌/GitHub)、资源服务器(API)。
  • 授权码流程是推荐的流程——短暂的授权码,令牌在服务器间交换。
  • PKCE对SPA和移动应用是强制要求的——不使用client_secret,使用一次性的verifier/challenge对。
  • Scope实施最小权限原则——只请求你真正需要的权限。
  • 严格验证redirect_uri,始终使用state,绝不在URL中放置令牌。

什么是JWT?JSON Web Token与API身份验证

什么是2FA?双因素身份验证详解

什么是API?REST API工作原理

OAuth 2.0 is an open authorization protocol (RFC 6749) that lets third-party applications access a user's resources on another service without knowing their password. It is the foundation of "Sign in with Google", "Login with GitHub", and countless API integrations used by billions of people every day.

In this article, we will explore how OAuth 2.0 works, distinguish authorization from authentication, walk through every step of the Authorization Code Flow, and understand why PKCE is essential for SPAs and mobile apps. By the end, you will know exactly what happens under the hood every time you click "Sign in with Google".

What is OAuth 2.0? Authorization vs Authentication

First, let us clear up two concepts that are commonly confused: authentication and authorization.

  • Authentication answers the question "Who are you?" — it verifies identity. When you enter a password, the system authenticates that you are the account owner.
  • Authorization answers the question "What are you allowed to do?" — it grants or restricts access after identity has been established.

OAuth 2.0 is an authorization protocol, not an authentication protocol. It solves the problem: how can an application (say, Canva) access your Google Drive without you handing over your Google password?

Consider a practical analogy: when you park at an upscale restaurant, the valet is given a valet key — a special key that only allows starting the engine and parking, not opening the glove box or the trunk. That is exactly how OAuth 2.0 works: instead of giving Canva full control over your Google account, you issue it a digital "valet key" — an access token — scoped only to reading Drive files, unable to read Gmail or change your password.

The OAuth 2.0 standard is defined in RFC 6749 (published October 2012) by the IETF. It is a full replacement for OAuth 1.0, simplifying the protocol and adding support for a wider range of client types.

Key properties of OAuth 2.0:

  • Applications receive a time-limited access token, never the password.
  • Users control scopes — the specific list of granted permissions.
  • Tokens can be revoked at any time without changing the password.
  • Supports multiple client types: web apps, mobile apps, SPAs, IoT devices.

The 4 Roles in OAuth 2.0

RFC 6749 defines four roles in a complete OAuth 2.0 flow. Understanding these roles clarifies who does what throughout the entire process.

1. Resource Owner

The Resource Owner is the end user — the entity that owns the resource and can grant access to it. In the "Canva accesses Google Drive" example, the Resource Owner is you — the person with the Google account who owns the Drive files.

The Resource Owner is not always a human. In machine-to-machine flows (Client Credentials), the Resource Owner and the Client may be the same entity.

2. Client

The Client is the application requesting access to the resource on behalf of the Resource Owner. In our example, Canva is the Client. The Client must register with the Authorization Server in advance to receive a client_id and client_secret.

There are two types of Clients:

  • Confidential client: A web app running on a server that can keep client_secret secure.
  • Public client: An SPA (React, Vue) or mobile app — cannot store secrets safely because the code runs on the user's device. Must use PKCE.

3. Authorization Server

The Authorization Server is the service that issues tokens after verifying the Resource Owner's identity and obtaining their consent. Google, GitHub, Facebook, and Microsoft each operate their own Authorization Servers.

The Authorization Server exposes two main endpoints:

  • Authorization Endpoint (/auth): receives the authorization request, displays the consent screen, returns an authorization code.
  • Token Endpoint (/token): receives the authorization code, validates the Client, returns an access token and refresh token.

4. Resource Server

The Resource Server is the API that holds the data the Client wants to access. It accepts the access token in the request header, validates it, and returns data if the token is valid and has the required scopes.

In practice, the Authorization Server and Resource Server are often deployed together (e.g., accounts.google.com issues tokens, www.googleapis.com is the Resource Server), but RFC 6749 separates them conceptually.

Authorization Code Flow — Step by Step

Authorization Code Flow is the most widely used grant type, recommended for web apps and mobile apps. It is the most secure flow because the access token never passes through the browser.

1. User clicks "Login with Google" in the App
2. App redirects → https://accounts.google.com/o/oauth2/auth
       ?client_id=APP_CLIENT_ID
       &redirect_uri=https://myapp.com/callback
       &response_type=code
       &scope=openid email profile
       &state=RANDOM_CSRF_TOKEN

3. User logs in and consents → Google redirects back to:
       https://myapp.com/callback?code=AUTH_CODE&state=TOKEN

4. App backend POSTs to Google:
       https://oauth2.googleapis.com/token
       client_id, client_secret, code, redirect_uri, grant_type=authorization_code

5. Google responds with:
       { access_token, expires_in, refresh_token, id_token }

6. App calls the API:
       GET https://www.googleapis.com/oauth2/v3/userinfo
       Authorization: Bearer ACCESS_TOKEN

Breaking down each step:

Steps 1–2: Authorization Request The app constructs a redirect URL with these parameters:

  • client_id: the app identifier registered with Google.
  • redirect_uri: the URL Google will redirect back to after user consent (must match the registered URI exactly).
  • response_type=code: specifies the Authorization Code Flow.
  • scope: the list of permissions being requested.
  • state: a random nonce to prevent CSRF — the app must verify this value when it receives the callback.

Step 3: Authorization Code After the user logs in and consents on Google's consent screen, Google redirects back to redirect_uri with a code — an authorization code that lives for roughly 10 seconds and can only be used once. This is a critical security property: the short-lived, single-use code prevents replay attacks.

Step 4: Token Exchange The app backend (not the browser!) sends a POST request to Google's Token Endpoint, including the client_secret. This exchange happens directly between servers over HTTPS, never through the browser — which is why the access token never appears in browser history or logs.

Step 5: Token Response Google returns:

  • access_token: the token used to call APIs, typically expiring in 1 hour.
  • expires_in: seconds until expiry.
  • refresh_token: a long-lived token used to obtain a new access token without user interaction (only issued when offline_access scope is requested).
  • id_token: a JWT containing user identity claims (only present when using OIDC with the openid scope).

Step 6: API Call The app includes the access token in the Authorization: Bearer <token> header when calling the Resource Server. This is the standard Bearer Token syntax defined in RFC 6750.

Grant Types

OAuth 2.0 defines multiple grant types for different use cases. Each grant type is designed for a specific kind of client and scenario.

As described above, this is the most secure grant type for:

  • Web apps (confidential clients): use client_secret on the server.
  • SPAs and mobile apps (public clients): use PKCE instead of client_secret.

Client Credentials

Used for machine-to-machine (M2M) flows — when no human user is involved. For example, microservice A needs to call microservice B's API.

POST /token
grant_type=client_credentials
client_id=SERVICE_A_ID
client_secret=SERVICE_A_SECRET
scope=read:orders write:inventory

Service A authenticates itself with client_id + client_secret and receives an access token directly, with no consent screen involved.

Device Authorization Grant (Device Code)

Used for devices without a browser or with limited input: Smart TVs, game consoles, IoT devices, CLI tools. The device displays a short URL and a verification code; the user enters the URL on their phone or computer, logs in and consents; the device polls the Authorization Server until it receives a token.

Implicit Flow (Deprecated)

Implicit Flow was once used for SPAs before CORS became widely supported. It returned the access token directly in the URL fragment, skipping the code exchange step — convenient but insecure (token exposed in browser history, referrer headers). Implicit Flow is no longer recommended; OAuth 2.1 (draft) removes it entirely. Use Authorization Code + PKCE for SPAs.

OAuth 2.0 vs OpenID Connect (OIDC)

This is one of the most misunderstood aspects of OAuth 2.0: when you "Sign in with Google", you are not using OAuth 2.0 alone — you are using OpenID Connect (OIDC).

Pure OAuth 2.0 solves the authorization problem: granting an app permission to access resources. It does not provide a standard mechanism for answering "who is this user?" An OAuth 2.0 access token is an opaque string — the Resource Server knows the token is valid, but without additional information, it does not know which user it belongs to.

OpenID Connect (OIDC) is an authentication layer built on top of OAuth 2.0, specified by the OpenID Foundation. OIDC adds:

  1. ID Token: A JWT returned alongside the access token, containing sub (subject/user ID), name, email, picture, and other user identity claims. The Client can read the ID Token without an extra API call.

  2. /userinfo Endpoint: A standardized Resource Server endpoint — call it with an access token to retrieve user information as JSON.

  3. openid Scope: A special scope that activates OIDC. When the authorization request includes scope=openid, the Authorization Server returns an ID Token.

  4. Discovery Document: The /.well-known/openid-configuration endpoint returns JSON describing all endpoints, supported scopes, and signing keys — clients can auto-configure without hardcoding URLs.

In summary:

  • OAuth 2.0: "Canva is allowed to read my Google Drive."
  • OIDC: "I am John Smith, email j@gmail.com — here is Google's cryptographic signature confirming that."

Most major identity providers (Google, GitHub, Microsoft, Apple) implement both OAuth 2.0 and OIDC on the same endpoint. Adding openid to the scope switches you into OIDC mode.

Common Security Mistakes

Implementing OAuth 2.0 incorrectly can introduce serious vulnerabilities. Here are the most common mistakes:

1. Not Validating redirect_uri — Authorization Code Hijacking

This is the most dangerous vulnerability. If the Authorization Server does not strictly validate redirect_uri, an attacker can:

  1. Craft a link with redirect_uri=https://attacker.com/steal.
  2. Send the link to a victim.
  3. The victim logs in; the authorization code is delivered to attacker.com.
  4. The attacker exchanges the code for an access token.

Prevention rules:

  • Exact match against the registered URI — no wildcards, no new subdomains.
  • Never prefix-only checking (https://myapp.com is not safe if an attacker uses https://myapp.com.evil.com).
  • Reject any URI not present in the whitelist.

2. Ignoring the state Parameter — CSRF Attack

The state parameter is a random nonce created by the client, sent in the authorization request, and verified on callback. If the app does not use or verify state:

  1. Attacker crafts their own authorization request and obtains an authorization code.
  2. Attacker embeds the code in a page and tricks the victim into visiting it.
  3. The victim's app automatically exchanges the attacker's code, binding the attacker's account to the victim's session.

Always generate a fresh state for each authorization request, store it in the session, and verify it in the callback before exchanging the code.

3. Access Tokens in URLs — Log Leakage

The deprecated Implicit Flow and some careless implementations place access tokens in URL fragments or query strings. Tokens in URLs can leak through:

  • Browser history.
  • The Referer header when the browser navigates elsewhere.
  • Server access logs.
  • Proxy logs.

Always transmit tokens in the Authorization: Bearer header, never in the URL.

4. Not Using PKCE for SPAs and Mobile Apps

Public clients (SPAs, mobile) cannot protect a client_secret. Without PKCE, an intercepted authorization code can be exchanged for a token by any app that knows the client_id (which is public).

PKCE for SPAs and Mobile Apps

PKCE (Proof Key for Code Exchange, RFC 7636) solves this problem. The idea: instead of a static client_secret, each authorization request generates a random code_verifier and a code_challenge that is a hash of it.

JavaScript
 1// SPA — PKCE flow (Web Crypto API)
 2async function generatePKCE() {
 3  const array = new Uint8Array(32);
 4  crypto.getRandomValues(array);
 5  const verifier = btoa(String.fromCharCode(...array))
 6    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
 7  const encoder = new TextEncoder();
 8  const data = encoder.encode(verifier);
 9  const digest = await crypto.subtle.digest('SHA-256', data);
10  const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
11    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
12  return { verifier, challenge };
13}

The PKCE flow:

  1. The SPA generates a code_verifier (a random string of 43–128 characters).
  2. The SPA hashes code_verifier using SHA-256 → code_challenge.
  3. The SPA sends code_challenge (not the verifier!) with the authorization request.
  4. The Authorization Server stores the code_challenge.
  5. When the SPA exchanges the code for a token, it sends the original code_verifier.
  6. The Authorization Server hashes code_verifier and compares it to the stored code_challenge — they match only if the request came from the same client that initiated the flow.

If an attacker intercepts the authorization code, they do not have code_verifier → they cannot exchange it for a token. PKCE turns each authorization request into a one-time keypair bound to a specific client instance.

As of OAuth 2.1 (draft), PKCE is mandatory for all Authorization Code Flows, including confidential clients.

Summary

OAuth 2.0 is the backbone of the modern API ecosystem. Keep these key points in mind:

  • OAuth 2.0 is authorization, not authentication — use OIDC when you need "Sign in with Google".
  • 4 roles: Resource Owner (user), Client (app), Authorization Server (Google/GitHub), Resource Server (API).
  • Authorization Code Flow is the recommended flow — short-lived code, token exchanged server-to-server.
  • PKCE is mandatory for SPAs and mobile — no client_secret, use a one-time verifier/challenge pair instead.
  • Scopes enforce least privilege — only request what you actually need.
  • Validate redirect_uri strictly, always use state, never put tokens in URLs.

What is JWT? JSON Web Token and API Authentication

What is 2FA? Two-Factor Authentication Explained

What is an API? REST API and How It Works