You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							670 lines
						
					
					
						
							19 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							670 lines
						
					
					
						
							19 KiB
						
					
					
				
								package oidc
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"crypto/ecdsa"
							 | 
						|
									"crypto/elliptic"
							 | 
						|
									"crypto/rsa"
							 | 
						|
									"encoding/base64"
							 | 
						|
									"encoding/json"
							 | 
						|
									"fmt"
							 | 
						|
									"math/big"
							 | 
						|
									"net/http"
							 | 
						|
									"strings"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/golang-jwt/jwt/v5"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/glog"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/iam/providers"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// OIDCProvider implements OpenID Connect authentication
							 | 
						|
								type OIDCProvider struct {
							 | 
						|
									name          string
							 | 
						|
									config        *OIDCConfig
							 | 
						|
									initialized   bool
							 | 
						|
									jwksCache     *JWKS
							 | 
						|
									httpClient    *http.Client
							 | 
						|
									jwksFetchedAt time.Time
							 | 
						|
									jwksTTL       time.Duration
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// OIDCConfig holds OIDC provider configuration
							 | 
						|
								type OIDCConfig struct {
							 | 
						|
									// Issuer is the OIDC issuer URL
							 | 
						|
									Issuer string `json:"issuer"`
							 | 
						|
								
							 | 
						|
									// ClientID is the OAuth2 client ID
							 | 
						|
									ClientID string `json:"clientId"`
							 | 
						|
								
							 | 
						|
									// ClientSecret is the OAuth2 client secret (optional for public clients)
							 | 
						|
									ClientSecret string `json:"clientSecret,omitempty"`
							 | 
						|
								
							 | 
						|
									// JWKSUri is the JSON Web Key Set URI
							 | 
						|
									JWKSUri string `json:"jwksUri,omitempty"`
							 | 
						|
								
							 | 
						|
									// UserInfoUri is the UserInfo endpoint URI
							 | 
						|
									UserInfoUri string `json:"userInfoUri,omitempty"`
							 | 
						|
								
							 | 
						|
									// Scopes are the OAuth2 scopes to request
							 | 
						|
									Scopes []string `json:"scopes,omitempty"`
							 | 
						|
								
							 | 
						|
									// RoleMapping defines how to map OIDC claims to roles
							 | 
						|
									RoleMapping *providers.RoleMapping `json:"roleMapping,omitempty"`
							 | 
						|
								
							 | 
						|
									// ClaimsMapping defines how to map OIDC claims to identity attributes
							 | 
						|
									ClaimsMapping map[string]string `json:"claimsMapping,omitempty"`
							 | 
						|
								
							 | 
						|
									// JWKSCacheTTLSeconds sets how long to cache JWKS before refresh (default 3600 seconds)
							 | 
						|
									JWKSCacheTTLSeconds int `json:"jwksCacheTTLSeconds,omitempty"`
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// JWKS represents JSON Web Key Set
							 | 
						|
								type JWKS struct {
							 | 
						|
									Keys []JWK `json:"keys"`
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// JWK represents a JSON Web Key
							 | 
						|
								type JWK struct {
							 | 
						|
									Kty string `json:"kty"` // Key Type (RSA, EC, etc.)
							 | 
						|
									Kid string `json:"kid"` // Key ID
							 | 
						|
									Use string `json:"use"` // Usage (sig for signature)
							 | 
						|
									Alg string `json:"alg"` // Algorithm (RS256, etc.)
							 | 
						|
									N   string `json:"n"`   // RSA public key modulus
							 | 
						|
									E   string `json:"e"`   // RSA public key exponent
							 | 
						|
									X   string `json:"x"`   // EC public key x coordinate
							 | 
						|
									Y   string `json:"y"`   // EC public key y coordinate
							 | 
						|
									Crv string `json:"crv"` // EC curve
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// NewOIDCProvider creates a new OIDC provider
							 | 
						|
								func NewOIDCProvider(name string) *OIDCProvider {
							 | 
						|
									return &OIDCProvider{
							 | 
						|
										name:       name,
							 | 
						|
										httpClient: &http.Client{Timeout: 30 * time.Second},
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Name returns the provider name
							 | 
						|
								func (p *OIDCProvider) Name() string {
							 | 
						|
									return p.name
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetIssuer returns the configured issuer URL for efficient provider lookup
							 | 
						|
								func (p *OIDCProvider) GetIssuer() string {
							 | 
						|
									if p.config == nil {
							 | 
						|
										return ""
							 | 
						|
									}
							 | 
						|
									return p.config.Issuer
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Initialize initializes the OIDC provider with configuration
							 | 
						|
								func (p *OIDCProvider) Initialize(config interface{}) error {
							 | 
						|
									if config == nil {
							 | 
						|
										return fmt.Errorf("config cannot be nil")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									oidcConfig, ok := config.(*OIDCConfig)
							 | 
						|
									if !ok {
							 | 
						|
										return fmt.Errorf("invalid config type for OIDC provider")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := p.validateConfig(oidcConfig); err != nil {
							 | 
						|
										return fmt.Errorf("invalid OIDC configuration: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									p.config = oidcConfig
							 | 
						|
									p.initialized = true
							 | 
						|
								
							 | 
						|
									// Configure JWKS cache TTL
							 | 
						|
									if oidcConfig.JWKSCacheTTLSeconds > 0 {
							 | 
						|
										p.jwksTTL = time.Duration(oidcConfig.JWKSCacheTTLSeconds) * time.Second
							 | 
						|
									} else {
							 | 
						|
										p.jwksTTL = time.Hour
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// For testing, we'll skip the actual OIDC client initialization
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// validateConfig validates the OIDC configuration
							 | 
						|
								func (p *OIDCProvider) validateConfig(config *OIDCConfig) error {
							 | 
						|
									if config.Issuer == "" {
							 | 
						|
										return fmt.Errorf("issuer is required")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if config.ClientID == "" {
							 | 
						|
										return fmt.Errorf("client ID is required")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Basic URL validation for issuer
							 | 
						|
									if config.Issuer != "" && config.Issuer != "https://accounts.google.com" && config.Issuer[0:4] != "http" {
							 | 
						|
										return fmt.Errorf("invalid issuer URL format")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Authenticate authenticates a user with an OIDC token
							 | 
						|
								func (p *OIDCProvider) Authenticate(ctx context.Context, token string) (*providers.ExternalIdentity, error) {
							 | 
						|
									if !p.initialized {
							 | 
						|
										return nil, fmt.Errorf("provider not initialized")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if token == "" {
							 | 
						|
										return nil, fmt.Errorf("token cannot be empty")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate token and get claims
							 | 
						|
									claims, err := p.ValidateToken(ctx, token)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Map claims to external identity
							 | 
						|
									email, _ := claims.GetClaimString("email")
							 | 
						|
									displayName, _ := claims.GetClaimString("name")
							 | 
						|
									groups, _ := claims.GetClaimStringSlice("groups")
							 | 
						|
								
							 | 
						|
									// Debug: Log available claims
							 | 
						|
									glog.V(3).Infof("Available claims: %+v", claims.Claims)
							 | 
						|
									if rolesFromClaims, exists := claims.GetClaimStringSlice("roles"); exists {
							 | 
						|
										glog.V(3).Infof("Roles claim found as string slice: %v", rolesFromClaims)
							 | 
						|
									} else if roleFromClaims, exists := claims.GetClaimString("roles"); exists {
							 | 
						|
										glog.V(3).Infof("Roles claim found as string: %s", roleFromClaims)
							 | 
						|
									} else {
							 | 
						|
										glog.V(3).Infof("No roles claim found in token")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Map claims to roles using configured role mapping
							 | 
						|
									roles := p.mapClaimsToRolesWithConfig(claims)
							 | 
						|
								
							 | 
						|
									// Create attributes map and add roles
							 | 
						|
									attributes := make(map[string]string)
							 | 
						|
									if len(roles) > 0 {
							 | 
						|
										// Store roles as a comma-separated string in attributes
							 | 
						|
										attributes["roles"] = strings.Join(roles, ",")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return &providers.ExternalIdentity{
							 | 
						|
										UserID:      claims.Subject,
							 | 
						|
										Email:       email,
							 | 
						|
										DisplayName: displayName,
							 | 
						|
										Groups:      groups,
							 | 
						|
										Attributes:  attributes,
							 | 
						|
										Provider:    p.name,
							 | 
						|
									}, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetUserInfo retrieves user information from the UserInfo endpoint
							 | 
						|
								func (p *OIDCProvider) GetUserInfo(ctx context.Context, userID string) (*providers.ExternalIdentity, error) {
							 | 
						|
									if !p.initialized {
							 | 
						|
										return nil, fmt.Errorf("provider not initialized")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if userID == "" {
							 | 
						|
										return nil, fmt.Errorf("user ID cannot be empty")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// For now, we'll use a token-based approach since OIDC UserInfo typically requires a token
							 | 
						|
									// In a real implementation, this would need an access token from the authentication flow
							 | 
						|
									return p.getUserInfoWithToken(ctx, userID, "")
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetUserInfoWithToken retrieves user information using an access token
							 | 
						|
								func (p *OIDCProvider) GetUserInfoWithToken(ctx context.Context, accessToken string) (*providers.ExternalIdentity, error) {
							 | 
						|
									if !p.initialized {
							 | 
						|
										return nil, fmt.Errorf("provider not initialized")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if accessToken == "" {
							 | 
						|
										return nil, fmt.Errorf("access token cannot be empty")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return p.getUserInfoWithToken(ctx, "", accessToken)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// getUserInfoWithToken is the internal implementation for UserInfo endpoint calls
							 | 
						|
								func (p *OIDCProvider) getUserInfoWithToken(ctx context.Context, userID, accessToken string) (*providers.ExternalIdentity, error) {
							 | 
						|
									// Determine UserInfo endpoint URL
							 | 
						|
									userInfoUri := p.config.UserInfoUri
							 | 
						|
									if userInfoUri == "" {
							 | 
						|
										// Use standard OIDC discovery endpoint convention
							 | 
						|
										userInfoUri = strings.TrimSuffix(p.config.Issuer, "/") + "/userinfo"
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create HTTP request
							 | 
						|
									req, err := http.NewRequestWithContext(ctx, "GET", userInfoUri, nil)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to create UserInfo request: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Set authorization header if access token is provided
							 | 
						|
									if accessToken != "" {
							 | 
						|
										req.Header.Set("Authorization", "Bearer "+accessToken)
							 | 
						|
									}
							 | 
						|
									req.Header.Set("Accept", "application/json")
							 | 
						|
								
							 | 
						|
									// Make HTTP request
							 | 
						|
									resp, err := p.httpClient.Do(req)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to call UserInfo endpoint: %v", err)
							 | 
						|
									}
							 | 
						|
									defer resp.Body.Close()
							 | 
						|
								
							 | 
						|
									// Check response status
							 | 
						|
									if resp.StatusCode != http.StatusOK {
							 | 
						|
										return nil, fmt.Errorf("UserInfo endpoint returned status %d", resp.StatusCode)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Parse JSON response
							 | 
						|
									var userInfo map[string]interface{}
							 | 
						|
									if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to decode UserInfo response: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(4).Infof("Received UserInfo response: %+v", userInfo)
							 | 
						|
								
							 | 
						|
									// Map UserInfo claims to ExternalIdentity
							 | 
						|
									identity := p.mapUserInfoToIdentity(userInfo)
							 | 
						|
								
							 | 
						|
									// If userID was provided but not found in claims, use it
							 | 
						|
									if userID != "" && identity.UserID == "" {
							 | 
						|
										identity.UserID = userID
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(3).Infof("Retrieved user info from OIDC provider: %s", identity.UserID)
							 | 
						|
									return identity, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ValidateToken validates an OIDC JWT token
							 | 
						|
								func (p *OIDCProvider) ValidateToken(ctx context.Context, token string) (*providers.TokenClaims, error) {
							 | 
						|
									if !p.initialized {
							 | 
						|
										return nil, fmt.Errorf("provider not initialized")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if token == "" {
							 | 
						|
										return nil, fmt.Errorf("token cannot be empty")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Parse token without verification first to get header info
							 | 
						|
									parsedToken, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to parse JWT token: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Get key ID from header
							 | 
						|
									kid, ok := parsedToken.Header["kid"].(string)
							 | 
						|
									if !ok {
							 | 
						|
										return nil, fmt.Errorf("missing key ID in JWT header")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Get signing key from JWKS
							 | 
						|
									publicKey, err := p.getPublicKey(ctx, kid)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to get public key: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Parse and validate token with proper signature verification
							 | 
						|
									claims := jwt.MapClaims{}
							 | 
						|
									validatedToken, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) {
							 | 
						|
										// Verify signing method
							 | 
						|
										switch token.Method.(type) {
							 | 
						|
										case *jwt.SigningMethodRSA:
							 | 
						|
											return publicKey, nil
							 | 
						|
										default:
							 | 
						|
											return nil, fmt.Errorf("unsupported signing method: %v", token.Header["alg"])
							 | 
						|
										}
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to validate JWT token: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if !validatedToken.Valid {
							 | 
						|
										return nil, fmt.Errorf("JWT token is invalid")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate required claims
							 | 
						|
									issuer, ok := claims["iss"].(string)
							 | 
						|
									if !ok || issuer != p.config.Issuer {
							 | 
						|
										return nil, fmt.Errorf("invalid or missing issuer claim")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check audience claim (aud) or authorized party (azp) - Keycloak uses azp
							 | 
						|
									// Per RFC 7519, aud can be either a string or an array of strings
							 | 
						|
									var audienceMatched bool
							 | 
						|
									if audClaim, ok := claims["aud"]; ok {
							 | 
						|
										switch aud := audClaim.(type) {
							 | 
						|
										case string:
							 | 
						|
											if aud == p.config.ClientID {
							 | 
						|
												audienceMatched = true
							 | 
						|
											}
							 | 
						|
										case []interface{}:
							 | 
						|
											for _, a := range aud {
							 | 
						|
												if str, ok := a.(string); ok && str == p.config.ClientID {
							 | 
						|
													audienceMatched = true
							 | 
						|
													break
							 | 
						|
												}
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if !audienceMatched {
							 | 
						|
										if azp, ok := claims["azp"].(string); ok && azp == p.config.ClientID {
							 | 
						|
											audienceMatched = true
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if !audienceMatched {
							 | 
						|
										return nil, fmt.Errorf("invalid or missing audience claim for client ID %s", p.config.ClientID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									subject, ok := claims["sub"].(string)
							 | 
						|
									if !ok {
							 | 
						|
										return nil, fmt.Errorf("missing subject claim")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Convert to our TokenClaims structure
							 | 
						|
									tokenClaims := &providers.TokenClaims{
							 | 
						|
										Subject: subject,
							 | 
						|
										Issuer:  issuer,
							 | 
						|
										Claims:  make(map[string]interface{}),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Copy all claims
							 | 
						|
									for key, value := range claims {
							 | 
						|
										tokenClaims.Claims[key] = value
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return tokenClaims, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// mapClaimsToRoles maps token claims to SeaweedFS roles (legacy method)
							 | 
						|
								func (p *OIDCProvider) mapClaimsToRoles(claims *providers.TokenClaims) []string {
							 | 
						|
									roles := []string{}
							 | 
						|
								
							 | 
						|
									// Get groups from claims
							 | 
						|
									groups, _ := claims.GetClaimStringSlice("groups")
							 | 
						|
								
							 | 
						|
									// Basic role mapping based on groups
							 | 
						|
									for _, group := range groups {
							 | 
						|
										switch group {
							 | 
						|
										case "admins":
							 | 
						|
											roles = append(roles, "admin")
							 | 
						|
										case "developers":
							 | 
						|
											roles = append(roles, "readwrite")
							 | 
						|
										case "users":
							 | 
						|
											roles = append(roles, "readonly")
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if len(roles) == 0 {
							 | 
						|
										roles = []string{"readonly"} // Default role
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return roles
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// mapClaimsToRolesWithConfig maps token claims to roles using configured role mapping
							 | 
						|
								func (p *OIDCProvider) mapClaimsToRolesWithConfig(claims *providers.TokenClaims) []string {
							 | 
						|
									glog.V(3).Infof("mapClaimsToRolesWithConfig: RoleMapping is nil? %t", p.config.RoleMapping == nil)
							 | 
						|
								
							 | 
						|
									if p.config.RoleMapping == nil {
							 | 
						|
										glog.V(2).Infof("No role mapping configured for provider %s, using legacy mapping", p.name)
							 | 
						|
										// Fallback to legacy mapping if no role mapping configured
							 | 
						|
										return p.mapClaimsToRoles(claims)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(3).Infof("Applying %d role mapping rules", len(p.config.RoleMapping.Rules))
							 | 
						|
									roles := []string{}
							 | 
						|
								
							 | 
						|
									// Apply role mapping rules
							 | 
						|
									for i, rule := range p.config.RoleMapping.Rules {
							 | 
						|
										glog.V(3).Infof("Rule %d: claim=%s, value=%s, role=%s", i, rule.Claim, rule.Value, rule.Role)
							 | 
						|
								
							 | 
						|
										if rule.Matches(claims) {
							 | 
						|
											glog.V(2).Infof("Rule %d matched! Adding role: %s", i, rule.Role)
							 | 
						|
											roles = append(roles, rule.Role)
							 | 
						|
										} else {
							 | 
						|
											glog.V(3).Infof("Rule %d did not match", i)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Use default role if no rules matched
							 | 
						|
									if len(roles) == 0 && p.config.RoleMapping.DefaultRole != "" {
							 | 
						|
										glog.V(2).Infof("No rules matched, using default role: %s", p.config.RoleMapping.DefaultRole)
							 | 
						|
										roles = []string{p.config.RoleMapping.DefaultRole}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(2).Infof("Role mapping result: %v", roles)
							 | 
						|
									return roles
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// getPublicKey retrieves the public key for the given key ID from JWKS
							 | 
						|
								func (p *OIDCProvider) getPublicKey(ctx context.Context, kid string) (interface{}, error) {
							 | 
						|
									// Fetch JWKS if not cached or refresh if expired
							 | 
						|
									if p.jwksCache == nil || (!p.jwksFetchedAt.IsZero() && time.Since(p.jwksFetchedAt) > p.jwksTTL) {
							 | 
						|
										if err := p.fetchJWKS(ctx); err != nil {
							 | 
						|
											return nil, fmt.Errorf("failed to fetch JWKS: %v", err)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Find the key with matching kid
							 | 
						|
									for _, key := range p.jwksCache.Keys {
							 | 
						|
										if key.Kid == kid {
							 | 
						|
											return p.parseJWK(&key)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Key not found in cache. Refresh JWKS once to handle key rotation and retry.
							 | 
						|
									if err := p.fetchJWKS(ctx); err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to refresh JWKS after key miss: %v", err)
							 | 
						|
									}
							 | 
						|
									for _, key := range p.jwksCache.Keys {
							 | 
						|
										if key.Kid == kid {
							 | 
						|
											return p.parseJWK(&key)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
									return nil, fmt.Errorf("key with ID %s not found in JWKS after refresh", kid)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// fetchJWKS fetches the JWKS from the provider
							 | 
						|
								func (p *OIDCProvider) fetchJWKS(ctx context.Context) error {
							 | 
						|
									jwksURL := p.config.JWKSUri
							 | 
						|
									if jwksURL == "" {
							 | 
						|
										jwksURL = strings.TrimSuffix(p.config.Issuer, "/") + "/.well-known/jwks.json"
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									req, err := http.NewRequestWithContext(ctx, "GET", jwksURL, nil)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to create JWKS request: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									resp, err := p.httpClient.Do(req)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to fetch JWKS: %v", err)
							 | 
						|
									}
							 | 
						|
									defer resp.Body.Close()
							 | 
						|
								
							 | 
						|
									if resp.StatusCode != http.StatusOK {
							 | 
						|
										return fmt.Errorf("JWKS endpoint returned status: %d", resp.StatusCode)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var jwks JWKS
							 | 
						|
									if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil {
							 | 
						|
										return fmt.Errorf("failed to decode JWKS response: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									p.jwksCache = &jwks
							 | 
						|
									p.jwksFetchedAt = time.Now()
							 | 
						|
									glog.V(3).Infof("Fetched JWKS with %d keys from %s", len(jwks.Keys), jwksURL)
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// parseJWK converts a JWK to a public key
							 | 
						|
								func (p *OIDCProvider) parseJWK(key *JWK) (interface{}, error) {
							 | 
						|
									switch key.Kty {
							 | 
						|
									case "RSA":
							 | 
						|
										return p.parseRSAKey(key)
							 | 
						|
									case "EC":
							 | 
						|
										return p.parseECKey(key)
							 | 
						|
									default:
							 | 
						|
										return nil, fmt.Errorf("unsupported key type: %s", key.Kty)
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// parseRSAKey parses an RSA key from JWK
							 | 
						|
								func (p *OIDCProvider) parseRSAKey(key *JWK) (*rsa.PublicKey, error) {
							 | 
						|
									// Decode the modulus (n)
							 | 
						|
									nBytes, err := base64.RawURLEncoding.DecodeString(key.N)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to decode RSA modulus: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Decode the exponent (e)
							 | 
						|
									eBytes, err := base64.RawURLEncoding.DecodeString(key.E)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to decode RSA exponent: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Convert exponent bytes to int
							 | 
						|
									var exponent int
							 | 
						|
									for _, b := range eBytes {
							 | 
						|
										exponent = exponent*256 + int(b)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create RSA public key
							 | 
						|
									pubKey := &rsa.PublicKey{
							 | 
						|
										E: exponent,
							 | 
						|
									}
							 | 
						|
									pubKey.N = new(big.Int).SetBytes(nBytes)
							 | 
						|
								
							 | 
						|
									return pubKey, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// parseECKey parses an Elliptic Curve key from JWK
							 | 
						|
								func (p *OIDCProvider) parseECKey(key *JWK) (*ecdsa.PublicKey, error) {
							 | 
						|
									// Validate required fields
							 | 
						|
									if key.X == "" || key.Y == "" || key.Crv == "" {
							 | 
						|
										return nil, fmt.Errorf("incomplete EC key: missing x, y, or crv parameter")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Get the curve
							 | 
						|
									var curve elliptic.Curve
							 | 
						|
									switch key.Crv {
							 | 
						|
									case "P-256":
							 | 
						|
										curve = elliptic.P256()
							 | 
						|
									case "P-384":
							 | 
						|
										curve = elliptic.P384()
							 | 
						|
									case "P-521":
							 | 
						|
										curve = elliptic.P521()
							 | 
						|
									default:
							 | 
						|
										return nil, fmt.Errorf("unsupported EC curve: %s", key.Crv)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Decode x coordinate
							 | 
						|
									xBytes, err := base64.RawURLEncoding.DecodeString(key.X)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to decode EC x coordinate: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Decode y coordinate
							 | 
						|
									yBytes, err := base64.RawURLEncoding.DecodeString(key.Y)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to decode EC y coordinate: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create EC public key
							 | 
						|
									pubKey := &ecdsa.PublicKey{
							 | 
						|
										Curve: curve,
							 | 
						|
										X:     new(big.Int).SetBytes(xBytes),
							 | 
						|
										Y:     new(big.Int).SetBytes(yBytes),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate that the point is on the curve
							 | 
						|
									if !curve.IsOnCurve(pubKey.X, pubKey.Y) {
							 | 
						|
										return nil, fmt.Errorf("EC key coordinates are not on the specified curve")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return pubKey, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// mapUserInfoToIdentity maps UserInfo response to ExternalIdentity
							 | 
						|
								func (p *OIDCProvider) mapUserInfoToIdentity(userInfo map[string]interface{}) *providers.ExternalIdentity {
							 | 
						|
									identity := &providers.ExternalIdentity{
							 | 
						|
										Provider:   p.name,
							 | 
						|
										Attributes: make(map[string]string),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Map standard OIDC claims
							 | 
						|
									if sub, ok := userInfo["sub"].(string); ok {
							 | 
						|
										identity.UserID = sub
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if email, ok := userInfo["email"].(string); ok {
							 | 
						|
										identity.Email = email
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if name, ok := userInfo["name"].(string); ok {
							 | 
						|
										identity.DisplayName = name
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Handle groups claim (can be array of strings or single string)
							 | 
						|
									if groupsData, exists := userInfo["groups"]; exists {
							 | 
						|
										switch groups := groupsData.(type) {
							 | 
						|
										case []interface{}:
							 | 
						|
											// Array of groups
							 | 
						|
											for _, group := range groups {
							 | 
						|
												if groupStr, ok := group.(string); ok {
							 | 
						|
													identity.Groups = append(identity.Groups, groupStr)
							 | 
						|
												}
							 | 
						|
											}
							 | 
						|
										case []string:
							 | 
						|
											// Direct string array
							 | 
						|
											identity.Groups = groups
							 | 
						|
										case string:
							 | 
						|
											// Single group as string
							 | 
						|
											identity.Groups = []string{groups}
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Map configured custom claims
							 | 
						|
									if p.config.ClaimsMapping != nil {
							 | 
						|
										for identityField, oidcClaim := range p.config.ClaimsMapping {
							 | 
						|
											if value, exists := userInfo[oidcClaim]; exists {
							 | 
						|
												if strValue, ok := value.(string); ok {
							 | 
						|
													switch identityField {
							 | 
						|
													case "email":
							 | 
						|
														if identity.Email == "" {
							 | 
						|
															identity.Email = strValue
							 | 
						|
														}
							 | 
						|
													case "displayName":
							 | 
						|
														if identity.DisplayName == "" {
							 | 
						|
															identity.DisplayName = strValue
							 | 
						|
														}
							 | 
						|
													case "userID":
							 | 
						|
														if identity.UserID == "" {
							 | 
						|
															identity.UserID = strValue
							 | 
						|
														}
							 | 
						|
													default:
							 | 
						|
														identity.Attributes[identityField] = strValue
							 | 
						|
													}
							 | 
						|
												}
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Store all additional claims as attributes
							 | 
						|
									for key, value := range userInfo {
							 | 
						|
										if key != "sub" && key != "email" && key != "name" && key != "groups" {
							 | 
						|
											if strValue, ok := value.(string); ok {
							 | 
						|
												identity.Attributes[key] = strValue
							 | 
						|
											} else if jsonValue, err := json.Marshal(value); err == nil {
							 | 
						|
												identity.Attributes[key] = string(jsonValue)
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return identity
							 | 
						|
								}
							 |