Enhance OAuth test setup with reusable certificate management
- Introduced global test certificate paths to avoid redundant certificate generation across tests, improving efficiency. - Implemented a function to create temporary certificates once for all tests, ensuring consistent usage of signing certificates. - Updated test configurations to utilize the new certificate management, enhancing clarity and maintainability. - Added cleanup functionality for global test certificates to ensure proper resource management after tests.
This commit is contained in:
parent
e1428551ba
commit
966e0cfd00
4 changed files with 558 additions and 32 deletions
|
|
@ -19,6 +19,8 @@ type Service struct {
|
|||
userProvider types.UserProvider
|
||||
clientProvider types.ClientProvider
|
||||
prefix string
|
||||
// Signing certificates for JWT token signing and verification
|
||||
signingCerts *SigningCertificates
|
||||
}
|
||||
|
||||
// Config OAuth service configuration
|
||||
|
|
@ -124,6 +126,17 @@ func NewService(config *Config) (*Service, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Load Certificates
|
||||
signingCerts, err := LoadSigningCertificates(&config.Signing)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load signing certificates: %w", err)
|
||||
}
|
||||
|
||||
// Validate the loaded certificates
|
||||
if err := signingCerts.ValidateCertificate(); err != nil {
|
||||
return nil, fmt.Errorf("certificate validation failed: %w", err)
|
||||
}
|
||||
|
||||
service := &Service{
|
||||
config: config,
|
||||
store: config.Store,
|
||||
|
|
@ -131,6 +144,7 @@ func NewService(config *Config) (*Service, error) {
|
|||
userProvider: userProvider,
|
||||
clientProvider: clientProvider,
|
||||
prefix: keyPrefix,
|
||||
signingCerts: signingCerts,
|
||||
}
|
||||
|
||||
return service, nil
|
||||
|
|
@ -231,11 +245,17 @@ func validateConfig(config *Config) error {
|
|||
return types.ErrIssuerURLMissing
|
||||
}
|
||||
|
||||
// Validate certificate configuration
|
||||
if config.Signing.SigningCertPath == "" || config.Signing.SigningKeyPath == "" {
|
||||
return types.ErrCertificateMissing
|
||||
// Certificate configuration validation
|
||||
// If both cert and key paths are provided, they must both exist or be empty
|
||||
certPathProvided := config.Signing.SigningCertPath != ""
|
||||
keyPathProvided := config.Signing.SigningKeyPath != ""
|
||||
|
||||
if certPathProvided != keyPathProvided {
|
||||
return types.ErrCertificateMissing // Both paths must be provided together or not at all
|
||||
}
|
||||
|
||||
// If paths are not provided, temporary certificates will be generated automatically
|
||||
|
||||
// Validate token configuration
|
||||
if config.Token.AccessTokenLifetime <= 0 {
|
||||
return types.ErrInvalidTokenLifetime
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ import (
|
|||
// $YAO_SOURCE_ROOT is the root directory of the Yao source code.
|
||||
// source $YAO_SOURCE_ROOT/env.local.sh
|
||||
|
||||
// Test certificate paths - created once and reused across tests
|
||||
var (
|
||||
testCertPath string
|
||||
testKeyPath string
|
||||
)
|
||||
|
||||
// Store configuration for parameterized tests
|
||||
type StoreConfig struct {
|
||||
Name string
|
||||
|
|
@ -311,14 +317,19 @@ func setupOAuthTestEnvironment(t *testing.T) (*Service, store.Store, store.Store
|
|||
// Create cache
|
||||
cache := getLRUCache(t)
|
||||
|
||||
// Create test certificates once if not already created
|
||||
if testCertPath == "" || testKeyPath == "" {
|
||||
createTestCertificatesOnce(t)
|
||||
}
|
||||
|
||||
// Create OAuth service configuration
|
||||
oauthConfig := &Config{
|
||||
Store: mainStore,
|
||||
Cache: cache,
|
||||
Signing: types.SigningConfig{
|
||||
SigningAlgorithm: "RS256",
|
||||
SigningCertPath: "/tmp/test-cert.pem",
|
||||
SigningKeyPath: "/tmp/test-key.pem",
|
||||
SigningCertPath: testCertPath,
|
||||
SigningKeyPath: testKeyPath,
|
||||
},
|
||||
Token: types.TokenConfig{
|
||||
AccessTokenLifetime: time.Hour,
|
||||
|
|
@ -498,6 +509,26 @@ func cleanupTestData(t *testing.T, service *Service) {
|
|||
}
|
||||
}
|
||||
|
||||
// createTestCertificatesOnce creates temporary certificate pair for all tests
|
||||
func createTestCertificatesOnce(t *testing.T) {
|
||||
// Generate temporary certificates (auto-generate with empty paths)
|
||||
config := &types.SigningConfig{
|
||||
SigningAlgorithm: "RS256",
|
||||
SigningCertPath: "",
|
||||
SigningKeyPath: "",
|
||||
}
|
||||
|
||||
certs, err := LoadSigningCertificates(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate test certificates: %v", err)
|
||||
}
|
||||
|
||||
testCertPath = certs.SigningCertPath
|
||||
testKeyPath = certs.SigningKeyPath
|
||||
|
||||
t.Logf("Created test certificates: cert=%s, key=%s", testCertPath, testKeyPath)
|
||||
}
|
||||
|
||||
// Helper functions for store setup (same as in other test files)
|
||||
|
||||
func getMongoStore(t *testing.T) store.Store {
|
||||
|
|
@ -561,9 +592,27 @@ func getStoreConfigs() []StoreConfig {
|
|||
func TestMain(m *testing.M) {
|
||||
// Run tests
|
||||
code := m.Run()
|
||||
|
||||
// Cleanup global test certificates
|
||||
cleanupGlobalTestCertificates()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// cleanupGlobalTestCertificates removes global test certificates
|
||||
func cleanupGlobalTestCertificates() {
|
||||
if testCertPath != "" {
|
||||
if _, err := os.Stat(testCertPath); !os.IsNotExist(err) {
|
||||
os.Remove(testCertPath)
|
||||
}
|
||||
}
|
||||
if testKeyPath != "" {
|
||||
if _, err := os.Stat(testKeyPath); !os.IsNotExist(err) {
|
||||
os.Remove(testKeyPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
t.Run("create service with valid config", func(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
|
|
@ -601,8 +650,8 @@ func TestNewService(t *testing.T) {
|
|||
config := &Config{
|
||||
Store: store,
|
||||
Signing: types.SigningConfig{
|
||||
SigningCertPath: "/tmp/cert.pem",
|
||||
SigningKeyPath: "/tmp/key.pem",
|
||||
SigningCertPath: testCertPath,
|
||||
SigningKeyPath: testKeyPath,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -645,8 +694,8 @@ func TestConfigDefaults(t *testing.T) {
|
|||
Store: store,
|
||||
IssuerURL: "https://test.example.com",
|
||||
Signing: types.SigningConfig{
|
||||
SigningCertPath: "/tmp/cert.pem",
|
||||
SigningKeyPath: "/tmp/key.pem",
|
||||
SigningCertPath: testCertPath,
|
||||
SigningKeyPath: testKeyPath,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -725,8 +774,8 @@ func TestProviderInitialization(t *testing.T) {
|
|||
Cache: cache,
|
||||
IssuerURL: "https://test.example.com",
|
||||
Signing: types.SigningConfig{
|
||||
SigningCertPath: "/tmp/cert.pem",
|
||||
SigningKeyPath: "/tmp/key.pem",
|
||||
SigningCertPath: testCertPath,
|
||||
SigningKeyPath: testKeyPath,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -753,8 +802,8 @@ func TestProviderInitialization(t *testing.T) {
|
|||
Cache: cache,
|
||||
IssuerURL: "https://test.example.com",
|
||||
Signing: types.SigningConfig{
|
||||
SigningCertPath: "/tmp/cert.pem",
|
||||
SigningKeyPath: "/tmp/key.pem",
|
||||
SigningCertPath: testCertPath,
|
||||
SigningKeyPath: testKeyPath,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -772,8 +821,8 @@ func TestProviderInitialization(t *testing.T) {
|
|||
ClientProvider: customClientProvider,
|
||||
IssuerURL: "https://test.example.com",
|
||||
Signing: types.SigningConfig{
|
||||
SigningCertPath: "/tmp/cert.pem",
|
||||
SigningKeyPath: "/tmp/key.pem",
|
||||
SigningCertPath: testCertPath,
|
||||
SigningKeyPath: testKeyPath,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -829,8 +878,8 @@ func TestConfigValidation(t *testing.T) {
|
|||
Store: getBadgerStore(t),
|
||||
IssuerURL: "https://test.example.com",
|
||||
Signing: types.SigningConfig{
|
||||
SigningCertPath: "/tmp/cert.pem",
|
||||
SigningKeyPath: "/tmp/key.pem",
|
||||
SigningCertPath: testCertPath,
|
||||
SigningKeyPath: testKeyPath,
|
||||
},
|
||||
Token: types.TokenConfig{
|
||||
AccessTokenLifetime: time.Hour,
|
||||
|
|
@ -839,7 +888,12 @@ func TestConfigValidation(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
err := validateConfig(config)
|
||||
// Set defaults first (like NewService does)
|
||||
err := setConfigDefaults(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Then validate
|
||||
err = validateConfig(config)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
|
|
@ -847,12 +901,17 @@ func TestConfigValidation(t *testing.T) {
|
|||
config := &Config{
|
||||
IssuerURL: "https://test.example.com",
|
||||
Signing: types.SigningConfig{
|
||||
SigningCertPath: "/tmp/cert.pem",
|
||||
SigningKeyPath: "/tmp/key.pem",
|
||||
SigningCertPath: testCertPath,
|
||||
SigningKeyPath: testKeyPath,
|
||||
},
|
||||
}
|
||||
|
||||
err := validateConfig(config)
|
||||
// Set defaults first (like NewService does)
|
||||
err := setConfigDefaults(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Then validate
|
||||
err = validateConfig(config)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, types.ErrStoreMissing, err)
|
||||
})
|
||||
|
|
@ -861,24 +920,37 @@ func TestConfigValidation(t *testing.T) {
|
|||
config := &Config{
|
||||
Store: getBadgerStore(t),
|
||||
Signing: types.SigningConfig{
|
||||
SigningCertPath: "/tmp/cert.pem",
|
||||
SigningKeyPath: "/tmp/key.pem",
|
||||
SigningCertPath: testCertPath,
|
||||
SigningKeyPath: testKeyPath,
|
||||
},
|
||||
}
|
||||
|
||||
err := validateConfig(config)
|
||||
// Set defaults first (like NewService does)
|
||||
err := setConfigDefaults(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Then validate
|
||||
err = validateConfig(config)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, types.ErrIssuerURLMissing, err)
|
||||
})
|
||||
|
||||
t.Run("missing certificate configuration", func(t *testing.T) {
|
||||
t.Run("partial certificate configuration", func(t *testing.T) {
|
||||
config := &Config{
|
||||
Store: getBadgerStore(t),
|
||||
IssuerURL: "https://test.example.com",
|
||||
Signing: types.SigningConfig{},
|
||||
Signing: types.SigningConfig{
|
||||
SigningCertPath: testCertPath, // Only cert path, missing key path
|
||||
SigningKeyPath: "",
|
||||
},
|
||||
}
|
||||
|
||||
err := validateConfig(config)
|
||||
// Set defaults first (like NewService does)
|
||||
err := setConfigDefaults(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Then validate
|
||||
err = validateConfig(config)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, types.ErrCertificateMissing, err)
|
||||
})
|
||||
|
|
@ -888,15 +960,20 @@ func TestConfigValidation(t *testing.T) {
|
|||
Store: getBadgerStore(t),
|
||||
IssuerURL: "https://test.example.com",
|
||||
Signing: types.SigningConfig{
|
||||
SigningCertPath: "/tmp/cert.pem",
|
||||
SigningKeyPath: "/tmp/key.pem",
|
||||
SigningCertPath: testCertPath,
|
||||
SigningKeyPath: testKeyPath,
|
||||
},
|
||||
Token: types.TokenConfig{
|
||||
AccessTokenLifetime: -1 * time.Hour,
|
||||
AccessTokenLifetime: -1 * time.Hour, // Invalid negative lifetime
|
||||
},
|
||||
}
|
||||
|
||||
err := validateConfig(config)
|
||||
// Set defaults first (like NewService does)
|
||||
err := setConfigDefaults(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Then validate
|
||||
err = validateConfig(config)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, types.ErrInvalidTokenLifetime, err)
|
||||
})
|
||||
|
|
|
|||
429
openapi/oauth/signing.go
Normal file
429
openapi/oauth/signing.go
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
package oauth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"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()
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ var (
|
|||
ErrInvalidConfiguration = &ErrorResponse{Code: "invalid_configuration", ErrorDescription: "Invalid OAuth service configuration"}
|
||||
ErrStoreMissing = &ErrorResponse{Code: "store_missing", ErrorDescription: "Store is required for OAuth service"}
|
||||
ErrIssuerURLMissing = &ErrorResponse{Code: "issuer_url_missing", ErrorDescription: "Issuer URL is required for OAuth service"}
|
||||
ErrCertificateMissing = &ErrorResponse{Code: "certificate_missing", ErrorDescription: "JWT signing certificate and key are required"}
|
||||
ErrCertificateMissing = &ErrorResponse{Code: "certificate_missing", ErrorDescription: "JWT signing certificate and key paths must both be provided or both be empty"}
|
||||
ErrInvalidTokenLifetime = &ErrorResponse{Code: "invalid_token_lifetime", ErrorDescription: "Token lifetime must be greater than 0"}
|
||||
ErrPKCEConfigurationInvalid = &ErrorResponse{Code: "pkce_configuration_invalid", ErrorDescription: "PKCE configuration is invalid"}
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue