feat: 实现 OIDC 服务端会话与请求刷新
使用 AES-256-GCM 保存认证中心令牌,并以随机 Cookie 哈希关联 PostgreSQL 会话。加入闲置与绝对期限、Refresh Token 轮换以及多实例刷新锁。
This commit is contained in:
@@ -58,6 +58,18 @@ const userContextKey contextKey = "easyai-auth-user"
|
||||
|
||||
var ErrUnauthorized = errors.New("unauthorized")
|
||||
|
||||
type RequestAuthError struct {
|
||||
Status int
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *RequestAuthError) Error() string { return e.Code }
|
||||
|
||||
func NewRequestAuthError(status int, code, message string) error {
|
||||
return &RequestAuthError{Status: status, Code: code, Message: message}
|
||||
}
|
||||
|
||||
type Authenticator struct {
|
||||
JWTSecret string
|
||||
ServerMainBaseURL string
|
||||
@@ -67,6 +79,7 @@ type Authenticator struct {
|
||||
HTTPClient *http.Client
|
||||
LocalAPIKeyVerifier func(ctx context.Context, apiKey string) (*User, error)
|
||||
OIDCVerifier *OIDCVerifier
|
||||
OIDCSessionResolver func(ctx context.Context, sessionID string) (*User, error)
|
||||
LegacyJWTEnabled bool
|
||||
}
|
||||
|
||||
@@ -95,13 +108,23 @@ func (a *Authenticator) Require(permission Permission, next http.Handler) http.H
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := a.Authenticate(r)
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), ErrUnauthorized.Error()+":") {
|
||||
slog.WarnContext(r.Context(), "OIDC authentication rejected", "reason", err.Error())
|
||||
}
|
||||
if permission == PermissionPublic {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
var requestError *RequestAuthError
|
||||
if errors.As(err, &requestError) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(requestError.Status)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{
|
||||
"message": requestError.Message, "status": requestError.Status, "code": requestError.Code,
|
||||
}})
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(err.Error(), ErrUnauthorized.Error()+":") {
|
||||
slog.WarnContext(r.Context(), "OIDC authentication rejected", "reason", err.Error())
|
||||
}
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@@ -115,7 +138,6 @@ func (a *Authenticator) Require(permission Permission, next http.Handler) http.H
|
||||
|
||||
func (a *Authenticator) Authenticate(r *http.Request) (*User, error) {
|
||||
token := extractBearer(r.Header.Get("Authorization"))
|
||||
fromOIDCSessionCookie := false
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(r.Header.Get("x-comfy-api-key"))
|
||||
}
|
||||
@@ -127,16 +149,16 @@ func (a *Authenticator) Authenticate(r *http.Request) (*User, error) {
|
||||
}
|
||||
if token == "" {
|
||||
if cookie, err := r.Cookie(OIDCSessionCookieName); err == nil {
|
||||
token = strings.TrimSpace(cookie.Value)
|
||||
fromOIDCSessionCookie = token != ""
|
||||
sessionID := strings.TrimSpace(cookie.Value)
|
||||
if sessionID == "" || a.OIDCSessionResolver == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
return a.OIDCSessionResolver(r.Context(), sessionID)
|
||||
}
|
||||
}
|
||||
if token == "" {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
if fromOIDCSessionCookie && jwtAlgorithm(token) != "RS256" && jwtAlgorithm(token) != "ES256" {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
if strings.HasPrefix(token, "sk-") {
|
||||
return a.verifyAPIKey(r.Context(), token)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user