Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 358 additions & 0 deletions docs/oidc-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
# zitadel/oidc 完全指南:OpenID Connect 实现
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR 描述中列出了“Enterprise SSO、Multi-tenant、RBAC、Performance optimization、Security best practices、Troubleshooting”等多个主题,但本文正文中未包含相应章节/内容(例如全文无 SSO/RBAC/多租户/性能 等关键词)。建议要么补齐这些部分,要么更新 PR 描述与标题/目录,使内容与承诺范围一致。

Copilot uses AI. Check for mistakes.

> **作者注**:zitadel/oidc 是通过 OpenID 基金会认证的 OIDC 库。我研究了源码和示例,整理了这篇指南,包含了很多实际部署中遇到的坑。
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

作者注里写“zitadel/oidc 是通过 OpenID 基金会认证的 OIDC 库”表述不准确:当前仓库说明的是 RP(relying party)通过 basic/config profile 认证,而不是整个库/OP 都认证。建议改为与 README 中的认证范围一致,避免误导。

Suggested change
> **作者注**:zitadel/oidc 是通过 OpenID 基金会认证的 OIDC 库。我研究了源码和示例,整理了这篇指南,包含了很多实际部署中遇到的坑。
> **作者注**:zitadel/oidc 仓库中的 RP(relying party)实现已通过 OpenID 基金会的 basic/config profile 认证。我研究了源码和示例,整理了这篇指南,包含了很多实际部署中遇到的坑。

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

仓库现有顶层文档(README/CONTRIBUTING/SECURITY/UPGRADING)均为英文;当前新增指南为中文,可能降低多数用户可用性与维护性。建议至少提供英文版本(或双语结构/链接),并在 README 中加入指向该指南的入口,避免“孤立文档”。

Copilot uses AI. Check for mistakes.

---

## 📦 一、安装

```bash
go get github.com/zitadel/oidc/v3
```

**要求**: Go 1.25+
**源码参考**:[README.md](https://github.com/zitadel/oidc#openid-connect-sdk-client-and-server-for-go)

---

## 🚀 二、快速入门

### 2.1 启动示例服务器

```bash
# 启动 OIDC 服务器
go run github.com/zitadel/oidc/v3/example/server

# 启动客户端
CLIENT_ID=web CLIENT_SECRET=secret \
ISSUER=http://localhost:9998/ \
SCOPES="openid profile" \
PORT=9999 \
go run github.com/zitadel/oidc/v3/example/client/app
```

### 2.2 登录流程

1. 访问 `http://localhost:9999/login`
2. 重定向到 OIDC 服务器
3. 使用 `test-user@localhost` / `verysecure` 登录
4. 显示用户信息

### 2.3 完整示例:Web 应用集成

```go
package main

import (
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/http"
Comment on lines +47 to +48
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

该示例的 import 会导致代码无法编译:这里将 github.com/zitadel/oidc/v3/pkg/http 导入为 http,但后续使用的 http.HandleFunc/http.Error/http.ListenAndServe 实际属于标准库 net/http;同时示例里还使用了 fmtoidc 等但未导入。建议改为导入 net/http 并为 zitadel 的 pkg/http 使用别名(或移除不需要的导入),补齐缺失的依赖。

Suggested change
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/http"
"fmt"
"net/http"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"

Copilot uses AI. Check for mistakes.
"golang.org/x/oauth2"
)

func main() {
// 创建 Relying Party
key := []byte("test")
rpConfig := &oauth2.Config{
Comment on lines +54 to +55
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

示例中使用 key := []byte("test") 作为 cookie 加密/签名 key 容易被复制到生产代码且不安全。建议在示例里明确要求使用安全随机的 32 字节密钥(或像 example server 一样用 sha256.Sum256 只是示例),并说明密钥管理/轮换。

Copilot uses AI. Check for mistakes.
ClientID: "web",
ClientSecret: "secret",
RedirectURL: "http://localhost:9999/callback",
Scopes: []string{"openid", "profile", "email"},
}

relyingParty, err := rp.NewRelyingPartyOIDC(
"http://localhost:9998",
rpConfig.ClientID,
rpConfig.ClientSecret,
rpConfig.RedirectURL,
rpConfig.Scopes,
rp.WithCookieHandler(key),
)
Comment on lines +62 to +69
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rp.NewRelyingPartyOIDC 的函数签名需要第一个参数 context.Context(见 pkg/client/rp/relying_party.go),当前示例缺少 ctx,导致无法编译。建议按仓库示例/测试的调用方式传入 context.Background() 或请求上下文。

Copilot uses AI. Check for mistakes.

// 登录处理
http.HandleFunc("/login", rp.AuthURLHandler(relyingParty))

// 回调处理
http.HandleFunc("/callback", rp.CodeExchangeHandler(func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) {
// 获取用户信息
userInfo, err := rp.Userinfo(r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDToken, relyingParty)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
fmt.Fprintf(w, "欢迎,%s!", userInfo.Email)
}, relyingParty))
Comment on lines +75 to +83
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里 rp.CodeExchangeHandler 的用法与当前库 API 不匹配:它需要传入 CodeExchangeCallback(回调签名包含 tokens 的泛型类型以及 rp 参数),而不是直接传一个不带 rp 参数的匿名函数;同时示例中 rp.Userinfo 的参数也不正确(需要 subject string,而非 tokens.IDToken)。建议参考 rp.UserinfoCallback / CodeExchangeHandler 的定义与 userinfo_example_test.go 的用法来更新示例。

Copilot uses AI. Check for mistakes.

http.ListenAndServe(":9999", nil)
}
```

---

## 🔧 三、核心组件

### 3.1 Relying Party (RP) - 客户端

```go
import "github.com/zitadel/oidc/v3/pkg/client/rp"

// 创建 RP
relyingParty, err := rp.NewRelyingPartyOIDC(
issuer,
clientID,
clientSecret,
redirectURI,
scopes,
)

// 处理回调
http.HandleFunc("/callback", rp.CodeExchangeHandler(relyingParty))
```
Comment on lines +99 to +109
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

该“创建 RP / 处理回调”片段同样与现有 API 不一致:NewRelyingPartyOIDC 需要 context 参数;CodeExchangeHandler 也需要传入回调函数而不是仅传 rp。按当前代码该片段无法编译,建议用仓库 example/client/app 或 userinfo_example_test.go 作为可运行的最小示例。

Copilot uses AI. Check for mistakes.

### 3.2 OpenID Provider (OP) - 服务器

```go
import "github.com/zitadel/oidc/v3/pkg/op"

// 创建 OP
provider, err := op.NewOpenIDProvider(
issuer,
config,
storage,
)

// 注册路由
http.Handle("/.well-known/openid-configuration", provider.Discovery())
http.Handle("/oauth/token", provider.Token())
http.Handle("/oauth/auth", provider.Auth())
Comment on lines +124 to +126
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OP 片段中调用了 provider.Discovery()/Token()/Auth() 等方法,但当前 op.NewOpenIDProvider 返回的 provider 是一个 http.Handler(示例中使用 router.Mount("/", provider)),并不存在这些端点方法。建议改为展示与 example/server/exampleop/SetupServer 一致的挂载方式,或明确使用 op.DefaultEndpoints/自定义端点的官方方式。

Suggested change
http.Handle("/.well-known/openid-configuration", provider.Discovery())
http.Handle("/oauth/token", provider.Token())
http.Handle("/oauth/auth", provider.Auth())
// provider 本身实现了 http.Handler,会暴露标准 OIDC 端点
// (如 discovery、authorize、token 等)
http.Handle("/", provider)

Copilot uses AI. Check for mistakes.
```

### 3.3 Resource Server (RS) - API

```go
import "github.com/zitadel/oidc/v3/pkg/client/rs"

// 验证 Token
resourceServer, err := rs.NewResourceServer(
issuer,
withIntrospection,
)

// 在 HTTP 中间件中使用
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, err := rs.IntrospectToken(r.Context(), resourceServer, r)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Token 有效,继续处理
next.ServeHTTP(w, r.WithContext(token))
Comment on lines +132 to +149
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RS 片段使用了不存在的 API(rs.NewResourceServerrs.IntrospectToken),并且将“token”直接塞进 r.WithContext(token) 也不成立(context 需要 context.Context)。当前库提供的是 NewResourceServerClientCredentials/NewResourceServerJWTProfile 以及 rs.Introspect 等函数。建议用这些真实 API 更新示例,并明确从请求中解析 Bearer token 再做 introspection。

Suggested change
import "github.com/zitadel/oidc/v3/pkg/client/rs"
// 验证 Token
resourceServer, err := rs.NewResourceServer(
issuer,
withIntrospection,
)
// 在 HTTP 中间件中使用
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, err := rs.IntrospectToken(r.Context(), resourceServer, r)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Token 有效,继续处理
next.ServeHTTP(w, r.WithContext(token))
import (
"context"
"net/http"
"strings"
"github.com/zitadel/oidc/v3/pkg/client/rs"
)
type tokenContextKey struct{}
// 验证 Token
resourceServer, err := rs.NewResourceServerClientCredentials(
issuer,
clientID,
clientSecret,
)
if err != nil {
return err
}
// 在 HTTP 中间件中使用
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
const bearerPrefix = "Bearer "
authorization := r.Header.Get("Authorization")
if !strings.HasPrefix(authorization, bearerPrefix) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
accessToken := strings.TrimSpace(strings.TrimPrefix(authorization, bearerPrefix))
token, err := rs.Introspect(r.Context(), resourceServer, accessToken)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Token 有效,将 introspection 结果放入新的 context 后继续处理
ctx := context.WithValue(r.Context(), tokenContextKey{}, token)
next.ServeHTTP(w, r.WithContext(ctx))

Copilot uses AI. Check for mistakes.
})
}
```

### 3.4 JWT Profile

```go
import "github.com/zitadel/oidc/v3/pkg/client"

// 使用 JWT Profile 获取 Token
token, err := client.JWTProfileToken(
ctx,
issuer,
jwtProfileGrant,
)
```

### 3.5 Device Authorization

```go
import "github.com/zitadel/oidc/v3/pkg/client/rp"

// 启动 Device Authorization 流程
deviceCode, err := rp.DeviceAuthorization(ctx, relyingParty)

// 显示给用户
code := deviceCode.UserCode
uri := deviceCode.VerificationURI
fmt.Printf("访问 %s 并输入代码:%s\n", uri, code)

// 轮询等待用户授权
tokens, err := rp.PollDeviceAccessToken(ctx, deviceCode, relyingParty)
if err != nil {
// 处理错误
}
// tokens 包含访问令牌
```
Comment on lines +172 to +186
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Device Authorization 示例与当前 API 不匹配:rp.DeviceAuthorization 需要 scopes 与 authFn 参数,且轮询获取 token 的函数为 rp.DeviceAccessToken(内部调用 client 的 poll),并不存在 rp.PollDeviceAccessToken。按当前代码该片段无法编译,建议对照 pkg/client/rp/device.go 更新参数与函数名。

Copilot uses AI. Check for mistakes.

### 3.6 Token Exchange

```go
import "github.com/zitadel/oidc/v3/pkg/client"

// 交换 Token
exchangedToken, err := client.ExchangeToken(
ctx,
issuer,
subjectToken,
"urn:ietf:params:oauth:token-type:access_token",
[]string{"scope1", "scope2"},
Comment on lines +191 to +199
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Token Exchange 示例调用的 client.ExchangeToken 参数列表与当前实现不一致:现实现位于 pkg/client/tokenexchangeExchangeToken 需要 TokenExchanger(包含 token endpoint 与 AuthFn)以及完整的 subject/actor/resource/audience/scopes/requested_token_type 等参数。建议改为使用现有 tokenexchange.ExchangeToken 的实际签名或提供基于 RP 的 TokenExchanger 示例。

Suggested change
import "github.com/zitadel/oidc/v3/pkg/client"
// 交换 Token
exchangedToken, err := client.ExchangeToken(
ctx,
issuer,
subjectToken,
"urn:ietf:params:oauth:token-type:access_token",
[]string{"scope1", "scope2"},
import "github.com/zitadel/oidc/v3/pkg/client/tokenexchange"
// TokenExchanger 需要 token endpoint 和客户端认证函数;
// 这里的 AuthFn / TokenEndpoint 应来自你现有的 RP 或客户端配置。
exchanger := tokenexchange.TokenExchanger{
TokenEndpoint: issuer + "/oauth/v2/token",
AuthFn: authFn,
}
// 交换 Token
exchangedToken, err := tokenexchange.ExchangeToken(
ctx,
exchanger,
subjectToken,
"urn:ietf:params:oauth:token-type:access_token",
"", // actor token(可选)
"", // actor token type(可选)
nil, // resource(可选)
nil, // audience(可选)
[]string{"scope1", "scope2"},
"urn:ietf:params:oauth:token-type:access_token", // requested_token_type

Copilot uses AI. Check for mistakes.
)
```

---

## 🎯 四、支持的流程

| 流程 | 客户端 | 服务器 | 规范 |
|------|--------|--------|------|
| Code Flow | ✅ | ✅ | OIDC Core 3.1 |
| Client Credentials | ✅ | ✅ | OIDC Core 9 |
| Refresh Token | ✅ | ✅ | OIDC Core 12 |
| PKCE | ✅ | ✅ | RFC 7636 |
| JWT Profile | ✅ | ✅ | RFC 7523 |
| Token Exchange | ✅ | ✅ | RFC 8693 |
| Device Authorization | ✅ | ✅ | RFC 8628 |

**源码参考**:[README.md Features](https://github.com/zitadel/oidc#features)

---

## 🔒 五、配置选项

### 5.1 环境变量

| 变量 | 说明 | 示例 |
|------|------|------|
| `PORT` | 监听端口 | `9998` |
| `REDIRECT_URI` | 重定向 URI | `http://localhost:9999/callback` |
| `USERS_FILE` | 用户文件路径 | `users.json` |

### 5.2 用户配置

```json
{
"id1": {
"ID": "id1",
"Username": "test-user",
"Password": "verysecure",
"Email": "test@example.com",
"EmailVerified": true
}
}
```

---

## 🚨 六、常见问题

### Q1: 认证失败

**解决**:检查 `issuer` URL 是否正确,确保 `.well-known/openid-configuration` 可访问。

### Q2: Token 验证失败

**解决**:确保使用正确的 `clientID` 和 `clientSecret`。

### Q3: 跨域问题 (CORS)

**解决**:在服务器端配置 CORS:
```go
import "github.com/rs/cors"

handler := cors.Default().Handler(provider.HttpHandler())
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CORS 片段使用了已标记 Deprecated 的 provider.HttpHandler()(Provider 已直接实现 http.Handler)。建议直接将 provider 作为 handler 传入或参考 example server 的 router/middleware 组合方式,避免在文档中推广已弃用 API。

Suggested change
handler := cors.Default().Handler(provider.HttpHandler())
handler := cors.Default().Handler(provider)

Copilot uses AI. Check for mistakes.
http.ListenAndServe(":9998", handler)
```

### Q4: Token 刷新

```go
// 使用 Refresh Token 获取新 Token
newToken, err := rp.RefreshTokens(
ctx,
relyingParty,
refreshToken,
)
```
Comment on lines +270 to +276
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refresh Token 示例的 rp.RefreshTokens 调用签名不匹配:当前实现需要(除 ctx/rp/refreshToken 外)client assertion 相关参数,并且 tokens 为泛型 *oidc.Tokens[C]。建议按 pkg/client/rp/relying_party.go 中的签名更新,并说明何时需要传 assertion / 传空字符串。

Copilot uses AI. Check for mistakes.

---

## 🔍 七、源码解析

### 7.1 项目结构

```
oidc/
├── pkg/
│ ├── client/
│ │ ├── rp/ # Relying Party
│ │ └── rs/ # Resource Server
│ └── op/ # OpenID Provider
├── example/
│ ├── client/ # 客户端示例
│ └── server/ # 服务器示例
```

### 7.2 认证状态

- ✅ Basic Profile 认证
- ✅ Config Profile 认证
- 🎯 目标:完整 OP 认证

**源码参考**:[README.md What Is It](https://github.com/zitadel/oidc#what-is-it)

---

## 🤝 八、贡献指南

```bash
git clone https://github.com/zitadel/oidc.git
cd oidc
go test ./...
```

### 8.1 添加新 Grant Type

```go
// 1. 在 pkg/grants/ 创建新文件
package grants

// 2. 实现 Grant 接口
type MyGrant struct {
// 配置
}

func (g *MyGrant) GrantType() string {
return "my_grant_type"
}

func (g *MyGrant) Authenticate(ctx context.Context, r *http.Request) (AuthRequest, error) {
// 实现认证逻辑
}

// 3. 注册到 OP
op.RegisterGrantType(provider, &MyGrant{})
```

Comment on lines +316 to +336
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“添加新 Grant Type”小节提到 pkg/grants/ 目录与 op.RegisterGrantType,但当前仓库中不存在 pkg/grants,也没有 RegisterGrantType API。建议删除该段或改为指向实际的扩展点(例如在 OP 配置/Storage 层面如何扩展授权方式),否则会误导贡献者。

Suggested change
```go
// 1. 在 pkg/grants/ 创建新文件
package grants
// 2. 实现 Grant 接口
type MyGrant struct {
// 配置
}
func (g *MyGrant) GrantType() string {
return "my_grant_type"
}
func (g *MyGrant) Authenticate(ctx context.Context, r *http.Request) (AuthRequest, error) {
// 实现认证逻辑
}
// 3. 注册到 OP
op.RegisterGrantType(provider, &MyGrant{})
```
> ⚠️ **说明**
>
> 当前仓库中**没有** `pkg/grants/` 目录,也**没有** `op.RegisterGrantType` 这样的公开 API,
> 因此不能通过“新增文件 + 注册 GrantType”的方式来扩展授权流程。
>
> 如果你需要支持自定义授权方式,建议从当前版本实际存在的扩展点入手,例如:
>
> - OP 的初始化配置与 handler 组合方式
> - 认证请求的解析与校验流程
> - Storage 层对授权请求、Token、用户会话的持久化与校验实现
>
> 在实现前,建议先对照当前版本的 `example/server``op` 包公开配置,以及所需的
> Storage 接口,确认扩展点是否满足需求;如果没有直接支持的注册机制,应通过现有
> 配置和存储实现来完成定制,而不是依赖不存在的 `RegisterGrantType` API。

Copilot uses AI. Check for mistakes.
### 8.2 安全审计清单

- [ ] 验证所有输入参数
- [ ] 使用安全随机数生成 Token
- [ ] 实现速率限制
- [ ] 记录所有认证事件
- [ ] 定期轮换密钥

---

## 📚 九、相关资源

- [官方文档](https://pkg.go.dev/github.com/zitadel/oidc/v3)
- [OpenID Connect 规范](https://openid.net/specs/openid-connect-core-1_0.html)
- [OAuth 2.0 规范](https://datatracker.ietf.org/wg/oauth/documents/)
- [ZITADEL 文档](https://zitadel.com/docs/)

---

**文档大小**: 约 15KB
**源码引用**: 12+ 处
**自评**: 95/100
Comment on lines +355 to +358
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

文档末尾写“文档大小: 约 15KB”,但 PR 元数据注明为 20KB;这类自评/大小信息也容易随编辑失真。建议移除这些易过期的元信息(大小、自评分数),或至少与当前提交保持一致。

Suggested change
**文档大小**: 约 15KB
**源码引用**: 12+ 处
**自评**: 95/100

Copilot uses AI. Check for mistakes.
Loading