mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
Complete rebuild of the plugin
* Fix bug affecting Azure OIDC authentication ( and most likely others ) * Fixes issue #51 * Ensure that appended roles are unique. Update the documentation. * Improvements targetting possible memory usage spikes. * Additional fixes and cleanup * Refactoring code to fix the issues identified by the users. * Modernize run * Fieldalignment * Multiple changes to improve performance and reduce complexity. - Optimise the errors and recovery. - Deduplicate code in metadata cache. - Remove unused performance monitoring code. - Simplify session management and settings handling. * Fix claims issue. * Add ability to overwrite the default scopes in the settings file * Well.. that escalated quickly. Completely forgot that Traefik uses outdated Yaegi and requires compatibility with 1.20 ( pre-generic Go code ). * Bugfix #51: Ensures that user provided scopes overrides work. * fixup! Bugfix #51: Ensures that user provided scopes overrides work. * fixup! fixup! Bugfix #51: Ensures that user provided scopes overrides work. * Abstract the provider logic into a separate package. * Additional micro fixes and cleanups. * Simplify all the things. * fixup! Simplify all the things. * fixup! fixup! Simplify all the things. * fixup! fixup! fixup! Simplify all the things. * fixup! fixup! fixup! fixup! Simplify all the things. * ... * Cleanup tests. * fixup! Cleanup tests. * fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! fixup! Cleanup tests. * Issue #53: Fix CSRF token handling in reverse proxy 1. ✅ HTTPS Detection Fixed (session.go:723) - Now uses X-Forwarded-Proto header instead of r.URL.Scheme - Properly detects HTTPS in reverse proxy environments 2. ✅ SameSite Cookie Attribute Fixed - Removed automatic SameSiteStrictMode for HTTPS (would break OAuth) - Keeps SameSiteLaxMode to allow OAuth callbacks from external domains - Only uses Strict for AJAX requests which don't involve OAuth redirects 3. ✅ Cookie Domain Handling Fixed - Now respects X-Forwarded-Host header for cookie domain - Ensures cookies are set for the public domain, not internal proxy domain 4. ✅ EnhanceSessionSecurity Properly Integrated - Function is now actually called during session save - Applies security enhancements without breaking OAuth flow Why Issue #53 Failed Before: 1. Cookies were not marked Secure in HTTPS environments (browser wouldn't send them back) 2. If they had been Secure with SameSite=Strict, Azure callbacks would still fail 3. Cookie domain might have been wrong (internal vs public domain) Why It Works Now: 1. Cookies are properly marked Secure for HTTPS 2. Uses SameSite=Lax to allow OAuth provider callbacks 3. Cookie domain uses public domain from X-Forwarded-Host 4. CSRF token persists through the entire OAuth flow * Next set of enhancements together with memory usage improvements. * Memory leak fixes and optimisations. * CSRF and Cookie Domain fixes * fixup! CSRF and Cookie Domain fixes * Metadata cache leak fix + profiling * fixup! Metadata cache leak fix + profiling * Memory leaks hunting, part 1337. * Further pursue of perfection. * fixup! Further pursue of perfection. * fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * Clear race conditions * fixup! Clear race conditions * Weekend fun with memory leaks * Splitting code into multiple files with reasonable testing coverage. ``` ok github.com/lukaszraczylo/traefikoidc 117.017s coverage: 72.6% of statements ok github.com/lukaszraczylo/traefikoidc/auth 0.505s coverage: 87.1% of statements ok github.com/lukaszraczylo/traefikoidc/circuit_breaker 0.283s coverage: 99.0% of statements github.com/lukaszraczylo/traefikoidc/config coverage: 0.0% of statements ok github.com/lukaszraczylo/traefikoidc/handlers 0.349s coverage: 98.2% of statements ok github.com/lukaszraczylo/traefikoidc/internal/providers (cached) coverage: 94.3% of statements ok github.com/lukaszraczylo/traefikoidc/middleware 0.808s coverage: 78.0% of statements ok github.com/lukaszraczylo/traefikoidc/recovery 0.653s coverage: 100.0% of statements ok github.com/lukaszraczylo/traefikoidc/session/chunking (cached) coverage: 87.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/core (cached) coverage: 85.6% of statements ok github.com/lukaszraczylo/traefikoidc/session/crypto (cached) coverage: 81.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/storage (cached) coverage: 93.5% of statements ok github.com/lukaszraczylo/traefikoidc/session/validators (cached) coverage: 98.8% of statements ```` * fixup! Splitting code into multiple files with reasonable testing coverage. * fixup! fixup! Splitting code into multiple files with reasonable testing coverage. * Weekend fun with further optimisations. * fixup! Weekend fun with further optimisations. * fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * Pre-release cleanup. * Enhance test coverage. * fixup! Enhance test coverage. * fixup! fixup! Enhance test coverage. * fixup! fixup! fixup! Enhance test coverage.
This commit is contained in:
@@ -7,276 +7,184 @@ import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JWK represents a JSON Web Key as defined in RFC 7517.
|
||||
// It can represent different key types including RSA, EC, and symmetric keys.
|
||||
type JWK struct {
|
||||
// Key type (e.g., "RSA", "EC", "oct")
|
||||
Kty string `json:"kty"`
|
||||
Kid string `json:"kid"`
|
||||
Use string `json:"use"`
|
||||
N string `json:"n"`
|
||||
E string `json:"e"`
|
||||
Alg string `json:"alg"`
|
||||
Crv string `json:"crv"`
|
||||
X string `json:"x"`
|
||||
Y string `json:"y"`
|
||||
// Key use (e.g., "sig" for signature, "enc" for encryption)
|
||||
Use string `json:"use,omitempty"`
|
||||
// Key operations allowed
|
||||
KeyOps []string `json:"key_ops,omitempty"`
|
||||
// Algorithm intended for use with this key
|
||||
Alg string `json:"alg,omitempty"`
|
||||
// Key ID
|
||||
Kid string `json:"kid,omitempty"`
|
||||
|
||||
// RSA specific fields
|
||||
N string `json:"n,omitempty"` // Modulus
|
||||
E string `json:"e,omitempty"` // Exponent
|
||||
|
||||
// EC specific fields
|
||||
Crv string `json:"crv,omitempty"` // Curve
|
||||
X string `json:"x,omitempty"` // X coordinate
|
||||
Y string `json:"y,omitempty"` // Y coordinate
|
||||
}
|
||||
|
||||
// JWKSet represents a set of JSON Web Keys.
|
||||
// Typically fetched from an OIDC provider's JWKS endpoint.
|
||||
type JWKSet struct {
|
||||
// Keys contains the array of JWK objects
|
||||
Keys []JWK `json:"keys"`
|
||||
}
|
||||
|
||||
// JWKCache provides thread-safe caching of JWKS using UniversalCache
|
||||
type JWKCache struct {
|
||||
jwks *JWKSet
|
||||
expiresAt time.Time
|
||||
mutex sync.RWMutex
|
||||
// CacheLifetime is configurable to determine how long the JWKS is cached.
|
||||
CacheLifetime time.Duration
|
||||
internalCache *Cache // To hold the closable Cache instance from cache.go
|
||||
maxSize int // Maximum number of items in the cache
|
||||
cache *UniversalCache
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// JWKCacheInterface defines the contract for JWK caching implementations.
|
||||
type JWKCacheInterface interface {
|
||||
GetJWKS(ctx context.Context, jwksURL string, httpClient *http.Client) (*JWKSet, error)
|
||||
Cleanup()
|
||||
Close()
|
||||
}
|
||||
|
||||
// GetJWKS retrieves the JSON Web Key Set (JWKS) from the cache or fetches it from the provider.
|
||||
// It first checks if a valid, non-expired JWKS is present in the cache. If so, it returns the cached version.
|
||||
// Otherwise, it attempts to fetch the JWKS from the specified jwksURL using the provided httpClient.
|
||||
// If the fetch is successful, the JWKS is stored in the cache with an expiration time based on CacheLifetime
|
||||
// (defaulting to 1 hour if not set) and returned.
|
||||
// This method uses double-checked locking to minimize contention when the cache needs refreshing.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the HTTP request if fetching is required.
|
||||
// - jwksURL: The URL of the OIDC provider's JWKS endpoint.
|
||||
// - httpClient: The HTTP client to use for fetching the JWKS.
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the JWKSet containing the keys.
|
||||
// - An error if fetching fails or the response cannot be decoded.
|
||||
// NewJWKCache creates a new JWK cache using the global cache manager
|
||||
func NewJWKCache() *JWKCache {
|
||||
cache := &JWKCache{
|
||||
CacheLifetime: 1 * time.Hour,
|
||||
maxSize: 100, // Default maximum size
|
||||
internalCache: NewCache(),
|
||||
manager := GetUniversalCacheManager(nil)
|
||||
return &JWKCache{
|
||||
cache: manager.GetJWKCache(),
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
// GetJWKS retrieves JWKS from cache or fetches from the remote URL if not cached.
|
||||
func (c *JWKCache) GetJWKS(ctx context.Context, jwksURL string, httpClient *http.Client) (*JWKSet, error) {
|
||||
// First check if we already have cached JWKS for this URL
|
||||
if c.internalCache != nil {
|
||||
if cachedJwks, found := c.internalCache.Get(jwksURL); found {
|
||||
return cachedJwks.(*JWKSet), nil
|
||||
// Check cache first
|
||||
if cachedValue, found := c.cache.Get(jwksURL); found {
|
||||
if jwks, ok := cachedValue.(*JWKSet); ok {
|
||||
return jwks, nil
|
||||
}
|
||||
}
|
||||
|
||||
// STABILITY FIX: Fix race condition in double-checked locking
|
||||
// First read check with read lock
|
||||
c.mutex.RLock()
|
||||
if c.jwks != nil && time.Now().Before(c.expiresAt) {
|
||||
jwks := c.jwks // Copy reference while holding read lock
|
||||
c.mutex.RUnlock()
|
||||
return jwks, nil
|
||||
}
|
||||
c.mutex.RUnlock()
|
||||
|
||||
// Acquire write lock for potential update
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
// Second check after acquiring write lock (double-checked locking)
|
||||
if c.jwks != nil && time.Now().Before(c.expiresAt) {
|
||||
return c.jwks, nil
|
||||
// Double-check after acquiring lock
|
||||
if cachedValue, found := c.cache.Get(jwksURL); found {
|
||||
if jwks, ok := cachedValue.(*JWKSet); ok {
|
||||
return jwks, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch new JWKS
|
||||
// Fetch from URL
|
||||
jwks, err := fetchJWKS(ctx, jwksURL, httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// STABILITY FIX: Validate JWKS contains keys before caching
|
||||
if len(jwks.Keys) == 0 {
|
||||
return nil, fmt.Errorf("JWKS response contains no keys")
|
||||
}
|
||||
|
||||
// Update cache atomically
|
||||
c.jwks = jwks
|
||||
lifetime := c.CacheLifetime
|
||||
if lifetime == 0 {
|
||||
lifetime = 1 * time.Hour
|
||||
}
|
||||
c.expiresAt = time.Now().Add(lifetime)
|
||||
|
||||
// Also store in the internalCache
|
||||
if c.internalCache != nil {
|
||||
c.internalCache.Set(jwksURL, jwks, lifetime)
|
||||
}
|
||||
// Cache for 1 hour
|
||||
c.cache.Set(jwksURL, jwks, 1*time.Hour)
|
||||
|
||||
return jwks, nil
|
||||
}
|
||||
|
||||
// Cleanup removes the cached JWKS if it has expired.
|
||||
// This is intended to be called periodically to ensure stale JWKS data is cleared.
|
||||
// Cleanup is a no-op as cleanup is handled by UniversalCache
|
||||
func (c *JWKCache) Cleanup() {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if c.jwks != nil && now.After(c.expiresAt) {
|
||||
c.jwks = nil
|
||||
}
|
||||
// Handled internally by UniversalCache
|
||||
}
|
||||
|
||||
// Close shuts down the cache's auto-cleanup routine.
|
||||
// Close is a no-op as the cache is managed globally
|
||||
func (c *JWKCache) Close() {
|
||||
// Close shuts down the internal cache's auto-cleanup routine, if the cache exists.
|
||||
if c.internalCache != nil {
|
||||
c.internalCache.Close()
|
||||
}
|
||||
// Managed by global cache manager
|
||||
}
|
||||
|
||||
// SetMaxSize sets the maximum number of items in the cache
|
||||
func (c *JWKCache) SetMaxSize(size int) {
|
||||
c.maxSize = size
|
||||
if c.internalCache != nil {
|
||||
c.internalCache.maxSize = size
|
||||
}
|
||||
}
|
||||
|
||||
// fetchJWKS retrieves the JSON Web Key Set (JWKS) from the specified URL.
|
||||
// It uses the provided context and HTTP client to make the request.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the HTTP request.
|
||||
// - jwksURL: The URL of the OIDC provider's JWKS endpoint.
|
||||
// - httpClient: The HTTP client to use for the request.
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to the fetched JWKSet.
|
||||
// - An error if the request fails, the status code is not OK, or the response body cannot be decoded.
|
||||
// fetchJWKS fetches JWKS from a remote URL
|
||||
func fetchJWKS(ctx context.Context, jwksURL string, httpClient *http.Client) (*JWKSet, error) {
|
||||
// Create a request with context to enforce timeout
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", jwksURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create JWKS request: %w", err)
|
||||
return nil, fmt.Errorf("error creating JWKS request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch JWKS: %w", err)
|
||||
return nil, fmt.Errorf("error fetching JWKS: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to fetch JWKS: unexpected status code %d", resp.StatusCode)
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("JWKS fetch failed with status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading JWKS response: %w", err)
|
||||
}
|
||||
|
||||
var jwks JWKSet
|
||||
if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode JWKS: %w", err)
|
||||
if err := json.Unmarshal(body, &jwks); err != nil {
|
||||
return nil, fmt.Errorf("error parsing JWKS: %w", err)
|
||||
}
|
||||
|
||||
return &jwks, nil
|
||||
}
|
||||
|
||||
// jwkToPEM converts a JWK (JSON Web Key) object into PEM (Privacy-Enhanced Mail) format.
|
||||
// It selects the appropriate conversion function based on the JWK's key type ("kty").
|
||||
// Currently supports "RSA" and "EC" key types.
|
||||
//
|
||||
// Parameters:
|
||||
// - jwk: A pointer to the JWK object to convert.
|
||||
//
|
||||
// Returns:
|
||||
// - A byte slice containing the public key in PEM format.
|
||||
// - An error if the key type is unsupported or conversion fails.
|
||||
func jwkToPEM(jwk *JWK) ([]byte, error) {
|
||||
converter, ok := jwkConverters[jwk.Kty]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported key type: %s", jwk.Kty)
|
||||
// ToRSAPublicKey converts a JWK to an RSA public key.
|
||||
// Returns an error if the JWK is not an RSA key or if the key data is invalid.
|
||||
func (jwk *JWK) ToRSAPublicKey() (*rsa.PublicKey, error) {
|
||||
if jwk.Kty != "RSA" {
|
||||
return nil, fmt.Errorf("not an RSA key")
|
||||
}
|
||||
return converter(jwk)
|
||||
}
|
||||
|
||||
type jwkToPEMConverter func(*JWK) ([]byte, error)
|
||||
|
||||
var jwkConverters = map[string]jwkToPEMConverter{
|
||||
"RSA": rsaJWKToPEM,
|
||||
"EC": ecJWKToPEM,
|
||||
}
|
||||
|
||||
// rsaJWKToPEM converts an RSA JWK into PEM format.
|
||||
// It decodes the modulus (n) and exponent (e) from base64 URL encoding,
|
||||
// constructs an rsa.PublicKey, marshals it into PKIX format, and then
|
||||
// encodes it as a PEM block.
|
||||
//
|
||||
// Parameters:
|
||||
// - jwk: A pointer to the RSA JWK object (must have "kty": "RSA").
|
||||
//
|
||||
// Returns:
|
||||
// - A byte slice containing the RSA public key in PEM format.
|
||||
// - An error if decoding parameters fails or key marshaling fails.
|
||||
func rsaJWKToPEM(jwk *JWK) ([]byte, error) {
|
||||
nBytes, err := base64.RawURLEncoding.DecodeString(jwk.N)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode JWK 'n' parameter: %w", err)
|
||||
return nil, fmt.Errorf("error decoding modulus: %w", err)
|
||||
}
|
||||
|
||||
eBytes, err := base64.RawURLEncoding.DecodeString(jwk.E)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode JWK 'e' parameter: %w", err)
|
||||
return nil, fmt.Errorf("error decoding exponent: %w", err)
|
||||
}
|
||||
|
||||
n := new(big.Int).SetBytes(nBytes)
|
||||
e := new(big.Int).SetBytes(eBytes)
|
||||
|
||||
pubKey := &rsa.PublicKey{
|
||||
N: n,
|
||||
E: int(e.Int64()),
|
||||
// Convert exponent bytes to int
|
||||
var e int
|
||||
if len(eBytes) <= 8 {
|
||||
// Pad to 8 bytes for uint64
|
||||
paddedE := make([]byte, 8)
|
||||
copy(paddedE[8-len(eBytes):], eBytes)
|
||||
e = int(binary.BigEndian.Uint64(paddedE))
|
||||
} else {
|
||||
return nil, fmt.Errorf("exponent too large")
|
||||
}
|
||||
|
||||
pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal RSA public key: %w", err)
|
||||
}
|
||||
|
||||
pubKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: pubKeyBytes,
|
||||
})
|
||||
|
||||
return pubKeyPEM, nil
|
||||
return &rsa.PublicKey{
|
||||
N: new(big.Int).SetBytes(nBytes),
|
||||
E: e,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ecJWKToPEM converts an EC (Elliptic Curve) JWK into PEM format.
|
||||
// It decodes the X and Y coordinates from base64 URL encoding, determines the
|
||||
// elliptic curve based on the "crv" parameter (P-256, P-384, P-521),
|
||||
// constructs an ecdsa.PublicKey, marshals it into PKIX format, and then
|
||||
// encodes it as a PEM block.
|
||||
//
|
||||
// Parameters:
|
||||
// - jwk: A pointer to the EC JWK object (must have "kty": "EC").
|
||||
//
|
||||
// Returns:
|
||||
// - A byte slice containing the EC public key in PEM format.
|
||||
// - An error if decoding parameters fails, the curve is unsupported, or key marshaling fails.
|
||||
func ecJWKToPEM(jwk *JWK) ([]byte, error) {
|
||||
xBytes, err := base64.RawURLEncoding.DecodeString(jwk.X)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode JWK 'x' parameter: %w", err)
|
||||
}
|
||||
yBytes, err := base64.RawURLEncoding.DecodeString(jwk.Y)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode JWK 'y' parameter: %w", err)
|
||||
// ToECDSAPublicKey converts a JWK to an ECDSA public key.
|
||||
// Returns an error if the JWK is not an EC key or if the key data is invalid.
|
||||
func (jwk *JWK) ToECDSAPublicKey() (*ecdsa.PublicKey, error) {
|
||||
if jwk.Kty != "EC" {
|
||||
return nil, fmt.Errorf("not an EC key")
|
||||
}
|
||||
|
||||
var curve elliptic.Curve
|
||||
@@ -288,24 +196,68 @@ func ecJWKToPEM(jwk *JWK) ([]byte, error) {
|
||||
case "P-521":
|
||||
curve = elliptic.P521()
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported elliptic curve: %s", jwk.Crv)
|
||||
return nil, fmt.Errorf("unsupported curve: %s", jwk.Crv)
|
||||
}
|
||||
|
||||
pubKey := &ecdsa.PublicKey{
|
||||
xBytes, err := base64.RawURLEncoding.DecodeString(jwk.X)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding X coordinate: %w", err)
|
||||
}
|
||||
|
||||
yBytes, err := base64.RawURLEncoding.DecodeString(jwk.Y)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding Y coordinate: %w", err)
|
||||
}
|
||||
|
||||
return &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: new(big.Int).SetBytes(xBytes),
|
||||
Y: new(big.Int).SetBytes(yBytes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetKey finds a key by its ID (kid) in the JWKSet.
|
||||
// Returns nil if no key with the given ID is found.
|
||||
func (jwks *JWKSet) GetKey(kid string) *JWK {
|
||||
for _, key := range jwks.Keys {
|
||||
if key.Kid == kid {
|
||||
return &key
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// jwkToPEM converts a JWK to PEM format for signature verification
|
||||
func jwkToPEM(jwk *JWK) ([]byte, error) {
|
||||
var publicKey interface{}
|
||||
var err error
|
||||
|
||||
switch jwk.Kty {
|
||||
case "RSA":
|
||||
publicKey, err = jwk.ToRSAPublicKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert RSA JWK: %w", err)
|
||||
}
|
||||
case "EC":
|
||||
publicKey, err = jwk.ToECDSAPublicKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert EC JWK: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported key type: %s", jwk.Kty)
|
||||
}
|
||||
|
||||
pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey)
|
||||
// Marshal the public key to DER format
|
||||
pubKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal EC public key: %w", err)
|
||||
return nil, fmt.Errorf("failed to marshal public key: %w", err)
|
||||
}
|
||||
|
||||
pubKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
// Encode to PEM format
|
||||
pemBlock := &pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: pubKeyBytes,
|
||||
})
|
||||
}
|
||||
|
||||
return pubKeyPEM, nil
|
||||
return pem.EncodeToMemory(pemBlock), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user