feat: add stable OIDC authentication

This commit is contained in:
2026-07-12 05:14:23 +08:00
parent 1e82253f43
commit b9c0e1a7a5
13 changed files with 861 additions and 23 deletions
+67 -10
View File
@@ -3,10 +3,16 @@ package auth
import (
"bytes"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"strconv"
"strings"
"time"
@@ -50,11 +56,15 @@ const userContextKey contextKey = "easyai-auth-user"
var ErrUnauthorized = errors.New("unauthorized")
type Authenticator struct {
JWTSecret string
ServerMainBaseURL string
ServerMainInternalToken string
HTTPClient *http.Client
LocalAPIKeyVerifier func(ctx context.Context, apiKey string) (*User, error)
JWTSecret string
ServerMainBaseURL string
ServerMainInternalToken string
ServerMainInternalKey string
ServerMainInternalSecret string
HTTPClient *http.Client
LocalAPIKeyVerifier func(ctx context.Context, apiKey string) (*User, error)
OIDCVerifier *OIDCVerifier
LegacyJWTEnabled bool
}
func New(jwtSecret string, serverMainBaseURL string, internalToken string) *Authenticator {
@@ -65,6 +75,7 @@ func New(jwtSecret string, serverMainBaseURL string, internalToken string) *Auth
HTTPClient: &http.Client{
Timeout: 10 * time.Second,
},
LegacyJWTEnabled: true,
}
}
@@ -77,6 +88,9 @@ 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
@@ -109,6 +123,16 @@ func (a *Authenticator) Authenticate(r *http.Request) (*User, error) {
if strings.HasPrefix(token, "sk-") {
return a.verifyAPIKey(r.Context(), token)
}
algorithm := jwtAlgorithm(token)
if algorithm == "RS256" || algorithm == "ES256" {
if a.OIDCVerifier == nil {
return nil, ErrUnauthorized
}
return a.OIDCVerifier.Verify(r.Context(), token)
}
if !a.LegacyJWTEnabled {
return nil, ErrUnauthorized
}
return a.verifyJWT(token)
}
@@ -196,16 +220,34 @@ func (a *Authenticator) verifyAPIKey(ctx context.Context, apiKey string) (*User,
return nil, ErrUnauthorized
}
}
if a.ServerMainBaseURL == "" || a.ServerMainInternalToken == "" {
secret := a.ServerMainInternalSecret
if secret == "" {
secret = a.ServerMainInternalToken
}
if a.ServerMainBaseURL == "" || a.ServerMainInternalKey == "" || secret == "" {
return nil, ErrUnauthorized
}
body, _ := json.Marshal(map[string]string{"apiKey": apiKey})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.ServerMainBaseURL+"/internal/platform/auth/verify-api-key", bytes.NewReader(body))
const path = "/internal/auth/verify-apikey"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.ServerMainBaseURL+path, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+a.ServerMainInternalToken)
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
nonceBytes := make([]byte, 16)
if _, err := rand.Read(nonceBytes); err != nil {
return nil, err
}
nonce := hex.EncodeToString(nonceBytes)
bodyHash := sha256.Sum256(body)
signingText := strings.Join([]string{http.MethodPost, path, timestamp, nonce, hex.EncodeToString(bodyHash[:]), a.ServerMainInternalKey}, "\n")
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(signingText))
req.Header.Set("X-Internal-Key", a.ServerMainInternalKey)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", hex.EncodeToString(mac.Sum(nil)))
resp, err := a.HTTPClient.Do(req)
if err != nil {
@@ -215,10 +257,16 @@ func (a *Authenticator) verifyAPIKey(ctx context.Context, apiKey string) (*User,
if resp.StatusCode != http.StatusOK {
return nil, ErrUnauthorized
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
var payload struct {
Success bool `json:"success"`
Data struct {
User User `json:"user"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
user := payload.Data.User
if user.ID == "" {
return nil, ErrUnauthorized
}
@@ -236,6 +284,15 @@ func extractBearer(value string) string {
return ""
}
func jwtAlgorithm(token string) string {
parsed, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
if err != nil || parsed == nil {
return ""
}
algorithm, _ := parsed.Header["alg"].(string)
return algorithm
}
func hasPermission(roles []string, required Permission) bool {
if required == PermissionPublic {
return true