feat: add stable OIDC authentication
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user