OAuth 2.0 与 JWT
概述#
OAuth 2.0 是一种流行的开放标准授权框架,它使您能够验证传入请求是否被授权使用您的 API。
在使用 Huma 与 OAuth 2.0 结合时,有三个主要部分:
- 向客户端应用程序颁发访问令牌
- 记录授权方案和所需权限
- 授权传入请求
颁发访问令牌#
Huma 不提供任何内置的访问令牌颁发功能。相反,您可以使用任何现有的库或服务来颁发令牌。为简单起见,我们假设您使用第三方服务来管理用户并颁发令牌,例如 Auth0 或 Okta。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 和几个定义的范围:
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)
在注册操作时,您可以引用该操作的授权方案和所需范围:
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 服务。有许多这样的网关(例如 Traefik、Istio 等),并且配置它们的方式有很多,但它们之间的总体思路相似:
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 验证它。它还会检查令牌是否具有该操作所需的范围(如果定义了任何范围)。
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 时,请确保包含此中间件:
支持不同的令牌格式#
如前所述,OAuth 2.0 标准并未指定访问令牌的格式——它仅定义了如何获取一个。虽然 JWT 是一种非常流行的格式,但给定的 OAuth 2.0 服务或库可能以不同的格式颁发访问令牌。以上概述的内容要旨应该可以适应支持此类令牌,但显然需要不同的验证和信息提取方法。在不透明令牌的情况下,中间件内可能需要与 IAM 服务器进行额外的交互,例如调用 introspection 端点。
可选:客户端自动配置#
一些客户端如 Restish 支持 基于 OpenAPI 的认证自动配置。这意味着您可以配置客户端获取 OpenAPI 文档并自动配置自身使用正确的认证机制。这是通过在 OpenAPI 中添加 x-cli-config 扩展来实现的: