Roles:
Resource Owner User who grants access
Client App requesting access
Authorization Server Issues tokens
Resource Server API serving protected data
Grant Types:
authorization_code Server-side apps (most secure)
implicit Legacy SPA (deprecated)
client_credentials Service-to-service
password Legacy (deprecated)
refresh_token Obtain new access token
device_code IoT / constrained devices
+----------+ +-----------+ +------------------+
| Client |---->| Resource |---->| Authorization |
| (App) | | Owner | | Server |
| |<----| (User) |<----| (auth provider) |
+----------+ +-----------+ +------------------+
| |
|---------- Token Request ----------->|
|<--------- Access Token -------------|
| |
|---------- API Request ------------->| Resource Server
|<---------- Protected Data ---------| (separate or same)
# Step 1: Redirect user to authorize
GET /authorize?
response_type=code
&client_id=myapp
&redirect_uri=https://app.example.com/callback
&scope=read write
&state=random_csrf_token HTTP/1.1
Host: auth.example.com
# Step 2: User approves, callback with code
HTTP/1.1 302 Found
Location: https://app.example.com/callback?
code=SplxlOBeZQQYbYS6WxSbIA
&state=random_csrf_token
# Step 3: Exchange code for token
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://app.example.com/callback
# Step 4: Token response
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read write"
}
PKCE (Proof Key for Code Exchange) - required for public clients (SPAs, mobile)
code_verifier = random_string_43_to_128_chars
code_challenge = base64url(sha256(code_verifier))
# e.g., "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
# Authorization request with PKCE
GET /authorize?
response_type=code
&client_id=myapp
&redirect_uri=https://app.example.com/callback
&scope=read
&state=abc123
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
# Token exchange with code_verifier
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&client_id=myapp
&redirect_uri=https://app.example.com/callback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
# Service-to-service (no user involved)
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=client_credentials
&scope=read write
# Response (no refresh_token for client credentials)
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write"
}
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=password
&username=user@example.com
&password=secret123
&scope=read
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read"
}
DEPRECATED: Use only for highly trusted first-party clients.
Migrate to authorization_code + PKCE instead.
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=refresh_token
&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "new_eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "new_tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read write"
}
Best practices:
- Rotation: issue new refresh_token on each use
- Detection: revoke if old refresh_token is reused (compromise)
- Expiry: set absolute lifetime + idle timeout
- Revocation: provide /revoke endpoint
POST /introspect HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
token=eyJhbGciOiJSUzI1NiJ9...
&token_type_hint=access_token
HTTP/1.1 200 OK
Content-Type: application/json
{
"active": true,
"scope": "read write",
"client_id": "myapp",
"sub": "user123",
"exp": 1735689600,
"iat": 1735686000,
"token_type": "Bearer"
}
# Revocation
POST /revoke HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
token=eyJhbGciOiJSUzI1NiJ9...
&token_type_hint=access_token
OpenID Connect = OAuth 2.0 + Identity Layer
ID Token: JWT containing user identity claims
Verified by checking: signature, issuer, audience, expiry, nonce
GET /authorize?
response_type=code
&client_id=myapp
&redirect_uri=https://app.example.com/callback
&scope=openid profile email
&state=abc123
&nonce=random_nonce HTTP/1.1
ID Token (JWT decoded payload):
{
"iss": "https://auth.example.com",
"sub": "user123",
"aud": "myapp",
"exp": 1735689600,
"iat": 1735686000,
"nonce": "random_nonce",
"name": "Alice",
"email": "alice@example.com",
"email_verified": true,
"picture": "https://auth.example.com/avatar.png"
}
GET /.well-known/openid-configuration HTTP/1.1
Host: auth.example.com
HTTP/1.1 200 OK
Content-Type: application/json
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"userinfo_endpoint": "https://auth.example.com/userinfo",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"revocation_endpoint": "https://auth.example.com/revoke",
"introspection_endpoint": "https://auth.example.com/introspect",
"end_session_endpoint": "https://auth.example.com/logout",
"scopes_supported": ["openid", "profile", "email"],
"response_types_supported": ["code", "id_token"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "private_key_jwt"]
}
GET /.well-known/jwks.json HTTP/1.1
Host: auth.example.com
OAuth 2.0 Scopes:
openid Required for OIDC
profile name, family_name, given_name, picture
email email, email_verified
address address
phone phone_number, phone_number_verified
offline_access Request refresh_token
Custom scopes:
read:posts Read user posts
write:posts Create/edit posts
admin Administrative access
GET /userinfo HTTP/1.1
Host: auth.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
HTTP/1.1 200 OK
Content-Type: application/json
{
"sub": "user123",
"name": "Alice",
"given_name": "Alice",
"family_name": "Smith",
"email": "alice@example.com",
"email_verified": true,
"picture": "https://auth.example.com/avatar.png",
"updated_at": 1735686000
}
JWT = Header.Payload.Signature
Header (base64url):
{ "alg": "RS256", "typ": "JWT", "kid": "key-2025-01" }
Payload (base64url):
{ "sub": "user123", "iss": "https://auth.example.com", "aud": "myapp",
"exp": 1735689600, "iat": 1735686000, "scope": "read write" }
Signature:
RS256(base64url(header) + "." + base64url(payload), private_key)
# Decode JWT (no verification)
echo "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMTIzIn0.sig" | cut -d. -f2 | base64 -d 2>/dev/null
# Using jq
echo "eyJ..." | awk -F. '{printf "%s", $2}' | basenc --base64url -d | jq .
Registered Claims:
iss Issuer aud Audience
sub Subject exp Expiration
iat Issued at nbf Not before
jti JWT ID scope Permissions
nonce ID token only azp Authorized party
Google:
Auth: https://accounts.google.com/o/oauth2/v2/auth
Token: https://oauth2.googleapis.com/token
UserInfo: https://www.googleapis.com/oauth2/v3/userinfo
Scope: openid profile email
Discovery: https://accounts.google.com/.well-known/openid-configuration
GET https://accounts.google.com/o/oauth2/v2/auth?
response_type=code
&client_id=xxx.apps.googleusercontent.com
&redirect_uri=https://app.example.com/callback
&scope=openid profile email
&state=abc123
&nonce=random_nonce
GitHub:
Auth: https://github.com/login/oauth/authorize
Token: https://github.com/login/oauth/access_token
UserInfo: https://api.github.com/user
Scope: user:email read:org repo
GET https://github.com/login/oauth/authorize?
client_id=Ov23liabc123
&redirect_uri=https://app.example.com/callback
&scope=user:email
&state=abc123
Authorization:
- Always use state parameter (CSRF protection)
- Use PKCE for all public clients
- Validate redirect_uri exactly (no wildcards)
- Use authorization_code flow (avoid implicit grant)
Tokens:
- Short access_token lifetime (5-15 minutes)
- Rotate refresh tokens on use
- Validate JWT: signature, iss, aud, exp
- Store tokens securely (not localStorage in browser)
Transport:
- Always use HTTPS
- Use TLS 1.2+
- Pin certificates for high-security apps
- HSTS headers on auth endpoints
Client:
- Never expose client_secret in public clients
- Use private_key_jwt for confidential clients
- Register all redirect_uris explicitly
- Use minimal scopes (principle of least privilege)
Browser (SPA):
✓ In-memory (JavaScript variable)
✓ Service worker cache
✓ httpOnly cookie (if backend-for-frontend)
✗ localStorage (XSS accessible)
✗ sessionStorage (XSS accessible)
Mobile:
✓ Keychain (iOS) / Keystore (Android)
✓ Secure enclave / hardware-backed
✗ AsyncStorage / SharedPreferences
Server:
✓ Encrypted database with access controls
✓ Hash refresh_tokens before storage
✓ Environment variables for client secrets
✗ Plain text in database
✗ Committed to version control
Token Lifecycle:
Access Token: 5-15 minutes, used in API calls
Refresh Token: Days-weeks, stored securely, rotated
ID Token: Validated once, extract claims, discard
Authorization Code: Single use, expires in ~10 minutes
角色:
Resource Owner(资源所有者) 授权访问的用户
Client(客户端) 请求访问的应用
Authorization Server(授权服务器) 签发令牌
Resource Server(资源服务器) 提供受保护数据的 API
授权类型:
authorization_code 服务端应用(最安全)
implicit 旧版 SPA(已废弃)
client_credentials 服务间通信
password 旧版(已废弃)
refresh_token 获取新的访问令牌
device_code IoT / 受限设备
+----------+ +-----------+ +------------------+
| 客户端 |---->| 资源 |---->| 授权 |
| (应用) | | 所有者 | | 服务器 |
| |<----| (用户) |<----| (认证提供者) |
+----------+ +-----------+ +------------------+
| |
|---------- 令牌请求 ---------------->|
|<--------- 访问令牌 ----------------|
| |
|---------- API 请求 ---------------->| 资源服务器
|<---------- 受保护数据 -------------| (独立或相同)
# 步骤 1:重定向用户到授权页
GET /authorize?
response_type=code
&client_id=myapp
&redirect_uri=https://app.example.com/callback
&scope=read write
&state=random_csrf_token HTTP/1.1
Host: auth.example.com
# 步骤 2:用户同意,回调携带授权码
HTTP/1.1 302 Found
Location: https://app.example.com/callback?
code=SplxlOBeZQQYbYS6WxSbIA
&state=random_csrf_token
# 步骤 3:用授权码换取令牌
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://app.example.com/callback
# 步骤 4:令牌响应
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read write"
}
PKCE(代码交换证明密钥)— 公共客户端(SPA、移动端)必须使用
code_verifier = 随机字符串_43到128个字符
code_challenge = base64url(sha256(code_verifier))
# 例如:"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
# 带 PKCE 的授权请求
GET /authorize?
response_type=code
&client_id=myapp
&redirect_uri=https://app.example.com/callback
&scope=read
&state=abc123
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
# 携带 code_verifier 的令牌交换
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&client_id=myapp
&redirect_uri=https://app.example.com/callback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
# 服务间通信(不涉及用户)
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=client_credentials
&scope=read write
# 响应(客户端凭证无 refresh_token)
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write"
}
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=password
&username=user@example.com
&password=secret123
&scope=read
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read"
}
已废弃:仅用于高度信任的第一方客户端。
建议迁移到 authorization_code + PKCE。
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=refresh_token
&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "new_eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "new_tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read write"
}
最佳实践:
- 轮换:每次使用时签发新的 refresh_token
- 检测:旧 refresh_token 被重用时撤销(泄露检测)
- 过期:设置绝对有效期 + 空闲超时
- 撤销:提供 /revoke 端点
POST /introspect HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
token=eyJhbGciOiJSUzI1NiJ9...
&token_type_hint=access_token
HTTP/1.1 200 OK
Content-Type: application/json
{
"active": true,
"scope": "read write",
"client_id": "myapp",
"sub": "user123",
"exp": 1735689600,
"iat": 1735686000,
"token_type": "Bearer"
}
# 令牌撤销
POST /revoke HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
token=eyJhbGciOiJSUzI1NiJ9...
&token_type_hint=access_token
OpenID Connect = OAuth 2.0 + 身份层
ID Token:包含用户身份声明的 JWT
验证项:签名、签发者、受众、过期时间、nonce
GET /authorize?
response_type=code
&client_id=myapp
&redirect_uri=https://app.example.com/callback
&scope=openid profile email
&state=abc123
&nonce=random_nonce HTTP/1.1
ID Token(JWT 解码后载荷):
{
"iss": "https://auth.example.com",
"sub": "user123",
"aud": "myapp",
"exp": 1735689600,
"iat": 1735686000,
"nonce": "random_nonce",
"name": "Alice",
"email": "alice@example.com",
"email_verified": true,
"picture": "https://auth.example.com/avatar.png"
}
GET /.well-known/openid-configuration HTTP/1.1
Host: auth.example.com
HTTP/1.1 200 OK
Content-Type: application/json
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"userinfo_endpoint": "https://auth.example.com/userinfo",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"revocation_endpoint": "https://auth.example.com/revoke",
"introspection_endpoint": "https://auth.example.com/introspect",
"end_session_endpoint": "https://auth.example.com/logout",
"scopes_supported": ["openid", "profile", "email"],
"response_types_supported": ["code", "id_token"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "private_key_jwt"]
}
GET /.well-known/jwks.json HTTP/1.1
Host: auth.example.com
OAuth 2.0 范围:
openid OIDC 必需
profile name, family_name, given_name, picture
email email, email_verified
address address
phone phone_number, phone_number_verified
offline_access 请求 refresh_token
自定义范围:
read:posts 读取用户文章
write:posts 创建/编辑文章
admin 管理权限
GET /userinfo HTTP/1.1
Host: auth.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
HTTP/1.1 200 OK
Content-Type: application/json
{
"sub": "user123",
"name": "Alice",
"given_name": "Alice",
"family_name": "Smith",
"email": "alice@example.com",
"email_verified": true,
"picture": "https://auth.example.com/avatar.png",
"updated_at": 1735686000
}
JWT = Header.Payload.Signature
Header(base64url):
{ "alg": "RS256", "typ": "JWT", "kid": "key-2025-01" }
Payload(base64url):
{ "sub": "user123", "iss": "https://auth.example.com", "aud": "myapp",
"exp": 1735689600, "iat": 1735686000, "scope": "read write" }
Signature:
RS256(base64url(header) + "." + base64url(payload), private_key)
# 解码 JWT(不验证)
echo "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMTIzIn0.sig" | cut -d. -f2 | base64 -d 2>/dev/null
# 使用 jq
echo "eyJ..." | awk -F. '{printf "%s", $2}' | basenc --base64url -d | jq .
标准声明:
iss 签发者 aud 受众
sub 主体 exp 过期时间
iat 签发时间 nbf 生效时间
jti JWT ID scope 权限
nonce 仅 ID Token azp 授权方
Google:
Auth: https://accounts.google.com/o/oauth2/v2/auth
Token: https://oauth2.googleapis.com/token
UserInfo: https://www.googleapis.com/oauth2/v3/userinfo
Scope: openid profile email
Discovery: https://accounts.google.com/.well-known/openid-configuration
GET https://accounts.google.com/o/oauth2/v2/auth?
response_type=code
&client_id=xxx.apps.googleusercontent.com
&redirect_uri=https://app.example.com/callback
&scope=openid profile email
&state=abc123
&nonce=random_nonce
GitHub:
Auth: https://github.com/login/oauth/authorize
Token: https://github.com/login/oauth/access_token
UserInfo: https://api.github.com/user
Scope: user:email read:org repo
GET https://github.com/login/oauth/authorize?
client_id=Ov23liabc123
&redirect_uri=https://app.example.com/callback
&scope=user:email
&state=abc123
授权:
- 始终使用 state 参数(CSRF 防护)
- 所有公共客户端使用 PKCE
- 精确验证 redirect_uri(禁止通配符)
- 使用 authorization_code 流程(避免 implicit)
令牌:
- 短有效期访问令牌(5-15 分钟)
- 每次使用轮换 refresh_token
- 验证 JWT:签名、iss、aud、exp
- 安全存储令牌(浏览器中不用 localStorage)
传输:
- 始终使用 HTTPS
- 使用 TLS 1.2+
- 高安全应用使用证书固定
- 认证端点启用 HSTS
客户端:
- 公共客户端不得暴露 client_secret
- 机密客户端使用 private_key_jwt
- 显式注册所有 redirect_uri
- 使用最小范围(最小权限原则)
浏览器(SPA):
✓ 内存中(JavaScript 变量)
✓ Service Worker 缓存
✓ httpOnly Cookie(如有 BFF 后端)
✗ localStorage(XSS 可访问)
✗ sessionStorage(XSS 可访问)
移动端:
✓ Keychain (iOS) / Keystore (Android)
✓ 安全 enclave / 硬件安全模块
✗ AsyncStorage / SharedPreferences
服务端:
✓ 加密数据库 + 访问控制
✓ 存储 refresh_token 前先哈希
✓ 环境变量存储 client_secret
✗ 数据库明文存储
✗ 提交到版本控制
令牌生命周期:
Access Token: 5-15 分钟,用于 API 调用
Refresh Token: 数天至数周,安全存储,轮换使用
ID Token: 验证一次,提取声明后丢弃
Authorization Code:单次使用,约 10 分钟过期