package auth import ( "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "net/http" "os" "strings" "github.com/golang-jwt/jwt/v5" ) // RS256Service validates RS256 JWT tokens using a public key // Compatible with ouroboros-identity (port 3208) and aamos-admin-v2 type RS256Service struct { publicKey *rsa.PublicKey issuer string audience string } // NewRS256Service loads the public key from a PEM file func NewRS256Service(publicKeyPath string) (*RS256Service, error) { pemData, err := os.ReadFile(publicKeyPath) if err != nil { return nil, fmt.Errorf("failed to read public key: %w", err) } block, _ := pem.Decode(pemData) if block == nil { return nil, fmt.Errorf("failed to decode PEM block") } pub, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { // Try PKCS1 format pub, err = x509.ParsePKCS1PublicKey(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse public key: %w", err) } } rsaPub, ok := pub.(*rsa.PublicKey) if !ok { return nil, fmt.Errorf("not an RSA public key") } return &RS256Service{ publicKey: rsaPub, issuer: "prexo-identity", audience: "prexo", }, nil } // Middleware returns HTTP middleware that validates Bearer tokens using RS256 func (s *RS256Service) Middleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" { http.Error(w, `{"error":"missing authorization header"}`, http.StatusUnauthorized) return } if !strings.HasPrefix(authHeader, "Bearer ") { http.Error(w, `{"error":"invalid authorization format"}`, http.StatusUnauthorized) return } tokenString := strings.TrimPrefix(authHeader, "Bearer ") claims, err := s.ValidateToken(tokenString) if err != nil { http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized) return } ctx := WithClaims(r.Context(), claims) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // ValidateToken verifies an RS256 JWT token func (s *RS256Service) ValidateToken(tokenString string) (*Claims, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return s.publicKey, nil }) if err != nil || !token.Valid { return nil, fmt.Errorf("invalid token: %w", err) } mapClaims, ok := token.Claims.(jwt.MapClaims) if !ok { return nil, fmt.Errorf("invalid claims format") } claims := &Claims{ Sub: getStringClaim(mapClaims, "sub"), Iss: getStringClaim(mapClaims, "iss"), Aud: getStringClaim(mapClaims, "aud"), Exp: getInt64Claim(mapClaims, "exp"), Iat: getInt64Claim(mapClaims, "iat"), } if email, ok := mapClaims["email"].(string); ok { claims.Email = email } if orgID, ok := mapClaims["org_id"].(string); ok { claims.OrgID = orgID } if roles, ok := mapClaims["roles"].([]interface{}); ok { claims.Roles = make([]string, len(roles)) for i, r := range roles { claims.Roles[i] = fmt.Sprint(r) } } if scopes, ok := mapClaims["scopes"].([]interface{}); ok { claims.Scopes = make([]string, len(scopes)) for i, sc := range scopes { claims.Scopes[i] = fmt.Sprint(sc) } } if err := claims.Valid(); err != nil { return nil, err } return claims, nil }