# 从 OAuth2.0 到 OIDC

02-18 / 2025

## **OAuth2.0：第三方授权的基石**

OAuth2.0 作为现代互联网的授权标准协议，其核心价值在于允许第三方应用在用户授权后访问特定资源，而无需共享用户密码。以 GitHub 的 OAuth 流程为例，典型授权过程分为三步：

1. 用户通过客户端跳转至授权端点（authorization\_endpoint）
2. 授权服务器通过回调地址返回授权码（code）
3. 客户端使用授权码换取访问令牌（access\_token）

这是 github 的 oauth2.0 接入文档，在文档中有详细描述整个认证流程。

[https://docs.github.com/zh/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps](https://docs.github.com/zh/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps)

如图，三步就可以完成整个流程。

![](https://fsb.huglight.cn/user/1/mbTyXhkkCwM__image.png?imageMogr2/thumbnail/3072x3072>/format/webp/quality/80)

### **OAuth2.0 的配置困境**

从 github 的认证流程文档我们就能看到，作为接入方的我们需要为 github 定义三个常量：

- authorization\_endpoint：请求认证的地址，当授权成功，会携带 code 跳转到传递给它的 redirect\_uri
- token\_endpoint：通过 code 换取 token 的 API 地址
- userinfo\_endpoint：获取用户信息的 API 地址

这就导致了接入不同的 OAuth 提供方时，需为每个提供方维护不同端点。当接入多个平台时会产生大量重复工作。

### OAuth2.0 缺少"认证"逻辑

OAuth 协议中没有定义如何认证用户，而依赖认证系统比如 Google 自己通过账号密码进行用户认证，也就是常说的"OAuth2.0 仅负责授权，不验证用户身份"。

这看似和我们第三方没有什么关系，但其实会间接导致另一个问题：

OAuth 协议最后只会给第三方访问令牌（Token）来授权第三方使用用户的部分数据，而没有告诉第三方这个用户是谁，第三方无法区分 A 用户和 B 用户，进而第三方也无法为这位用户分配属于他自己系统的唯一身份。

这里有一个常见误解，许多应用使用 OAuth 2.0 实现"社交登录"（如"用 Google 账号登录"），那这些应用是如何知道用户是谁的呢？

答案是：虽然 OAuth 没有规定如何识别用户是谁，但有了 Token 之后，第三方其实可以调用各个 Provider 的接口获取用户信息，如用户的 id / openid，这样第三方就知道用户是谁。

所以我们平时使用的 "OAuth 第三方登录"，实则已经不是只有 OAuth 了，只是部分依赖了 OAuth 协议。

而上面提到的解决方案（通过接口再次调用接口获取用户信息）有什么问题呢？

由于获取用户信息的接口没有被纳入规范，所以各个 Provider 的接口千奇百怪，并不好适配，而大多数情况下，用户信息是被需要的，这就导致又需要为不同的 Provider 写不同的代码。

题外话：如果你和我一样对"授权"、"认证"有点模糊，那么可以参考下下面 Deepseek 的答案：

> 如何理解"授权"，"认证"。
>
> 这两个概念确实打脑壳，让 deepseek r1 帮我理一下：
>
> • **授权（Authorization）**：OAuth 2.0 的核心功能是允许第三方应用（Client）在用户同意后， **安全地获取访问资源的权限**（如访问用户的云存储文件或社交媒体数据）。
>
> • **认证（Authentication）**：验证用户身份（例如确认用户是"谁"）并非 OAuth 2.0 的职责，而是由 \*\*授权服务器（Authorization Server）\*\*通过其他机制（如用户名/密码、多因素认证等）完成的。
>
> **OAuth 2.0 的流程中隐含认证，但协议本身不定义认证**
>
> 在 OAuth 流程中，用户通常需要登录到授权服务器（如 Google、Facebook）以同意授权请求。虽然这看似是"认证"，但：
>
> - **认证由授权服务器独立完成**：OAuth 2.0 协议不规定如何认证用户，而是依赖授权服务器自行实现认证逻辑（例如通过传统登录表单或生物识别）。
> - **OAuth 仅关注授权结果**：协议只关心用户是否同意授权，并返回访问令牌（Access Token），而不关心用户是如何被认证的。
>
> **实际场景中的误解**
>
> • **"使用 OAuth 登录"的真相**：许多应用使用 OAuth 2.0 实现"社交登录"（如"用 Google 账号登录"），这实际上是通过 OAuth 的授权流程间接获取用户身份信息。但这种身份信息是由授权服务器提供的（例如通过 OpenID Connect 扩展），而非 OAuth 2.0 原生支持的功能。

## **OIDC 的改进**

### DiscoveryUrl：实现配置信息的动态获取

DiscoveryUrl 是 OIDC 协议中的一个标准化机制，用于动态获取 OIDC 提供者的配置信息。它简化了客户端的配置，使客户端能够自动适应不同的 OIDC 提供者。通过 DiscoveryUrl，客户端可以获取授权端点、令牌端点、用户信息端点等关键信息，从而实现身份验证和授权流程。

一般的 DiscoveryUrl 通常是 OIDC 提供者的基础 URL 加上标准路径 /.well-known/openid-configuration。例如： [https://accounts.google.com/.well-known/openid-configuration](https://accounts.google.com/.well-known/openid-configuration)

访问这个地址会返回如下内容：

```rounded bg-ink/[0.06] px-1.5 py-0.5 font-monocn text-[0.88em] text-ink
{
  "issuer": "https://accounts.google.com",
  "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
  "token_endpoint": "https://oauth2.googleapis.com/token",
  "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
  "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
  "response_types_supported": ["code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported": ["openid", "email", "profile"]
}

```

可以看到这里面包括了 OAuth 需要用到的所有端点（Endpoint），所以有了 DiscoveryUrl，我们就能实现配置信息的动态获取。

在各个语言应该都有 OIDC 库用于请求 DiscoveryUrl 获取 OAuth 流程中需要的配置。

例如 golang 中的这个库： [https://github.com/coreos/go-oidc](https://github.com/coreos/go-oidc)

```rounded bg-ink/[0.06] px-1.5 py-0.5 font-monocn text-[0.88em] text-ink
provider, err := oidc.NewProvider(ctx, "https://accounts.google.com")
if err != nil {
    // handle error
}

// Configure an OpenID Connect aware OAuth2 client.
oauth2Config := oauth2.Config{
    ClientID:     clientID,
    ClientSecret: clientSecret,
    RedirectURL:  redirectURL,

    // Discovery returns the OAuth2 endpoints.
    Endpoint: provider.Endpoint(),

    // "openid" is a required scope for OpenID Connect flows.
    Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}

```

### **ID Token：身份认证的标准化方案**

而大多数情况下，用户信息是被需要的，所以规范就诞生了： **ID Token**.

OIDC 协议规定通过 code 换取 token 的时候，除了返回 access\_token，还会返回 id\_token。

ID Token 是一个 JWT（JSON Web Token），包含用户的身份信息（如用户 ID、姓名、电子邮件等）。包含标准声明（如 iss、sub、aud、exp 等）和自定义声明。

例如：

```rounded bg-ink/[0.06] px-1.5 py-0.5 font-monocn text-[0.88em] text-ink
{
  "iss": "https://accounts.google.com",
  "sub": "1234567890",
  "aud": "client-id",
  "exp": 1672531199,
  "name": "John Doe",
  "email": "john.doe@example.com"
}

```

其中 **sub** 就是用户的唯一标识符。

！需要注意的是，JWT 需要另外的用 Provider 提供的公钥来验证其有效性，这点我们后面在"安全"部分详细介绍下。

### **UserInfo Endpoint**

另外 OIDC 同时也定义了 " **UserInfo Endpoint**"

IDC 中的 UserInfo Endpoint 规定了返回的用户信息格式。根据 OpenID Connect Core 1.0 规范，UserInfo Endpoint 返回的响应必须是一个 JSON 对象，包含用户的相关声明（claims）。

必须包含的字段：

- **sub** (Subject Identifier): 用户的唯一标识符，必须与 ID Token 中的 sub 声明一致。

可选字段：

- **name**: 用户的全名。
- **given\_name**: 用户的名字。
- **family\_name**: 用户的姓氏。
- **middle\_name**: 用户的中间名。
- **nickname**: 用户的昵称。
- **preferred\_username**: 用户的首选用户名。
- **profile**: 用户个人资料的 URL。
- **picture**: 用户头像的 URL。

现在我们要获取用户信息就有了比较统一的规范，有两个方法：

- 直接解析 ID Token，里面会存放简单的如用户唯一标识（sub）的信息。
- 如果我们需要更多信息，比如头像，我们可以请求 **UserInfo Endpoint。**

## **JWT 的安全哲学**

为什么 **ID Token** 是 JWT 格式，而不是明文？

我们知道在通过 code 交换 token 的过程中，都是直接由服务端直接发起，并且流程全程使用HTTPS加密，ID Token 应该不会被泄露或者伪造才对。

我们来演练下如果没有 JWT 的防护可能发生的问题：

现在"支付宝"是 OAuth 使用方，而"百度"是提供方，另有个有钱的"百度用户"，百度账户 baidu\_id = 'richman'。

有一个有能力的攻击者劫持了 baidu.com 域名，现在支付宝在进行 百度 OAuth 授权过程中的信息就会被攻击者劫持，于是他使用自己的账号进行登录，并替换了 ID Token，把 ID Token 中的 sub 由 'poolman' 替换成了 'richman'，而 支付宝 在收到 ID Token 之后并没有校验 ID Token 是否被篡改，于是攻击者登录上 支付宝 的账号变成了 richman，现在攻击者就可以随意使用 richman 的账号做非法的事情了。

JWT 如何避免这次攻击？只需要 支付宝 在使用 ID Token 的时候使用 百度 提供的公钥来校验下 ID Token 是否被篡改即可。

```rounded bg-ink/[0.06] px-1.5 py-0.5 font-monocn text-[0.88em] text-ink
// JWT 验证示例（使用提供方公钥）
keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs")
verifier := provider.Verifier(&oidc.Config{ClientID: clientID})

idToken, _ := verifier.Verify(ctx, rawIDToken)
claims := struct {
    Email string `json:"email"`
    Sub   string `json:"sub"`
}{}
idToken.Claims(&claims)

```

这里就引生出安全设计原则： **纵深防御（Defense in Depth）**

- **多层保护**：
  - HTTPS（传输安全） + JWT 签名（数据安全） + 短有效期（减少泄露影响）。
- **不信任任何单一机制**：
  - 即使 HTTPS 被绕过（如服务器配置错误、证书伪造），JWT 签名仍能提供保护。

## Golang OIDC 库的选择

截止 2025-02-18，Github 上 star 最多的两个库是： [coreos/go-oidc](https://github.com/coreos/go-oidc)（2k star，前文提到了）和 [zitadel/oidc](https://github.com/zitadel/oidc)（1.5k star）。

这两个库我都试用了，可以提供一些建议：

[zitadel/oidc](https://github.com/zitadel/oidc) 和 [coreos/go-oidc](https://github.com/coreos/go-oidc) 最大的功能区别是：前者支持 OP（OpenID Provider，OIDC 提供者）功能，而后者只支持 RP（Relying Party，依赖方）。

[zitadel/oidc](https://github.com/zitadel/oidc) 看似很强，但实际使用过程中，如果你不需要用到 OP 功能，那么还是不要碰这个库，因为这个库代码很复杂并且没有说明文档，大而全但定制复杂，有点过度设计嫌疑，不要以为 OIDC 是标准就无需定制了，并不是每个 Provider 都严格按照标准执行，很可能会需要针对不同 Provider 打补丁。

而后者 [coreos/go-oidc](https://github.com/coreos/go-oidc) 没有过度设计，就算这个库无法提供对应功能或者需要定制，我们可以直接回退到使用 oauth2 库并部分使用这个库的工具方法。

> 全站页面清单：[/llms.txt](/llms.txt)
