@@ -71,6 +71,12 @@ type VerifierConfig struct {
7171 // UserIDClaim allows overriding default claim used to extract user ID from token.
7272 // By default, Centrifugo uses "sub" and we recommend keeping the default if possible.
7373 UserIDClaim string
74+
75+ // InsecureSkipJWKSEndpointSafetyCheck disables the config-time safety validation of
76+ // JWKS endpoint URL templates. This is INSECURE and exists only as a temporary escape
77+ // hatch for users upgrading with existing permissive regex patterns. This option will
78+ // be removed in a future release.
79+ InsecureSkipJWKSEndpointSafetyCheck bool
7480}
7581
7682func (c VerifierConfig ) Validate () error {
@@ -80,6 +86,15 @@ func (c VerifierConfig) Validate() error {
8086 if c .Issuer != "" && c .IssuerRegex != "" {
8187 return errors .New ("can not use both token_issuer and token_issuer_regex, configure only one of them" )
8288 }
89+ if c .JWKSPublicEndpoint != "" {
90+ if err := validateJWKSEndpointSafety (c .JWKSPublicEndpoint , c .IssuerRegex , c .AudienceRegex ); err != nil {
91+ if c .InsecureSkipJWKSEndpointSafetyCheck {
92+ log .Warn ().Err (err ).Msg ("JWKS endpoint template safety check skipped — this is INSECURE and the escape hatch will be removed in a future release, please update your regex to use an explicit list of allowed values" )
93+ } else {
94+ return err
95+ }
96+ }
97+ }
8398 return nil
8499}
85100
@@ -456,19 +471,15 @@ func (verifier *VerifierJWT) verifySignatureByJWK(token *jwt.Token, tokenVars ma
456471 return verifier .jwksManager .verify (token , tokenVars )
457472}
458473
459- func (verifier * VerifierJWT ) validateClaims (claims jwt.RegisteredClaims , tokenVars map [string ]any ) error {
460- if verifier .audience != "" && ! claims .IsForAudience (verifier .audience ) {
461- return fmt .Errorf ("%w: invalid audience" , ErrInvalidToken )
462- }
463-
464- if verifier .issuer != "" && ! claims .IsIssuer (verifier .issuer ) {
465- return fmt .Errorf ("%w: invalid issuer" , ErrInvalidToken )
466- }
467-
474+ // extractTokenVars extracts named group matches from issuer/audience regex into tokenVars.
475+ // This must be called before signature verification because the extracted values may be
476+ // needed to construct the JWKS endpoint URL. Returns an error if regex is configured but
477+ // the claim doesn't match.
478+ func (verifier * VerifierJWT ) extractTokenVars (claims jwt.RegisteredClaims , tokenVars map [string ]any ) error {
468479 if verifier .issuerRe != nil {
469480 match := verifier .issuerRe .FindStringSubmatch (claims .Issuer )
470481 if len (match ) == 0 {
471- return fmt . Errorf ( "%w: issuer not matched", ErrInvalidToken )
482+ return errors . New ( " issuer not matched" )
472483 }
473484 for i , name := range verifier .issuerRe .SubexpNames () {
474485 if i != 0 && name != "" {
@@ -493,13 +504,27 @@ func (verifier *VerifierJWT) validateClaims(claims jwt.RegisteredClaims, tokenVa
493504 break
494505 }
495506 if ! matched {
496- return fmt . Errorf ( "%w: audience not matched", ErrInvalidToken )
507+ return errors . New ( " audience not matched" )
497508 }
498509 }
499510
500511 return nil
501512}
502513
514+ // validateClaims checks issuer and audience claims against configured values.
515+ // Must be called after signature verification.
516+ func (verifier * VerifierJWT ) validateClaims (claims jwt.RegisteredClaims ) error {
517+ if verifier .audience != "" && ! claims .IsForAudience (verifier .audience ) {
518+ return errors .New ("invalid audience" )
519+ }
520+
521+ if verifier .issuer != "" && ! claims .IsIssuer (verifier .issuer ) {
522+ return errors .New ("invalid issuer" )
523+ }
524+
525+ return nil
526+ }
527+
503528func (verifier * VerifierJWT ) VerifyConnectToken (t string , skipVerify bool ) (ConnectToken , error ) {
504529 token , err := jwt .ParseNoVerify ([]byte (t )) // Will be verified later.
505530 if err != nil {
@@ -513,8 +538,9 @@ func (verifier *VerifierJWT) VerifyConnectToken(t string, skipVerify bool) (Conn
513538
514539 tokenVars := map [string ]any {}
515540
516- if err := verifier .validateClaims (claims .RegisteredClaims , tokenVars ); err != nil {
517- return ConnectToken {}, err
541+ // Extract token vars before signature verification — needed to construct JWKS URL.
542+ if err = verifier .extractTokenVars (claims .RegisteredClaims , tokenVars ); err != nil {
543+ return ConnectToken {}, fmt .Errorf ("%w: %v" , ErrInvalidToken , err )
518544 }
519545
520546 if ! skipVerify {
@@ -528,6 +554,11 @@ func (verifier *VerifierJWT) VerifyConnectToken(t string, skipVerify bool) (Conn
528554 }
529555 }
530556
557+ // Validate claims after signature verification.
558+ if err = verifier .validateClaims (claims .RegisteredClaims ); err != nil {
559+ return ConnectToken {}, fmt .Errorf ("%w: %v" , ErrInvalidToken , err )
560+ }
561+
531562 if claims .Channel != "" {
532563 return ConnectToken {}, fmt .Errorf (
533564 "%w: connection JWT can not contain channel claim, only subscription JWT can" , ErrInvalidToken )
@@ -679,8 +710,9 @@ func (verifier *VerifierJWT) VerifySubscribeToken(t string, skipVerify bool) (Su
679710
680711 tokenVars := map [string ]any {}
681712
682- if err := verifier .validateClaims (claims .RegisteredClaims , tokenVars ); err != nil {
683- return SubscribeToken {}, err
713+ // Extract token vars before signature verification — needed to construct JWKS URL.
714+ if err = verifier .extractTokenVars (claims .RegisteredClaims , tokenVars ); err != nil {
715+ return SubscribeToken {}, fmt .Errorf ("%w: %v" , ErrInvalidToken , err )
684716 }
685717
686718 if ! skipVerify {
@@ -694,6 +726,11 @@ func (verifier *VerifierJWT) VerifySubscribeToken(t string, skipVerify bool) (Su
694726 }
695727 }
696728
729+ // Validate claims after signature verification.
730+ if err = verifier .validateClaims (claims .RegisteredClaims ); err != nil {
731+ return SubscribeToken {}, fmt .Errorf ("%w: %v" , ErrInvalidToken , err )
732+ }
733+
697734 now := time .Now ()
698735 if ! claims .IsValidExpiresAt (now ) || ! claims .IsValidNotBefore (now ) {
699736 return SubscribeToken {}, ErrTokenExpired
0 commit comments