yao/openapi/oauth/signing.go
Max b68660b3cd Enhance OAuth token handling and refresh logic
- Update the `Authenticate` method in the OAuth guard to allow for token refresh when an access token is expired but still valid.
- Introduce `TryRefreshToken` method to handle the refresh token logic, including token rotation and cookie management.
- Implement `VerifyTokenAllowExpired` and `VerifyRefreshToken` methods to improve token verification processes.
- Adjust error handling to provide clearer responses for token refresh failures.
- Refactor token expiration strategies in the login process to ensure consistent handling of access and refresh tokens.
2026-02-21 18:06:27 +08:00

1036 lines
30 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) {
if strings.Contains(token, ".") {
return s.verifyJWTToken(token)
}
return s.verifyOpaqueToken(token)
}
// VerifyTokenAllowExpired verifies token signature but allows expired tokens.
// Used by Guard to parse expired access tokens before attempting refresh.
func (s *Service) VerifyTokenAllowExpired(token string) (*types.TokenClaims, error) {
if strings.Contains(token, ".") {
return s.verifyJWTTokenAllowExpired(token)
}
return s.verifyOpaqueToken(token)
}
// VerifyRefreshToken verifies a refresh token based on its format.
// For opaque tokens it looks up the refresh token store (not the access token store).
func (s *Service) VerifyRefreshToken(token string) (*types.TokenClaims, error) {
if strings.Contains(token, ".") {
// JWT refresh tokens can be verified with the same JWT logic
return s.verifyJWTToken(token)
}
return s.verifyOpaqueRefreshToken(token)
}
// verifyOpaqueRefreshToken verifies an opaque refresh token using the refresh token store.
func (s *Service) verifyOpaqueRefreshToken(token string) (*types.TokenClaims, error) {
tokenInfo, err := s.getRefreshTokenData(token)
if err != nil {
return nil, fmt.Errorf("refresh token not found or invalid: %w", err)
}
clientID, _ := tokenInfo["client_id"].(string)
scope, _ := tokenInfo["scope"].(string)
subject, _ := tokenInfo["subject"].(string)
claims := &types.TokenClaims{
Subject: subject,
ClientID: clientID,
Scope: scope,
TokenType: "refresh_token",
Issuer: s.config.IssuerURL,
}
if issuedAt, ok := tokenInfo["issued_at"].(int64); ok {
claims.IssuedAt = time.Unix(issuedAt, 0)
}
if expiresAt, ok := tokenInfo["expires_at"].(int64); ok {
claims.ExpiresAt = time.Unix(expiresAt, 0)
if time.Now().After(claims.ExpiresAt) {
return nil, fmt.Errorf("refresh token expired")
}
}
return claims, nil
}
// 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) {
return s.parseJWTToken(tokenString, false)
}
// verifyJWTTokenAllowExpired parses a JWT token, verifying signature but allowing expiration.
// Returns claims even if the token is expired (signature must still be valid).
func (s *Service) verifyJWTTokenAllowExpired(tokenString string) (*types.TokenClaims, error) {
return s.parseJWTToken(tokenString, true)
}
// parseJWTToken is the shared JWT parsing logic.
// When allowExpired is true, expired tokens are still parsed (signature-only verification).
func (s *Service) parseJWTToken(tokenString string, allowExpired bool) (*types.TokenClaims, error) {
if s.signingCerts == nil || s.signingCerts.SigningCert == nil {
return nil, fmt.Errorf("signing certificates not initialized")
}
parserOpts := []jwt.ParserOption{}
if allowExpired {
parserOpts = append(parserOpts, jwt.WithoutClaimsValidation())
}
token, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
expectedMethod := getSigningMethod(s.config.Token.AccessTokenSigningAlg)
if token.Method != expectedMethod {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return s.signingCerts.GetPublicKey(), nil
}, parserOpts...)
if err != nil {
return nil, fmt.Errorf("failed to parse JWT token: %w", err)
}
if !allowExpired && !token.Valid {
return nil, fmt.Errorf("invalid JWT token")
}
mapClaims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid JWT claims type")
}
return s.extractTokenClaims(mapClaims), nil
}
// extractTokenClaims converts jwt.MapClaims to types.TokenClaims
func (s *Service) extractTokenClaims(mapClaims jwt.MapClaims) *types.TokenClaims {
tokenClaims := &types.TokenClaims{
Extra: make(map[string]interface{}),
}
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
}
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)
}
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
}
if teamID, ok := mapClaims["team_id"].(string); ok {
tokenClaims.TeamID = teamID
}
if tenantID, ok := mapClaims["tenant_id"].(string); ok {
tokenClaims.TenantID = tenantID
}
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
}
// 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)
}