- Introduce `YaoAuthSource` and `OAuthEmail` fields in various structures to capture authentication source and OAuth email during user login and registration processes. - Update `SignIDToken`, `GetInfo`, and `Map` functions to include new claims for Yao authentication source. - Modify login context to preserve authentication source and OAuth email across different user flows. - Enhance token issuance to include authentication source and OAuth email in claims for better tracking and user context. - Ensure proper handling of OAuth email for third-party logins without affecting user profile email. This change improves the user experience by providing clearer context on authentication methods used during login and registration processes.
976 lines
28 KiB
Go
976 lines
28 KiB
Go
package oauth
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v4"
|
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
|
"github.com/yaoapp/yao/share"
|
|
)
|
|
|
|
// SigningCertificates holds the loaded signing certificates and keys
|
|
type SigningCertificates struct {
|
|
// Primary signing certificate and private key
|
|
SigningCert *x509.Certificate `json:"-"`
|
|
SigningKey interface{} `json:"-"` // *rsa.PrivateKey, *ecdsa.PrivateKey, etc.
|
|
SigningKeyPair *tls.Certificate `json:"-"`
|
|
|
|
// Verification certificates for token validation
|
|
VerificationCerts []*x509.Certificate `json:"-"`
|
|
|
|
// mTLS CA certificate for client validation
|
|
MTLSClientCACert *x509.Certificate `json:"-"`
|
|
|
|
// Signing algorithm
|
|
Algorithm string `json:"algorithm"`
|
|
|
|
// Certificate paths for reference
|
|
SigningCertPath string `json:"signing_cert_path"`
|
|
SigningKeyPath string `json:"signing_key_path"`
|
|
|
|
// Auto-generated flag
|
|
IsAutoGenerated bool `json:"is_auto_generated"`
|
|
}
|
|
|
|
// LoadSigningCertificates loads or generates signing certificates based on configuration
|
|
func LoadSigningCertificates(config *types.SigningConfig) (*SigningCertificates, error) {
|
|
certs := &SigningCertificates{
|
|
Algorithm: config.SigningAlgorithm,
|
|
}
|
|
|
|
// Check if certificate files exist
|
|
signingCertExists := fileExists(config.SigningCertPath)
|
|
signingKeyExists := fileExists(config.SigningKeyPath)
|
|
|
|
// If both files exist, try to load them
|
|
if signingCertExists && signingKeyExists {
|
|
err := loadExistingCertificates(certs, config)
|
|
if err != nil {
|
|
// If loading fails, log warning and generate new certificates
|
|
fmt.Printf("Warning: Failed to load existing certificates (%v), generating new temporary certificates\n", err)
|
|
return generateTemporaryCertificates(config)
|
|
}
|
|
return certs, nil
|
|
}
|
|
|
|
// If certificates don't exist, generate temporary ones
|
|
return generateTemporaryCertificates(config)
|
|
}
|
|
|
|
// loadExistingCertificates loads certificates from the configured paths
|
|
func loadExistingCertificates(certs *SigningCertificates, config *types.SigningConfig) error {
|
|
// Load signing certificate
|
|
certPEM, err := os.ReadFile(config.SigningCertPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read signing certificate: %w", err)
|
|
}
|
|
|
|
certBlock, _ := pem.Decode(certPEM)
|
|
if certBlock == nil {
|
|
return fmt.Errorf("failed to decode signing certificate PEM")
|
|
}
|
|
|
|
signingCert, err := x509.ParseCertificate(certBlock.Bytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse signing certificate: %w", err)
|
|
}
|
|
|
|
// Load signing key
|
|
keyPEM, err := os.ReadFile(config.SigningKeyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read signing key: %w", err)
|
|
}
|
|
|
|
keyBlock, _ := pem.Decode(keyPEM)
|
|
if keyBlock == nil {
|
|
return fmt.Errorf("failed to decode signing key PEM")
|
|
}
|
|
|
|
var signingKey interface{}
|
|
if config.SigningKeyPassword != "" {
|
|
// Decrypt encrypted key
|
|
keyBytes, err := x509.DecryptPEMBlock(keyBlock, []byte(config.SigningKeyPassword))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt signing key: %w", err)
|
|
}
|
|
signingKey, err = parsePrivateKey(keyBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse decrypted signing key: %w", err)
|
|
}
|
|
} else {
|
|
// Parse unencrypted key
|
|
var err error
|
|
signingKey, err = parsePrivateKey(keyBlock.Bytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse signing key: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create TLS certificate pair
|
|
keyPair, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create key pair: %w", err)
|
|
}
|
|
|
|
certs.SigningCert = signingCert
|
|
certs.SigningKey = signingKey
|
|
certs.SigningKeyPair = &keyPair
|
|
certs.SigningCertPath = config.SigningCertPath
|
|
certs.SigningKeyPath = config.SigningKeyPath
|
|
certs.IsAutoGenerated = false
|
|
|
|
// Load verification certificates if configured
|
|
if len(config.VerificationCerts) > 0 {
|
|
verificationCerts, err := loadVerificationCertificates(config.VerificationCerts)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load verification certificates: %w", err)
|
|
}
|
|
certs.VerificationCerts = verificationCerts
|
|
}
|
|
|
|
// Load mTLS CA certificate if configured
|
|
if config.MTLSClientCACertPath != "" {
|
|
mtlsCACert, err := loadCertificateFromFile(config.MTLSClientCACertPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load mTLS CA certificate: %w", err)
|
|
}
|
|
certs.MTLSClientCACert = mtlsCACert
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateTemporaryCertificates generates temporary self-signed certificates
|
|
func generateTemporaryCertificates(config *types.SigningConfig) (*SigningCertificates, error) {
|
|
// Generate RSA private key
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate private key: %w", err)
|
|
}
|
|
|
|
// Create certificate template
|
|
template := x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{
|
|
Organization: []string{share.App.Name},
|
|
Country: []string{"US"},
|
|
Province: []string{""},
|
|
Locality: []string{""},
|
|
StreetAddress: []string{""},
|
|
PostalCode: []string{""},
|
|
CommonName: fmt.Sprintf("%s OAuth Signing Certificate", share.App.Name),
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(365 * 24 * time.Hour), // Valid for 1 year
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
// Generate certificate
|
|
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create certificate: %w", err)
|
|
}
|
|
|
|
// Parse the generated certificate
|
|
cert, err := x509.ParseCertificate(certDER)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse generated certificate: %w", err)
|
|
}
|
|
|
|
// Create PEM blocks
|
|
certPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: certDER,
|
|
})
|
|
|
|
keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal private key: %w", err)
|
|
}
|
|
|
|
keyPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "PRIVATE KEY",
|
|
Bytes: keyDER,
|
|
})
|
|
|
|
// Create TLS certificate pair
|
|
keyPair, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create key pair: %w", err)
|
|
}
|
|
|
|
// Determine system directory for storing temporary certificates
|
|
systemDir := getSystemCertificateDirectory()
|
|
|
|
// Create directory if it doesn't exist
|
|
if err := os.MkdirAll(systemDir, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create system certificate directory: %w", err)
|
|
}
|
|
|
|
// Generate unique filenames
|
|
timestamp := time.Now().Format("20060102150405")
|
|
certPath := filepath.Join(systemDir, fmt.Sprintf("oauth_signing_cert_%s.pem", timestamp))
|
|
keyPath := filepath.Join(systemDir, fmt.Sprintf("oauth_signing_key_%s.pem", timestamp))
|
|
|
|
// Save certificate and key to system directory
|
|
if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
|
|
return nil, fmt.Errorf("failed to write certificate file: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
|
|
return nil, fmt.Errorf("failed to write key file: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Generated temporary OAuth signing certificate at: %s\n", certPath)
|
|
fmt.Printf("Generated temporary OAuth signing key at: %s\n", keyPath)
|
|
|
|
return &SigningCertificates{
|
|
SigningCert: cert,
|
|
SigningKey: privateKey,
|
|
SigningKeyPair: &keyPair,
|
|
Algorithm: config.SigningAlgorithm,
|
|
SigningCertPath: certPath,
|
|
SigningKeyPath: keyPath,
|
|
IsAutoGenerated: true,
|
|
}, nil
|
|
}
|
|
|
|
// loadVerificationCertificates loads additional verification certificates
|
|
func loadVerificationCertificates(certPaths []string) ([]*x509.Certificate, error) {
|
|
var certs []*x509.Certificate
|
|
|
|
for _, certPath := range certPaths {
|
|
cert, err := loadCertificateFromFile(certPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load verification certificate %s: %w", certPath, err)
|
|
}
|
|
certs = append(certs, cert)
|
|
}
|
|
|
|
return certs, nil
|
|
}
|
|
|
|
// loadCertificateFromFile loads a certificate from a PEM file
|
|
func loadCertificateFromFile(certPath string) (*x509.Certificate, error) {
|
|
certPEM, err := os.ReadFile(certPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read certificate file: %w", err)
|
|
}
|
|
|
|
certBlock, _ := pem.Decode(certPEM)
|
|
if certBlock == nil {
|
|
return nil, fmt.Errorf("failed to decode certificate PEM")
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(certBlock.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse certificate: %w", err)
|
|
}
|
|
|
|
return cert, nil
|
|
}
|
|
|
|
// parsePrivateKey parses a private key from DER bytes
|
|
func parsePrivateKey(der []byte) (interface{}, error) {
|
|
// Try PKCS#8 first
|
|
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
|
return key, nil
|
|
}
|
|
|
|
// Try PKCS#1 RSA
|
|
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
|
return key, nil
|
|
}
|
|
|
|
// Try EC private key
|
|
if key, err := x509.ParseECPrivateKey(der); err == nil {
|
|
return key, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("unable to parse private key")
|
|
}
|
|
|
|
// fileExists checks if a file exists
|
|
func fileExists(filename string) bool {
|
|
_, err := os.Stat(filename)
|
|
return !os.IsNotExist(err)
|
|
}
|
|
|
|
// getSystemCertificateDirectory returns the appropriate system directory for storing certificates
|
|
func getSystemCertificateDirectory() string {
|
|
// Use different directories based on the operating system
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
// Fallback to temporary directory
|
|
return filepath.Join(os.TempDir(), "yao-oauth-certs")
|
|
}
|
|
|
|
// Create a hidden directory in user's home
|
|
return filepath.Join(homeDir, ".yao", "oauth", "certs")
|
|
}
|
|
|
|
// ValidateCertificate validates a certificate for OAuth signing
|
|
func (c *SigningCertificates) ValidateCertificate() error {
|
|
if c.SigningCert == nil {
|
|
return fmt.Errorf("signing certificate is nil")
|
|
}
|
|
|
|
// Check if certificate is expired
|
|
now := time.Now()
|
|
if now.Before(c.SigningCert.NotBefore) {
|
|
return fmt.Errorf("signing certificate is not yet valid")
|
|
}
|
|
|
|
if now.After(c.SigningCert.NotAfter) {
|
|
return fmt.Errorf("signing certificate has expired")
|
|
}
|
|
|
|
// Check if certificate has appropriate key usage
|
|
if c.SigningCert.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
|
|
return fmt.Errorf("signing certificate does not have digital signature key usage")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetPublicKey returns the public key from the signing certificate
|
|
func (c *SigningCertificates) GetPublicKey() interface{} {
|
|
if c.SigningCert == nil {
|
|
return nil
|
|
}
|
|
return c.SigningCert.PublicKey
|
|
}
|
|
|
|
// GetKeyID returns a key identifier for the signing certificate
|
|
func (c *SigningCertificates) GetKeyID() string {
|
|
if c.SigningCert == nil {
|
|
return ""
|
|
}
|
|
|
|
// Use the certificate's serial number as key ID
|
|
return c.SigningCert.SerialNumber.String()
|
|
}
|
|
|
|
// CleanupTemporaryCertificates removes auto-generated temporary certificates
|
|
func (c *SigningCertificates) CleanupTemporaryCertificates() error {
|
|
if !c.IsAutoGenerated {
|
|
return nil // Don't delete user-provided certificates
|
|
}
|
|
|
|
var errs []error
|
|
|
|
if c.SigningCertPath != "" && fileExists(c.SigningCertPath) {
|
|
if err := os.Remove(c.SigningCertPath); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to remove certificate file %s: %w", c.SigningCertPath, err))
|
|
}
|
|
}
|
|
|
|
if c.SigningKeyPath != "" && fileExists(c.SigningKeyPath) {
|
|
if err := os.Remove(c.SigningKeyPath); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to remove key file %s: %w", c.SigningKeyPath, err))
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("cleanup errors: %v", errs)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Service signing certificate methods
|
|
|
|
// GetSigningCertificates returns the signing certificates for the service
|
|
func (s *Service) GetSigningCertificates() *SigningCertificates {
|
|
return s.signingCerts
|
|
}
|
|
|
|
// GetSigningKey returns the signing private key
|
|
func (s *Service) GetSigningKey() interface{} {
|
|
if s.signingCerts == nil {
|
|
return nil
|
|
}
|
|
return s.signingCerts.SigningKey
|
|
}
|
|
|
|
// GetSigningCertificate returns the signing certificate
|
|
func (s *Service) GetSigningCertificate() interface{} {
|
|
if s.signingCerts == nil {
|
|
return nil
|
|
}
|
|
return s.signingCerts.SigningCert
|
|
}
|
|
|
|
// GetSigningAlgorithm returns the signing algorithm
|
|
func (s *Service) GetSigningAlgorithm() string {
|
|
if s.signingCerts == nil {
|
|
return "RS256" // default
|
|
}
|
|
return s.signingCerts.Algorithm
|
|
}
|
|
|
|
// GetKeyID returns the key identifier for JWT token signing
|
|
func (s *Service) GetKeyID() string {
|
|
if s.signingCerts == nil {
|
|
return ""
|
|
}
|
|
return s.signingCerts.GetKeyID()
|
|
}
|
|
|
|
// SignToken signs a token based on the configured format (jwt or opaque)
|
|
// extraClaims: optional extra claims to add to the token (e.g., team_id, tenant_id)
|
|
func (s *Service) SignToken(tokenType, clientID, scope, subject string, expiresIn int, extraClaims ...map[string]interface{}) (string, error) {
|
|
var claims map[string]interface{}
|
|
if len(extraClaims) > 0 {
|
|
claims = extraClaims[0]
|
|
}
|
|
|
|
switch s.config.Token.AccessTokenFormat {
|
|
case "jwt":
|
|
return s.signJWTToken(tokenType, clientID, scope, subject, expiresIn, claims)
|
|
case "opaque":
|
|
return s.signOpaqueToken(tokenType, clientID, scope, subject)
|
|
default:
|
|
// Default to JWT if format is not specified or unknown
|
|
return s.signJWTToken(tokenType, clientID, scope, subject, expiresIn, claims)
|
|
}
|
|
}
|
|
|
|
// VerifyToken verifies a token based on its format and returns token claims
|
|
func (s *Service) VerifyToken(token string) (*types.TokenClaims, error) {
|
|
// First try to verify as JWT (JWT tokens contain dots)
|
|
if strings.Contains(token, ".") {
|
|
return s.verifyJWTToken(token)
|
|
}
|
|
|
|
// Otherwise, verify as opaque token
|
|
return s.verifyOpaqueToken(token)
|
|
}
|
|
|
|
// SignIDToken signs an ID token with specific parameters and stores it
|
|
func (s *Service) SignIDToken(clientID, scope string, expiresIn int, userdata *types.OIDCUserInfo, extraClaims ...map[string]interface{}) (string, error) {
|
|
if s.signingCerts == nil || s.signingCerts.SigningKey == nil {
|
|
return "", fmt.Errorf("signing certificates not initialized")
|
|
}
|
|
|
|
// Check if userdata and subject are provided
|
|
if userdata == nil || userdata.Sub == "" {
|
|
return "", fmt.Errorf("userdata or userdata.Sub is required for ID token")
|
|
}
|
|
|
|
tokenSubject := userdata.Sub
|
|
|
|
now := time.Now()
|
|
|
|
// Create OIDC ID Token claims
|
|
idTokenClaims := &types.OIDCIDToken{
|
|
// Required ID Token claims
|
|
Iss: s.config.IssuerURL,
|
|
Sub: tokenSubject,
|
|
Aud: clientID,
|
|
Exp: now.Add(time.Duration(expiresIn) * time.Second).Unix(),
|
|
Iat: now.Unix(),
|
|
}
|
|
|
|
// Create a map for custom claims that includes both standard and user info
|
|
claims := jwt.MapClaims{
|
|
// Standard OIDC ID Token claims
|
|
"iss": idTokenClaims.Iss,
|
|
"sub": idTokenClaims.Sub,
|
|
"aud": idTokenClaims.Aud,
|
|
"exp": idTokenClaims.Exp,
|
|
"iat": idTokenClaims.Iat,
|
|
"jti": generateJTI(),
|
|
}
|
|
|
|
// Add extra claims if provided (e.g., team_id, tenant_id)
|
|
if len(extraClaims) > 0 {
|
|
for key, value := range extraClaims[0] {
|
|
claims[key] = value
|
|
}
|
|
}
|
|
|
|
// Add user information from userdata
|
|
// Add standard OIDC user claims if they exist
|
|
if userdata.Name != "" {
|
|
claims["name"] = userdata.Name
|
|
}
|
|
if userdata.GivenName != "" {
|
|
claims["given_name"] = userdata.GivenName
|
|
}
|
|
if userdata.FamilyName != "" {
|
|
claims["family_name"] = userdata.FamilyName
|
|
}
|
|
if userdata.MiddleName != "" {
|
|
claims["middle_name"] = userdata.MiddleName
|
|
}
|
|
if userdata.Nickname != "" {
|
|
claims["nickname"] = userdata.Nickname
|
|
}
|
|
if userdata.PreferredUsername != "" {
|
|
claims["preferred_username"] = userdata.PreferredUsername
|
|
}
|
|
if userdata.Profile != "" {
|
|
claims["profile"] = userdata.Profile
|
|
}
|
|
if userdata.Picture != "" {
|
|
claims["picture"] = userdata.Picture
|
|
}
|
|
if userdata.Website != "" {
|
|
claims["website"] = userdata.Website
|
|
}
|
|
if userdata.Email != "" {
|
|
claims["email"] = userdata.Email
|
|
}
|
|
if userdata.EmailVerified != nil {
|
|
claims["email_verified"] = *userdata.EmailVerified
|
|
}
|
|
if userdata.Gender != "" {
|
|
claims["gender"] = userdata.Gender
|
|
}
|
|
if userdata.Birthdate != "" {
|
|
claims["birthdate"] = userdata.Birthdate
|
|
}
|
|
if userdata.Zoneinfo != "" {
|
|
claims["zoneinfo"] = userdata.Zoneinfo
|
|
}
|
|
if userdata.Locale != "" {
|
|
claims["locale"] = userdata.Locale
|
|
}
|
|
if userdata.PhoneNumber != "" {
|
|
claims["phone_number"] = userdata.PhoneNumber
|
|
}
|
|
if userdata.PhoneNumberVerified != nil {
|
|
claims["phone_number_verified"] = *userdata.PhoneNumberVerified
|
|
}
|
|
if userdata.Address != nil {
|
|
claims["address"] = userdata.Address
|
|
}
|
|
if userdata.UpdatedAt != nil {
|
|
claims["updated_at"] = *userdata.UpdatedAt
|
|
}
|
|
|
|
// Add Yao custom fields with namespace
|
|
if userdata.YaoUserID != "" {
|
|
claims["yao:user_id"] = userdata.YaoUserID
|
|
}
|
|
if userdata.YaoTenantID != "" {
|
|
claims["yao:tenant_id"] = userdata.YaoTenantID
|
|
}
|
|
if userdata.YaoTeamID != "" {
|
|
claims["yao:team_id"] = userdata.YaoTeamID
|
|
}
|
|
if userdata.YaoIsOwner != nil {
|
|
claims["yao:is_owner"] = *userdata.YaoIsOwner
|
|
}
|
|
if userdata.YaoTypeID != "" {
|
|
claims["yao:type_id"] = userdata.YaoTypeID
|
|
}
|
|
if userdata.YaoAuthSource != "" {
|
|
claims["yao:auth_source"] = userdata.YaoAuthSource
|
|
}
|
|
// Add Yao team info if present
|
|
if userdata.YaoTeam != nil {
|
|
teamMap := make(map[string]interface{})
|
|
if userdata.YaoTeam.TeamID != "" {
|
|
teamMap["team_id"] = userdata.YaoTeam.TeamID
|
|
}
|
|
if userdata.YaoTeam.Logo != "" {
|
|
teamMap["logo"] = userdata.YaoTeam.Logo
|
|
}
|
|
if userdata.YaoTeam.Name != "" {
|
|
teamMap["name"] = userdata.YaoTeam.Name
|
|
}
|
|
if userdata.YaoTeam.OwnerID != "" {
|
|
teamMap["owner_id"] = userdata.YaoTeam.OwnerID
|
|
}
|
|
if userdata.YaoTeam.Description != "" {
|
|
teamMap["description"] = userdata.YaoTeam.Description
|
|
}
|
|
if userdata.YaoTeam.UpdatedAt != nil {
|
|
teamMap["updated_at"] = *userdata.YaoTeam.UpdatedAt
|
|
}
|
|
if len(teamMap) > 0 {
|
|
claims["yao:team"] = teamMap
|
|
}
|
|
}
|
|
// Add Yao member info if present (for team context)
|
|
if userdata.YaoMember != nil {
|
|
memberMap := make(map[string]interface{})
|
|
if userdata.YaoMember.MemberID != "" {
|
|
memberMap["member_id"] = userdata.YaoMember.MemberID
|
|
}
|
|
if userdata.YaoMember.DisplayName != "" {
|
|
memberMap["display_name"] = userdata.YaoMember.DisplayName
|
|
}
|
|
if userdata.YaoMember.Bio != "" {
|
|
memberMap["bio"] = userdata.YaoMember.Bio
|
|
}
|
|
if userdata.YaoMember.Avatar != "" {
|
|
memberMap["avatar"] = userdata.YaoMember.Avatar
|
|
}
|
|
if userdata.YaoMember.Email != "" {
|
|
memberMap["email"] = userdata.YaoMember.Email
|
|
}
|
|
if len(memberMap) > 0 {
|
|
claims["yao:member"] = memberMap
|
|
}
|
|
}
|
|
// Add Yao type info if present
|
|
if userdata.YaoType != nil {
|
|
typeMap := make(map[string]interface{})
|
|
if userdata.YaoType.TypeID != "" {
|
|
typeMap["type_id"] = userdata.YaoType.TypeID
|
|
}
|
|
if userdata.YaoType.Name != "" {
|
|
typeMap["name"] = userdata.YaoType.Name
|
|
}
|
|
if userdata.YaoType.Locale != "" {
|
|
typeMap["locale"] = userdata.YaoType.Locale
|
|
}
|
|
if len(typeMap) > 0 {
|
|
claims["yao:type"] = typeMap
|
|
}
|
|
}
|
|
|
|
// Add scope if provided (useful for determining which claims to include)
|
|
if scope != "" {
|
|
claims["scope"] = scope
|
|
}
|
|
|
|
// Create token with claims
|
|
token := jwt.NewWithClaims(getSigningMethod(s.config.Token.AccessTokenSigningAlg), claims)
|
|
|
|
// Set key ID in header
|
|
token.Header["kid"] = s.GetKeyID()
|
|
|
|
// Set token type in header
|
|
token.Header["typ"] = "JWT"
|
|
|
|
// Sign token with private key
|
|
signedToken, err := token.SignedString(s.signingCerts.SigningKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to sign ID token: %w", err)
|
|
}
|
|
|
|
return signedToken, nil
|
|
}
|
|
|
|
// signJWTToken signs a JWT token using the configured signing algorithm
|
|
func (s *Service) signJWTToken(tokenType, clientID, scope, subject string, expiresIn int, extraClaims map[string]interface{}) (string, error) {
|
|
if s.signingCerts == nil || s.signingCerts.SigningKey == nil {
|
|
return "", fmt.Errorf("signing certificates not initialized")
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
// Use MapClaims to support extra claims
|
|
claims := jwt.MapClaims{
|
|
// Standard JWT claims
|
|
"iss": s.config.IssuerURL,
|
|
"sub": subject,
|
|
"aud": clientID,
|
|
"exp": now.Add(time.Duration(expiresIn) * time.Second).Unix(),
|
|
"nbf": now.Unix(),
|
|
"iat": now.Unix(),
|
|
"jti": generateJTI(),
|
|
// Custom OAuth claims
|
|
"client_id": clientID,
|
|
"scope": scope,
|
|
"token_type": tokenType,
|
|
}
|
|
|
|
// Add extra claims if provided (e.g., team_id, tenant_id)
|
|
for key, value := range extraClaims {
|
|
claims[key] = value
|
|
}
|
|
|
|
// Create token with claims
|
|
token := jwt.NewWithClaims(getSigningMethod(s.config.Token.AccessTokenSigningAlg), claims)
|
|
|
|
// Set key ID in header
|
|
token.Header["kid"] = s.GetKeyID()
|
|
|
|
// Sign token with private key
|
|
return token.SignedString(s.signingCerts.SigningKey)
|
|
}
|
|
|
|
// verifyJWTToken verifies a JWT token and returns its claims
|
|
func (s *Service) verifyJWTToken(tokenString string) (*types.TokenClaims, error) {
|
|
if s.signingCerts == nil || s.signingCerts.SigningCert == nil {
|
|
return nil, fmt.Errorf("signing certificates not initialized")
|
|
}
|
|
|
|
// Parse token with MapClaims to support extra claims
|
|
token, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
|
|
// Validate signing method
|
|
expectedMethod := getSigningMethod(s.config.Token.AccessTokenSigningAlg)
|
|
if token.Method != expectedMethod {
|
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
}
|
|
|
|
// Return public key for verification
|
|
return s.signingCerts.GetPublicKey(), nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse JWT token: %w", err)
|
|
}
|
|
|
|
if !token.Valid {
|
|
return nil, fmt.Errorf("invalid JWT token")
|
|
}
|
|
|
|
// Extract claims
|
|
mapClaims, ok := token.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid JWT claims type")
|
|
}
|
|
|
|
// Convert to TokenClaims
|
|
tokenClaims := &types.TokenClaims{
|
|
Extra: make(map[string]interface{}),
|
|
}
|
|
|
|
// Extract standard claims
|
|
if sub, ok := mapClaims["sub"].(string); ok {
|
|
tokenClaims.Subject = sub
|
|
}
|
|
if clientID, ok := mapClaims["client_id"].(string); ok {
|
|
tokenClaims.ClientID = clientID
|
|
}
|
|
if scope, ok := mapClaims["scope"].(string); ok {
|
|
tokenClaims.Scope = scope
|
|
}
|
|
if tokenType, ok := mapClaims["token_type"].(string); ok {
|
|
tokenClaims.TokenType = tokenType
|
|
}
|
|
if iss, ok := mapClaims["iss"].(string); ok {
|
|
tokenClaims.Issuer = iss
|
|
}
|
|
if jti, ok := mapClaims["jti"].(string); ok {
|
|
tokenClaims.JTI = jti
|
|
}
|
|
|
|
// Extract time claims
|
|
if exp, ok := mapClaims["exp"].(float64); ok {
|
|
tokenClaims.ExpiresAt = time.Unix(int64(exp), 0)
|
|
}
|
|
if iat, ok := mapClaims["iat"].(float64); ok {
|
|
tokenClaims.IssuedAt = time.Unix(int64(iat), 0)
|
|
}
|
|
|
|
// Extract audience
|
|
if aud, ok := mapClaims["aud"].(string); ok {
|
|
tokenClaims.Audience = []string{aud}
|
|
} else if audArray, ok := mapClaims["aud"].([]interface{}); ok {
|
|
audience := make([]string, 0, len(audArray))
|
|
for _, a := range audArray {
|
|
if audStr, ok := a.(string); ok {
|
|
audience = append(audience, audStr)
|
|
}
|
|
}
|
|
tokenClaims.Audience = audience
|
|
}
|
|
|
|
// Extract extended claims for multi-tenancy and team support
|
|
if teamID, ok := mapClaims["team_id"].(string); ok {
|
|
tokenClaims.TeamID = teamID
|
|
}
|
|
if tenantID, ok := mapClaims["tenant_id"].(string); ok {
|
|
tokenClaims.TenantID = tenantID
|
|
}
|
|
|
|
// Store all extra claims for flexibility
|
|
standardClaims := map[string]bool{
|
|
"sub": true, "client_id": true, "scope": true, "token_type": true,
|
|
"exp": true, "iat": true, "nbf": true, "iss": true, "aud": true, "jti": true,
|
|
"team_id": true, "tenant_id": true,
|
|
}
|
|
for key, value := range mapClaims {
|
|
if !standardClaims[key] {
|
|
tokenClaims.Extra[key] = value
|
|
}
|
|
}
|
|
|
|
return tokenClaims, nil
|
|
}
|
|
|
|
// signOpaqueToken signs an opaque token using HMAC or RSA signature
|
|
func (s *Service) signOpaqueToken(tokenType, clientID, scope, subject string) (string, error) {
|
|
// Generate base opaque token
|
|
baseToken, err := s.generateOpaqueTokenBase(tokenType, clientID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate base opaque token: %w", err)
|
|
}
|
|
|
|
// Create token metadata for signature
|
|
tokenData := fmt.Sprintf("%s.%s.%s.%s.%d", baseToken, clientID, scope, subject, time.Now().Unix())
|
|
|
|
// Sign the token data
|
|
signature, err := s.signData([]byte(tokenData))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to sign opaque token: %w", err)
|
|
}
|
|
|
|
// Combine base token with signature
|
|
signedToken := fmt.Sprintf("%s.%s", baseToken, base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(signature))
|
|
|
|
return signedToken, nil
|
|
}
|
|
|
|
// verifyOpaqueToken verifies an opaque token signature and returns token claims
|
|
func (s *Service) verifyOpaqueToken(token string) (*types.TokenClaims, error) {
|
|
parts := strings.Split(token, ".")
|
|
if len(parts) < 2 {
|
|
return nil, fmt.Errorf("invalid opaque token format")
|
|
}
|
|
|
|
baseToken := parts[0]
|
|
signaturePart := parts[len(parts)-1]
|
|
|
|
// Decode signature
|
|
signature, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(signaturePart)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode token signature: %w", err)
|
|
}
|
|
|
|
// Extract token information from store
|
|
tokenInfo, err := s.getAccessTokenData(token)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("token not found or invalid: %w", err)
|
|
}
|
|
|
|
// Reconstruct token data for verification
|
|
clientID := tokenInfo["client_id"].(string)
|
|
scope := ""
|
|
if scopeVal, ok := tokenInfo["scope"].(string); ok {
|
|
scope = scopeVal
|
|
}
|
|
subject := ""
|
|
if subjectVal, ok := tokenInfo["subject"].(string); ok {
|
|
subject = subjectVal
|
|
}
|
|
issuedAt := tokenInfo["issued_at"].(int64)
|
|
|
|
tokenData := fmt.Sprintf("%s.%s.%s.%s.%d", baseToken, clientID, scope, subject, issuedAt)
|
|
|
|
// Verify signature
|
|
if err := s.verifySignature([]byte(tokenData), signature); err != nil {
|
|
return nil, fmt.Errorf("invalid token signature: %w", err)
|
|
}
|
|
|
|
// Build token claims
|
|
tokenClaims := &types.TokenClaims{
|
|
Subject: subject,
|
|
ClientID: clientID,
|
|
Scope: scope,
|
|
TokenType: "access_token",
|
|
IssuedAt: time.Unix(issuedAt, 0),
|
|
Issuer: s.config.IssuerURL,
|
|
}
|
|
|
|
if expiresAt, ok := tokenInfo["expires_at"].(int64); ok {
|
|
tokenClaims.ExpiresAt = time.Unix(expiresAt, 0)
|
|
}
|
|
|
|
return tokenClaims, nil
|
|
}
|
|
|
|
// signData signs data using the configured signing key
|
|
func (s *Service) signData(data []byte) ([]byte, error) {
|
|
if s.signingCerts == nil || s.signingCerts.SigningKey == nil {
|
|
return nil, fmt.Errorf("signing key not available")
|
|
}
|
|
|
|
switch key := s.signingCerts.SigningKey.(type) {
|
|
case *rsa.PrivateKey:
|
|
// Use RSA-PSS for signing
|
|
hash := sha256.Sum256(data)
|
|
signature, err := rsa.SignPSS(rand.Reader, key, crypto.SHA256, hash[:], nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to sign with RSA key: %w", err)
|
|
}
|
|
return signature, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported signing key type: %T", key)
|
|
}
|
|
}
|
|
|
|
// verifySignature verifies a signature using the configured public key
|
|
func (s *Service) verifySignature(data []byte, signature []byte) error {
|
|
if s.signingCerts == nil || s.signingCerts.SigningCert == nil {
|
|
return fmt.Errorf("signing certificate not available")
|
|
}
|
|
|
|
switch pubKey := s.signingCerts.GetPublicKey().(type) {
|
|
case *rsa.PublicKey:
|
|
// Use RSA-PSS for verification
|
|
hash := sha256.Sum256(data)
|
|
err := rsa.VerifyPSS(pubKey, crypto.SHA256, hash[:], signature, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to verify RSA signature: %w", err)
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unsupported public key type: %T", pubKey)
|
|
}
|
|
}
|
|
|
|
// generateOpaqueTokenBase generates the base part of an opaque token
|
|
func (s *Service) generateOpaqueTokenBase(tokenType, clientID string) (string, error) {
|
|
// Generate random bytes for token
|
|
randomBytes := make([]byte, 32)
|
|
if _, err := rand.Read(randomBytes); err != nil {
|
|
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
|
}
|
|
|
|
// Create base token with type, client ID, timestamp, and random component
|
|
randomPart := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(randomBytes)
|
|
timestamp := time.Now().Format("20060102150405")
|
|
|
|
return fmt.Sprintf("%s_%s_%s_%s", tokenType, clientID, timestamp, randomPart), nil
|
|
}
|
|
|
|
// getSigningMethod returns the JWT signing method for the given algorithm
|
|
func getSigningMethod(algorithm string) jwt.SigningMethod {
|
|
switch algorithm {
|
|
case "RS256":
|
|
return jwt.SigningMethodRS256
|
|
case "RS384":
|
|
return jwt.SigningMethodRS384
|
|
case "RS512":
|
|
return jwt.SigningMethodRS512
|
|
case "PS256":
|
|
return jwt.SigningMethodPS256
|
|
case "PS384":
|
|
return jwt.SigningMethodPS384
|
|
case "PS512":
|
|
return jwt.SigningMethodPS512
|
|
default:
|
|
return jwt.SigningMethodRS256 // Default
|
|
}
|
|
}
|
|
|
|
// generateJTI generates a unique JWT ID
|
|
func generateJTI() string {
|
|
randomBytes := make([]byte, 16)
|
|
rand.Read(randomBytes)
|
|
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(randomBytes)
|
|
}
|