Skip to content

OAuth 2.0 与 JWT

概述#

OAuth 2.0 是一种流行的开放标准授权框架,它使您能够验证传入请求是否被授权使用您的 API。

在使用 Huma 与 OAuth 2.0 结合时,有三个主要部分:

  1. 向客户端应用程序颁发访问令牌
  2. 记录授权方案和所需权限
  3. 授权传入请求

颁发访问令牌#

Huma 不提供任何内置的访问令牌颁发功能。相反,您可以使用任何现有的库或服务来颁发令牌。为简单起见,我们假设您使用第三方服务来管理用户并颁发令牌,例如 Auth0Okta。OAuth 2.0 授权的简化流程图如下所示:

graph LR
	User -->|1: Login| Auth0
	Auth0 -->|2: Issue access token| User
	Auth0 -.->|Refresh JWKS| API
	User --->|3: Make request| API
	API -->|4: Verify access token & roles| Validate
	Validate -->|5: Accept/reject| API
	API --->|6: Success| Handler

访问令牌可能以不同的变体和格式颁发,但在本文档的其余部分,我们假设它们是 JWTs

您将配置第三方服务使用 OAuth 2.0 流程(如授权码或客户端凭据等)来颁发访问令牌,并将获得例如授权和令牌 URL,这些 URL 将在 OpenAPI 中使用,并在后续配置客户端获取访问令牌。

如果**不**使用第三方服务,您需要设置签名颁发机构、发布您自己的 JWKS,并自行颁发短寿命令牌。这超出了本指南的范围,但您可以查看 github.com/lestrrat-go/jwx 以获取一个有助于此的库。

在 OpenAPI 中记录授权方案#

接下来,您需要在 OpenAPI 文档中记录授权方案。这是使用 SecuritySchemes 组件完成的。以下是一个定义 OAuth 2.0 授权码流程的示例,使用上面提到的 URL 和几个定义的范围:

main.go
router := chi.NewMux()

config := huma.DefaultConfig("My API", "1.0.0")
config.Components.SecuritySchemes = map[string]*huma.SecurityScheme{
	// Example Authorization Code flow.
	"myAuth": {
		Type: "oauth2",
		Flows: &huma.OAuthFlows{
			AuthorizationCode: &huma.OAuthFlow{
				AuthorizationURL: "https://example.com/oauth/authorize",
				TokenURL:         "https://example.com/oauth/token",
				Scopes: map[string]string{
					"scope1": "Scope 1 description...",
					"scope2": "Scope 2 description...",
				},
			},
		},
	},

	// Example alternative describing the use of JWTs without documenting how
	// they are issued or which flows might be supported. This is simpler but
	// tells clients less information.
	"anotherAuth": {
		Type:         "http",
		Scheme:       "bearer",
		BearerFormat: "JWT",
	},
}

api := humachi.New(router, config)

在注册操作时,您可以引用该操作的授权方案和所需范围:

main.go
huma.Register(api, huma.Operation{
	OperationID: "get-greeting",
	Summary:     "Get a greeting",
	Method:      http.MethodGet,
	Path:        "/greeting/{name}",
	Security: []map[string][]string{
		{"myAuth": {"scope1"}},
	},
}, func(ctx context.Context, input *GreetingInput) (*GreetingOutput, error) {
	// TODO: operation implementation goes here
	return nil, nil
})

Warning

到目前为止,以上代码仅记录了授权方案和所需范围,但并未实际授权传入请求。下一节将解释如何实现后者。

授权传入请求#

认证和授权发生的位置取决于您的服务设置。在某些场景中,您可能有一个 API 网关来处理认证并将请求转发到您的服务。在其他场景中,您可能希望在服务中处理认证。

API 网关认证#

在 API 网关场景中,您通常会配置网关检查 Authorization 头部中的令牌,并根据 JWKS URL 验证它。如果令牌有效,则网关将请求转发到您的 API 服务。有许多这样的网关(例如 TraefikIstio 等),并且配置它们的方式有很多,但它们之间的总体思路相似:

graph LR
	APIGateway[API Gateway]
	AuthMiddleware[Auth Middleware]

	User -->|Request| APIGateway
	APIGateway --> AuthMiddleware
	AuthMiddleware --> APIGateway
	APIGateway --->|Forward| API

在这种情况下,根据您的安全要求,您可能可以跳过本节,因为所有传入到您的 API 的请求都已经被网关审核。在这种场景中,前一节的 Huma 代码主要作为您的客户端文档。

Huma 认证中间件#

Huma 提供了可以用于在 API 服务内部授权传入请求的中间件功能。以下是一个示例,它将检查 Authorization 头部中的令牌,并根据您的 JWT 颁发者(例如 Auth0/Okta)提供的 JWKS URL 验证它。它还会检查令牌是否具有该操作所需的范围(如果定义了任何范围)。

main.go
import (
	"github.com/lestrrat-go/jwx/v2/jwk"
	"github.com/lestrrat-go/jwx/v2/jwt"
)

// NewJWKSet creates an auto-refreshing key set to validate JWT signatures.
func NewJWKSet(jwkUrl string) jwk.Set {
    jwkCache := jwk.NewCache(context.Background())

    // register a minimum refresh interval for this URL.
    // when not specified, defaults to Cache-Control and similar resp headers
    err := jwkCache.Register(jwkUrl, jwk.WithMinRefreshInterval(10*time.Minute))
    if err != nil {
        panic("failed to register jwk location")
    }

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // fetch once on application startup
    _, err = jwkCache.Refresh(ctx, jwkUrl)
    if err != nil {
        panic("failed to fetch on startup")
    }
    // create the cached key set
    return jwk.NewCachedSet(jwkCache, jwkUrl)
}

// NewAuthMiddleware creates a middleware that will authorize requests based on
// the required scopes for the operation.
func NewAuthMiddleware(api huma.API, jwksURL string) func(ctx huma.Context, next func(huma.Context)) {
	keySet := NewJWKSet(jwksURL)

	return func(ctx huma.Context, next func(huma.Context)) {
		var anyOfNeededScopes []string
		isAuthorizationRequired := false
		for _, opScheme := range ctx.Operation().Security {
			var ok bool
			if anyOfNeededScopes, ok = opScheme["myAuth"]; ok {
				isAuthorizationRequired = true
				break
			}
		}

		if !isAuthorizationRequired {
			next(ctx)
			return
		}

		token := strings.TrimPrefix(ctx.Header("Authorization"), "Bearer ")
		if len(token) == 0 {
			huma.WriteErr(api, ctx, http.StatusUnauthorized, "Unauthorized")
			return
		}

		// Parse and validate the JWT.
		parsed, err := jwt.ParseString(token,
			jwt.WithKeySet(keySet),
			jwt.WithValidate(true),
			jwt.WithIssuer("my-issuer"),
			jwt.WithAudience("my-audience"),
		)
		if err != nil {
			huma.WriteErr(api, ctx, http.StatusUnauthorized, "Unauthorized")
			return
		}

		// Ensure the claims required for this operation are present.
		scopes, _ := parsed.Get("scopes")
		if scopes, ok := scopes.([]string); ok {
			for _, scope := range scopes {
				if slices.Contains(anyOfNeededScopes, scope) {
					next(ctx)
					return
				}
			}
		}

		huma.WriteErr(api, ctx, http.StatusForbidden, "Forbidden")
	}
}

最后,在配置您的 API 时,请确保包含此中间件:

main.go
api.UseMiddleware(NewAuthMiddleware(api, "https://example.com/.well-known/jwks.json"))

支持不同的令牌格式#

如前所述,OAuth 2.0 标准并未指定访问令牌的格式——它仅定义了如何获取一个。虽然 JWT 是一种非常流行的格式,但给定的 OAuth 2.0 服务或库可能以不同的格式颁发访问令牌。以上概述的内容要旨应该可以适应支持此类令牌,但显然需要不同的验证和信息提取方法。在不透明令牌的情况下,中间件内可能需要与 IAM 服务器进行额外的交互,例如调用 introspection 端点。

可选:客户端自动配置#

一些客户端如 Restish 支持 基于 OpenAPI 的认证自动配置。这意味着您可以配置客户端获取 OpenAPI 文档并自动配置自身使用正确的认证机制。这是通过在 OpenAPI 中添加 x-cli-config 扩展来实现的:

main.go
config.Extensions["x-cli-config"] = huma.AutoConfig{ /* ... */ }