golang实现OpenID Connect认证的客户端与服务端插件库oidc的使用
Golang实现OpenID Connect认证的客户端与服务端插件库oidc的使用
什么是oidc库
这是一个为Go语言编写的易于使用的OIDC(OpenID Connect)标准的客户端(RP)和服务端(OP)实现。该库已通过OpenID Connect基础配置文件的认证。
基本概述
库中最重要的包:
/pkg
/client 使用OP获取、交换和验证令牌的客户端
/rp OIDC依赖方(客户端)的定义和实现
/rs OAuth资源服务器(API)的定义和实现
/op OIDC OpenID提供者(服务端)的定义和实现
/oidc 客户端和服务端共享的定义
/example
/client/api 使用令牌自省的API/资源服务器实现示例
/client/app 使用各种认证方法(代码、PKCE、JWT profile)演示授权码流程的Web应用/RP
/client/github 扩展OAuth2库的示例,提供带有重用令牌源的HTTP客户端
/client/service JWT Profile授权授予的演示
/server 包含一些基本登录UI的OpenID提供者实现示例(包括动态)
如何使用
快速开始示例
// 启动OIDC OP服务器
// OIDC发现地址: http://localhost:9998/.well-known/openid-configuration
go run github.com/zitadel/oidc/v3/example/server
// 启动OIDC Web客户端(在新终端中)
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
操作步骤:
- 在浏览器中打开 http://localhost:9999/login
- 你将被重定向到OP服务器和登录UI
- 使用用户
test-user@localhost
和密码verysecure
登录 - OP会将你重定向回客户端应用,显示用户信息
客户端代码示例
package main
import (
"context"
"log"
"net/http"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
func main() {
// 配置依赖方(RP)
issuer := "http://localhost:9998/"
clientID := "web"
clientSecret := "secret"
redirectURI := "http://localhost:9999/callback"
scopes := []string{"openid", "profile"}
// 创建依赖方
provider, err := rp.NewRelyingPartyOIDC(
context.Background(),
issuer,
clientID,
clientSecret,
redirectURI,
scopes,
)
if err != nil {
log.Fatal(err)
}
// 创建授权码流程处理器
http.Handle("/login", rp.AuthURLHandler(
rp.GeneratePKCEVerifier(), // 使用PKCE
provider,
[]oidc.AddAuthURLOpt{},
))
// 创建回调处理器
http.Handle("/callback", rp.CodeExchangeHandler(
func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty) {
// 在这里处理令牌和用户信息
log.Printf("ID Token: %v", tokens.IDToken)
log.Printf("Access Token: %v", tokens.AccessToken)
log.Printf("Refresh Token: %v", tokens.RefreshToken)
},
provider,
))
// 启动服务器
log.Println("Server listening on http://localhost:9999")
log.Fatal(http.ListenAndServe(":9999", nil))
}
服务端配置
示例服务器允许使用环境变量进行额外配置,可用于服务的端到端测试。
名称 | 格式 | 描述 |
---|---|---|
PORT | 1-65535之间的数字 | OIDC监听端口 |
REDIRECT_URI | 逗号分隔的URI | 允许的重定向URI列表 |
USERS_FILE | 本地文件系统中的json路径 | 用户及其数据和凭据 |
用户JSON示例:
{
"id2": {
"ID": "id2",
"Username": "test-user2",
"Password": "verysecure",
"FirstName": "Test",
"LastName": "User2",
"Email": "test-user2@zitadel.ch",
"EmailVerified": true,
"Phone": "",
"PhoneVerified": false,
"PreferredLanguage": "DE",
"IsAdmin": false
}
}
特性支持
特性 | 依赖方 | OpenID提供者 | 规范 |
---|---|---|---|
代码流 | 是 | 是 | OpenID Connect Core 1.0, 3.1节 |
隐式流 | 否 | 是 | OpenID Connect Core 1.0, 3.2节 |
混合流 | 否 | 尚未 | OpenID Connect Core 1.0, 3.3节 |
客户端凭据 | 是 | 是 | OpenID Connect Core 1.0, 9节 |
刷新令牌 | 是 | 是 | OpenID Connect Core 1.0, 12节 |
发现 | 是 | 是 | OpenID Connect Discovery 1.0 |
JWT Profile | 是 | 是 | RFC 7523 |
PKCE | 是 | 是 | RFC 7636 |
令牌交换 | 是 | 是 | RFC 8693 |
设备授权 | 是 | 是 | RFC 8628 |
mTLS | 尚未 | 尚未 | RFC 8705 |
后端通道注销 | 尚未 | 是 | OpenID Connect Back-Channel Logout 1.0 |
支持的Go版本
出于安全原因,我们仅支持和推荐使用最新的两个Go版本(✅)。 也能构建的版本标记为⚠️。
版本 | 支持 |
---|---|
<1.23 | ❌ |
1.23 | ✅ |
1.24 | ✅ |
为什么选择这个库
截至2020年,Go中没有很多可以处理服务端和客户端实现的OIDC库。ZITADEL致力于IAM(身份和访问管理)领域,因此我们需要可靠的框架来实现服务。
目标
- 将此库认证为OP
其他Go OpenID Connect库
-
https://github.com/coreos/go-oidc
go-oidc
仅支持RP,不适合用作OP,因此我们不能依赖它 -
https://github.com/ory/fosite
我们没有选择fosite
,因为它自己实现了OAuth 2.0,而不依赖golang提供的包。尽管如此,这是一个很棒的项目。
许可证
该库的全部功能现在是并将保持开源,任何人都可以免费使用。访问我们的网站并取得联系。
除非适用法律要求或书面同意,否则根据许可证分发的软件按"原样"分发,不附带任何明示或暗示的担保或条件。有关权限和限制的具体语言,请参阅许可证。
更多关于golang实现OpenID Connect认证的客户端与服务端插件库oidc的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于golang实现OpenID Connect认证的客户端与服务端插件库oidc的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
使用Golang实现OpenID Connect认证:oidc库指南
OpenID Connect (OIDC) 是构建在OAuth 2.0之上的身份认证层,允许客户端验证用户的身份并获取基本用户信息。在Go语言中,coreos/go-oidc
库提供了实现OIDC客户端和服务端的便捷方式。
安装oidc库
go get github.com/coreos/go-oidc/v3/oidc
客户端实现
1. 基本配置
package main
import (
"context"
"log"
"net/http"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
var (
clientID = "your-client-id"
clientSecret = "your-client-secret"
redirectURL = "http://localhost:8080/auth/callback"
// 例如: https://accounts.google.com
issuerURL = "your-oidc-provider-url"
)
func main() {
ctx := context.Background()
// 初始化provider
provider, err := oidc.NewProvider(ctx, issuerURL)
if err != nil {
log.Fatal(err)
}
// 配置OAuth2
oauth2Config := oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
// 初始化oidc配置
oidcConfig := &oidc.Config{
ClientID: clientID,
}
verifier := provider.Verifier(oidcConfig)
// 设置HTTP路由
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 生成随机state防止CSRF攻击
state := "random-state"
http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
})
http.HandleFunc("/auth/callback", func(w http.ResponseWriter, r *http.Request) {
// 验证state
if r.URL.Query().Get("state") != "random-state" {
http.Error(w, "Invalid state", http.StatusBadRequest)
return
}
// 交换授权码获取token
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
// 提取ID Token
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
return
}
// 验证ID Token
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return
}
// 提取用户信息
var claims struct {
Email string `json:"email"`
Name string `json:"name"`
Picture string `json:"picture"`
}
if err := idToken.Claims(&claims); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 在这里处理用户认证逻辑...
w.Write([]byte("Hello, " + claims.Name))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
服务端实现
虽然OIDC服务端通常由专门的身份提供商(如Keycloak, Auth0等)实现,但我们可以使用oidc库来验证令牌:
package main
import (
"context"
"log"
"net/http"
"github.com/coreos/go-oidc/v3/oidc"
)
var (
issuerURL = "your-oidc-provider-url"
clientID = "your-client-id"
)
func main() {
ctx := context.Background()
provider, err := oidc.NewProvider(ctx, issuerURL)
if err != nil {
log.Fatal(err)
}
verifier := provider.Verifier(&oidc.Config{ClientID: clientID})
http.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
// 从Authorization头获取token
rawToken := r.Header.Get("Authorization")
if rawToken == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// 验证token
token, err := verifier.Verify(ctx, rawToken)
if err != nil {
http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized)
return
}
// 提取claims
var claims struct {
Email string `json:"email"`
Name string `json:"name"`
}
if err := token.Claims(&claims); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 在这里处理授权逻辑...
w.Write([]byte("Protected resource accessed by: " + claims.Name))
})
log.Fatal(http.ListenAndServe(":8081", nil))
}
高级用法
1. 自定义claims验证
type customClaims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
func verifyToken(ctx context.Context, verifier *oidc.IDTokenVerifier, rawToken string) (*customClaims, error) {
token, err := verifier.Verify(ctx, rawToken)
if err != nil {
return nil, err
}
var claims customClaims
if err := token.Claims(&claims); err != nil {
return nil, err
}
if !claims.EmailVerified {
return nil, fmt.Errorf("email not verified")
}
return &claims, nil
}
2. 刷新令牌
func refreshToken(ctx context.Context, oauth2Config oauth2.Config, refreshToken string) (*oauth2.Token, error) {
token := &oauth2.Token{
RefreshToken: refreshToken,
Expiry: time.Now().Add(-time.Hour), // 强制过期
}
return oauth2Config.TokenSource(ctx, token).Token()
}
安全注意事项
- 始终验证state参数以防止CSRF攻击
- 使用HTTPS保护所有通信
- 安全存储客户端密钥和令牌
- 设置适当的令牌有效期
- 实现令牌撤销机制
通过go-oidc
库,我们可以相对容易地在Go应用中实现OpenID Connect认证流程。无论是构建客户端还是验证令牌的服务端组件,该库都提供了简洁的API来处理OIDC协议的大部分复杂性。