132 lines
4.2 KiB
Go
132 lines
4.2 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||
)
|
||
|
||
const (
|
||
errorCodeOIDCBrowserSessionDisabled = "OIDC_BROWSER_SESSION_DISABLED"
|
||
errorCodeOIDCSessionInvalid = "OIDC_SESSION_INVALID"
|
||
errorCodeOIDCSessionTooLarge = "OIDC_SESSION_TOKEN_TOO_LARGE"
|
||
errorCodeOIDCSessionCSRF = "OIDC_SESSION_CSRF_REJECTED"
|
||
maxOIDCSessionCookieTokenBytes = 3800
|
||
)
|
||
|
||
// createOIDCBrowserSession godoc
|
||
// @Summary 建立 OIDC 浏览器会话
|
||
// @Description 验证 Auth Center Access Token 后写入 HttpOnly 会话 Cookie;不会签发 Gateway JWT。
|
||
// @Tags auth
|
||
// @Success 204
|
||
// @Failure 400 {object} ErrorEnvelope
|
||
// @Failure 401 {object} ErrorEnvelope
|
||
// @Failure 404 {object} ErrorEnvelope
|
||
// @Router /api/v1/auth/oidc/session [post]
|
||
func (s *Server) createOIDCBrowserSession(w http.ResponseWriter, r *http.Request) {
|
||
if !s.cfg.OIDCEnabled || !s.cfg.OIDCBrowserSessionEnabled || s.auth == nil || s.auth.OIDCVerifier == nil {
|
||
writeError(w, http.StatusNotFound, "OIDC browser session is disabled", errorCodeOIDCBrowserSessionDisabled)
|
||
return
|
||
}
|
||
raw := bearerToken(r.Header.Get("Authorization"))
|
||
if raw == "" {
|
||
writeError(w, http.StatusUnauthorized, "valid OIDC access token is required", errorCodeOIDCSessionInvalid)
|
||
return
|
||
}
|
||
if len(raw) > maxOIDCSessionCookieTokenBytes {
|
||
writeError(w, http.StatusBadRequest, "OIDC access token is too large for browser session", errorCodeOIDCSessionTooLarge)
|
||
return
|
||
}
|
||
user, err := s.auth.AuthenticateOIDCAccessToken(r.Context(), raw)
|
||
if err != nil || user == nil || !strings.EqualFold(strings.TrimSpace(user.Source), "oidc") {
|
||
writeError(w, http.StatusUnauthorized, "valid OIDC access token is required", errorCodeOIDCSessionInvalid)
|
||
return
|
||
}
|
||
now := time.Now()
|
||
if user.TokenExpiresAt.IsZero() || !user.TokenExpiresAt.After(now) {
|
||
writeError(w, http.StatusUnauthorized, "OIDC access token has expired", errorCodeOIDCSessionInvalid)
|
||
return
|
||
}
|
||
maxAge := int(time.Until(user.TokenExpiresAt).Seconds())
|
||
if maxAge < 1 {
|
||
maxAge = 1
|
||
}
|
||
http.SetCookie(w, &http.Cookie{
|
||
Name: auth.OIDCSessionCookieName,
|
||
Value: raw,
|
||
Path: "/",
|
||
Expires: user.TokenExpiresAt,
|
||
MaxAge: maxAge,
|
||
HttpOnly: true,
|
||
Secure: s.cfg.OIDCSessionCookieSecure,
|
||
SameSite: http.SameSiteStrictMode,
|
||
})
|
||
w.Header().Set("Cache-Control", "no-store")
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
|
||
// deleteOIDCBrowserSession godoc
|
||
// @Summary 注销 OIDC 浏览器会话
|
||
// @Tags auth
|
||
// @Success 204
|
||
// @Router /api/v1/auth/oidc/session [delete]
|
||
func (s *Server) deleteOIDCBrowserSession(w http.ResponseWriter, _ *http.Request) {
|
||
http.SetCookie(w, &http.Cookie{
|
||
Name: auth.OIDCSessionCookieName,
|
||
Value: "",
|
||
Path: "/",
|
||
Expires: time.Unix(1, 0),
|
||
MaxAge: -1,
|
||
HttpOnly: true,
|
||
Secure: s.cfg.OIDCSessionCookieSecure,
|
||
SameSite: http.SameSiteStrictMode,
|
||
})
|
||
w.Header().Set("Cache-Control", "no-store")
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
|
||
func (s *Server) protectOIDCSessionCookie(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if !s.cfg.OIDCEnabled || !s.cfg.OIDCBrowserSessionEnabled || isSafeHTTPMethod(r.Method) || hasExplicitCredential(r) {
|
||
next.ServeHTTP(w, r)
|
||
return
|
||
}
|
||
if _, err := r.Cookie(auth.OIDCSessionCookieName); err != nil {
|
||
next.ServeHTTP(w, r)
|
||
return
|
||
}
|
||
origin := strings.TrimSpace(r.Header.Get("Origin"))
|
||
if origin == "" || !originAllowed(origin, s.cfg.CORSAllowedOrigin) {
|
||
writeError(w, http.StatusForbidden, "browser session request origin was rejected", errorCodeOIDCSessionCSRF)
|
||
return
|
||
}
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
|
||
func bearerToken(value string) string {
|
||
fields := strings.Fields(value)
|
||
if len(fields) == 2 && strings.EqualFold(fields[0], "bearer") {
|
||
return fields[1]
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func isSafeHTTPMethod(method string) bool {
|
||
switch method {
|
||
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func hasExplicitCredential(r *http.Request) bool {
|
||
return strings.TrimSpace(r.Header.Get("Authorization")) != "" ||
|
||
strings.TrimSpace(r.Header.Get("x-comfy-api-key")) != "" ||
|
||
strings.TrimSpace(r.Header.Get("x-goog-api-key")) != "" ||
|
||
strings.TrimSpace(r.URL.Query().Get("key")) != ""
|
||
}
|