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 取 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") }