Files
agentic-coding-workflow/internal/mcp/auth_middleware.go
T
2026-06-22 08:55:57 +08:00

82 lines
2.8 KiB
Go

package mcp
import (
"context"
"net/http"
"strings"
"github.com/google/uuid"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http"
"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
)
// authCtxKey 是 ctx 上注入鉴权结果的 key 类型。
type authCtxKey struct{}
// AuthContextKey 暴露给同包文件(auth_middleware / handler / tools)从 ctx 取值。
var AuthContextKey = authCtxKey{}
// AuthSession 是中间件解析后注入 ctx 的鉴权快照。
// 不直接暴露 *Token(隐藏 hash 字段,避免下游误用)。
type AuthSession struct {
TokenID string // 用于审计 metadata 写入
UserID string // user_id 字符串形式(避免反复 uuid.UUID -> string 转换)
Scope Scope
Issuer Issuer // system token(agent 会话持有)受额外限制,见 create_acp_session
// ACPSessionID 仅 system token 非 nil:标识持有该 token 的 agent 所在 acp session。
// request_subtask 据此把调用方映射回其编排 step。
ACPSessionID *uuid.UUID
}
// FromContext 从 ctx 取出 AuthSession,未鉴权时返 (nil, false)。
func FromContext(ctx context.Context) (*AuthSession, bool) {
s, ok := ctx.Value(AuthContextKey).(*AuthSession)
return s, ok
}
// NewAuthMiddleware 返回 chi 风格 middleware:
// 1. 从 Authorization: Bearer <token> 取 plaintext;fallback 到 ?token=
// 2. 调 svc.Authenticate 验证
// 3. 把 AuthSession 注入 ctx 后 next.ServeHTTP
// 4. 失败 → 调 httpx.WriteError 返 4xx
func NewAuthMiddleware(svc TokenService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
plain := extractPlaintext(r)
if plain == "" {
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()),
errs.New(errs.CodeMcpTokenInvalid, "missing token"))
return
}
tok, err := svc.Authenticate(r.Context(), plain)
if err != nil {
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err)
return
}
sess := &AuthSession{
TokenID: tok.ID.String(),
UserID: tok.UserID.String(),
Scope: tok.Scope,
Issuer: tok.Issuer,
ACPSessionID: tok.ACPSessionID,
}
ctx := context.WithValue(r.Context(), AuthContextKey, sess)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// extractPlaintext 优先取 Authorization 头部 Bearer 后的 token;空时回落 ?token= 查询参数。
// 这与 ACP WS 鉴权对齐(spec §4.3)。
func extractPlaintext(r *http.Request) string {
if h := r.Header.Get("Authorization"); h != "" {
const prefix = "Bearer "
if strings.HasPrefix(h, prefix) {
return strings.TrimSpace(h[len(prefix):])
}
}
return r.URL.Query().Get("token")
}