Skip to content
/ oauth2 Public

A lightweight and extensible SAAS OAuth 2.0 server and client written in Go. Supports authorization code, client credentials, and password grants. Ideal for modern auth systems and microservices.

License

Notifications You must be signed in to change notification settings

nilorg/oauth2

Repository files navigation

oauth2

OAuth 2.0 授权框架的 Go 语言实现,支持 SaaS 多租户场景。

Go implementation of OAuth 2.0 Authorization Framework with SaaS multi-tenant support.

Features / 特性

  • ✅ 授权码模式 (Authorization Code)
  • ✅ 简化模式 (Implicit)
  • ✅ 密码模式 (Resource Owner Password Credentials)
  • ✅ 客户端凭证模式 (Client Credentials)
  • ✅ 设备授权模式 (Device Code) - RFC 8628
  • ✅ 令牌内省 (Token Introspection) - RFC 7662
  • ✅ 令牌撤销 (Token Revocation) - RFC 7009
  • PKCE 支持 - RFC 7636 增强公开客户端安全性
  • SaaS 多租户动态 Issuer - 根据请求域名动态生成 JWT issuer
  • SaaS 多租户动态 JWT 密钥 - 每个租户使用独立的签名密钥
  • 反向代理支持 - 支持 X-Forwarded-* 头部

Installation / 安装

go get -u github.com/nilorg/oauth2

Import / 导入

import "github.com/nilorg/oauth2"

Quick Start / 快速开始

基础用法 / Basic Usage

srv := oauth2.NewServer()
// 配置回调函数...
srv.InitWithError()

SaaS 多租户场景 / SaaS Multi-tenant Scenario

srv := oauth2.NewServer(
    // 动态 Issuer:根据请求 Host 自动生成 JWT 的 iss 字段
    oauth2.ServerIssuerFunc(func(ctx context.Context, req oauth2.IssuerRequest) string {
        // req.Host   = "tenant1.example.com"
        // req.Scheme = "https"
        return fmt.Sprintf("%s://%s", req.Scheme, req.Host)
    }),
)

反向代理场景 / Reverse Proxy Scenario

srv := oauth2.NewServer(
    // 使用内置的反向代理支持,从 X-Forwarded-* 头部获取信息
    oauth2.ServerIssuerRequestFunc(oauth2.ProxyIssuerRequestFunc),
    oauth2.ServerIssuerFunc(func(ctx context.Context, req oauth2.IssuerRequest) string {
        return fmt.Sprintf("%s://%s", req.Scheme, req.Host)
    }),
)

自定义 IssuerRequest 提取 / Custom IssuerRequest Extraction

srv := oauth2.NewServer(
    oauth2.ServerIssuerRequestFunc(func(r *http.Request) oauth2.IssuerRequest {
        return oauth2.IssuerRequest{
            Host:   r.Header.Get("X-Tenant-Domain"),
            Scheme: r.Header.Get("X-Forwarded-Proto"),
        }
    }),
    oauth2.ServerIssuerFunc(func(ctx context.Context, req oauth2.IssuerRequest) string {
        return fmt.Sprintf("%s://%s", req.Scheme, req.Host)
    }),
)

静态 Issuer(单租户)/ Static Issuer (Single Tenant)

srv := oauth2.NewServer(
    oauth2.ServerIssuer("https://auth.example.com"),
)

多租户 JWT 密钥 / Multi-tenant JWT Key

// 每个租户使用独立的 JWT 签名密钥
srv.AccessToken = oauth2.NewMultiTenantAccessToken(func(ctx context.Context, issuer string) []byte {
    // issuer = "https://tenant1.example.com"
    // 根据 issuer 从数据库/配置中获取对应租户的密钥
    return getTenantJwtKey(issuer)
})

PKCE Support / PKCE 支持

PKCE (Proof Key for Code Exchange) 是 RFC 7636 定义的扩展,用于增强公开客户端(如移动应用、单页应用)的安全性。

客户端实现 / Client Implementation

// 1. 生成 code_verifier 和 code_challenge
codeVerifier := oauth2.RandomCodeVerifier()
codeChallenge := oauth2.GenerateCodeChallenge(codeVerifier, oauth2.CodeChallengeMethodS256)

// 2. 授权请求中包含 code_challenge
// GET /authorize?response_type=code&client_id=xxx&redirect_uri=xxx
//     &code_challenge=xxx&code_challenge_method=S256

// 3. Token 请求中包含 code_verifier
// POST /token
//     grant_type=authorization_code&code=xxx&code_verifier=xxx

服务端实现 / Server Implementation

// GenerateCode 需要存储 PKCE 参数
srv.GenerateCode = func(ctx context.Context, clientID, openID, redirectURI string, 
    scope []string, codeChallenge, codeChallengeMethod string) (string, error) {
    code := oauth2.RandomCode()
    // 存储: code -> {clientID, openID, redirectURI, scope, codeChallenge, codeChallengeMethod}
    return code, nil
}

// VerifyCode 需要返回 PKCE 参数
srv.VerifyCode = func(ctx context.Context, code, clientID, redirectURI string) (*oauth2.CodeValue, error) {
    // 从存储中获取
    return &oauth2.CodeValue{
        ClientID:            clientID,
        RedirectURI:         redirectURI,
        Scope:               []string{"read", "write"},
        CodeChallenge:       savedCodeChallenge,       // PKCE
        CodeChallengeMethod: savedCodeChallengeMethod, // PKCE
    }, nil
}

Examples / 示例

oauth2-server

server/client examples

Documentation / 文档参考

  1. 《理解OAuth 2.0》阮一峰
  2. RFC 6749 - The OAuth 2.0 Authorization Framework
  3. RFC 7636 - Proof Key for Code Exchange (PKCE)
  4. RFC 8628 - OAuth 2.0 Device Authorization Grant
  5. RFC 7662 - OAuth 2.0 Token Introspection
  6. RFC 7009 - OAuth 2.0 Token Revocation

Grant Types / 授权模式

Authorization Code / 授权码模式

授权码模式是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。

Implicit / 简化模式

简化模式不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤。

Resource Owner Password Credentials / 密码模式

用户向客户端提供自己的用户名和密码,客户端使用这些信息向"服务商提供商"索要授权。

Client Credentials / 客户端凭证模式

客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。

Device Code / 设备模式

设备授权模式用于无法输入的设备(如智能电视、IoT设备等)。

Server Configuration / 服务器配置

Server Options / 服务器选项

Option Description
ServerLogger(log) 设置日志记录器
ServerIssuer(issuer) 设置静态 JWT issuer
ServerIssuerFunc(fn) 设置动态 JWT issuer 函数(SaaS多租户)
ServerIssuerRequestFunc(fn) 设置从HTTP请求提取信息的函数
ServerDeviceAuthorizationEndpointEnabled(bool) 启用设备授权端点
ServerIntrospectEndpointEnabled(bool) 启用令牌内省端点
ServerTokenRevocationEnabled(bool) 启用令牌撤销端点

AccessToken 配置 / AccessToken Configuration

Function Description
NewDefaultAccessToken(key) 创建静态密钥的 AccessToken 处理器(单租户)
NewMultiTenantAccessToken(fn) 创建动态密钥的 AccessToken 处理器(多租户)

PKCE 工具函数 / PKCE Utility Functions

Function Description
RandomCodeVerifier() 生成随机的 code_verifier (43字符)
GenerateCodeChallenge(verifier, method) 根据 verifier 生成 code_challenge
VerifyCodeChallenge(challenge, method, verifier) 验证 code_verifier 是否匹配

PKCE 常量 / PKCE Constants

Constant Description
CodeChallengeMethodPlain PKCE plain 方法
CodeChallengeMethodS256 PKCE S256 方法 (推荐)

Complete Server Example / 完整服务器示例

package main

import (
    "context"
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/nilorg/oauth2"
)

var clients = map[string]string{
    "oauth2_client": "password",
}

func main() {
    srv := oauth2.NewServer(
        // SaaS多租户:动态Issuer
        oauth2.ServerIssuerFunc(func(ctx context.Context, req oauth2.IssuerRequest) string {
            return fmt.Sprintf("%s://%s", req.Scheme, req.Host)
        }),
        oauth2.ServerDeviceAuthorizationEndpointEnabled(true),
    )

    srv.VerifyClient = func(ctx context.Context, basic *oauth2.ClientBasic) (err error) {
        pwd, ok := clients[basic.ID]
        if !ok || basic.Secret != pwd {
            return oauth2.ErrInvalidClient
        }
        return nil
    }

    srv.VerifyClientID = func(ctx context.Context, clientID string) (err error) {
        if _, ok := clients[clientID]; !ok {
            return oauth2.ErrInvalidClient
        }
        return nil
    }

    srv.VerifyCode = func(ctx context.Context, code, clientID, redirectURI string) (*oauth2.CodeValue, error) {
        return &oauth2.CodeValue{
            ClientID:    clientID,
            RedirectURI: redirectURI,
            Scope:       []string{"read", "write"},
            // PKCE: 如果启用 PKCE,需要从存储中返回 CodeChallenge 和 CodeChallengeMethod
        }, nil
    }

    srv.GenerateCode = func(ctx context.Context, clientID, openID, redirectURI string, 
        scope []string, codeChallenge, codeChallengeMethod string) (string, error) {
        code := oauth2.RandomCode()
        // 存储 code 信息,包括 PKCE 参数
        return code, nil
    }

    srv.VerifyRedirectURI = func(ctx context.Context, clientID, redirectURI string) error {
        return nil
    }

    srv.VerifyPassword = func(ctx context.Context, clientID, username, password string) (string, error) {
        if username == "admin" && password == "123456" {
            return "user_001", nil
        }
        return "", oauth2.ErrUnauthorizedClient
    }

    srv.VerifyScope = func(ctx context.Context, scopes []string, clientID string) error {
        return nil
    }

    srv.VerifyGrantType = func(ctx context.Context, clientID, grantType string) error {
        return nil
    }

    srv.AccessToken = oauth2.NewDefaultAccessToken([]byte("your-jwt-secret"))

    srv.GenerateDeviceAuthorization = func(ctx context.Context, issuer, verificationURI, clientID string, scope []string) (*oauth2.DeviceAuthorizationResponse, error) {
        return &oauth2.DeviceAuthorizationResponse{
            DeviceCode:      oauth2.RandomCode(),
            UserCode:        oauth2.RandomUserCode(),
            VerificationURI: issuer + verificationURI,
            ExpiresIn:       1800,
            Interval:        5,
        }, nil
    }

    srv.VerifyDeviceCode = func(ctx context.Context, deviceCode, clientID string) (*oauth2.DeviceCodeValue, error) {
        return nil, nil
    }

    if err := srv.InitWithError(); err != nil {
        panic(err)
    }

    r := gin.Default()
    oauth2Group := r.Group("/oauth2")
    {
        oauth2Group.GET("/authorize", func(c *gin.Context) {
            srv.HandleAuthorize(c.Writer, c.Request)
        })
        oauth2Group.POST("/token", func(c *gin.Context) {
            srv.HandleToken(c.Writer, c.Request)
        })
        oauth2Group.POST("/device_authorization", func(c *gin.Context) {
            srv.HandleDeviceAuthorization(c.Writer, c.Request)
        })
    }

    http.ListenAndServe(":8003", r)
}

Test / 测试

# Password Grant
curl -X POST http://localhost:8003/oauth2/token \
  -u oauth2_client:password \
  -d 'grant_type=password&username=admin&password=123456&scope=read'

# Client Credentials Grant  
curl -X POST http://localhost:8003/oauth2/token \
  -u oauth2_client:password \
  -d 'grant_type=client_credentials&scope=read'

# Refresh Token (所有 grant_type 都支持刷新)
curl -X POST http://localhost:8003/oauth2/token \
  -u oauth2_client:password \
  -d 'grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN'

PKCE 测试 / PKCE Test

# 1. 生成 code_verifier 和 code_challenge (S256)
# code_verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
# code_challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM

# 2. 授权请求 (包含 code_challenge)
# GET /oauth2/authorize?response_type=code&client_id=oauth2_client
#     &redirect_uri=http://localhost:8080/callback
#     &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
#     &code_challenge_method=S256

# 3. Token 请求 (包含 code_verifier)
curl -X POST http://localhost:8003/oauth2/token \
  -u oauth2_client:password \
  -d 'grant_type=authorization_code&code=YOUR_CODE&redirect_uri=http://localhost:8080/callback&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'

JWT Payload / JWT 载荷

标准声明 (Registered Claims):

Claim Description
iss 令牌颁发者 (Issuer) - 在SaaS场景下会动态生成
sub 令牌主体 (Subject) - 通常是用户标识
aud 令牌受众 (Audience)
exp 过期时间 (Expiration Time)
nbf 生效时间 (Not Before)
iat 颁发时间 (Issued At)
jti 令牌唯一标识 (JWT ID)

License

MIT

About

A lightweight and extensible SAAS OAuth 2.0 server and client written in Go. Supports authorization code, client credentials, and password grants. Ideal for modern auth systems and microservices.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages