You've already forked agentic-coding-workflow
feat(mcp): auth middleware (Bearer + ?token= fallback)
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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 的鉴权快照。
|
||||
// 不直接暴露 *MCPToken(隐藏 hash 字段,避免下游误用)。
|
||||
type AuthSession struct {
|
||||
TokenID string // 用于审计 metadata 写入
|
||||
UserID string // user_id 字符串形式(避免反复 uuid.UUID -> string 转换)
|
||||
Scope Scope
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user