You've already forked agentic-coding-workflow
82 lines
2.8 KiB
Go
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")
|
|
}
|