@@ -56,6 +56,9 @@ type Manager struct {
5656 config Config
5757 parsedSignKey interface {} // cached at init: ed25519.PrivateKey or []byte (HS256)
5858 fast * fastJWTState
59+ methodAlg string
60+ accessParser * jwt.Parser
61+ accessKeyFunc jwt.Keyfunc
5962}
6063
6164// fastJWTState holds pre-computed state for zero-overhead JWT creation.
@@ -95,6 +98,10 @@ type AccessClaims struct {
9598 jwt.RegisteredClaims
9699}
97100
101+ // plainAccessClaims uses default JSON decoding (string tid only) for the fast path.
102+ // Legacy numeric tid tokens are handled by a fallback parse path.
103+ type plainAccessClaims AccessClaims
104+
98105type accessClaimsJSON struct {
99106 UID string `json:"uid"`
100107 TID json.RawMessage `json:"tid,omitempty"`
@@ -186,6 +193,9 @@ func NewManager(cfg Config) (*Manager, error) {
186193 }
187194
188195 m := & Manager {config : cfg }
196+ m .methodAlg = m .getMethod ().Alg ()
197+ m .accessParser = jwt .NewParser (buildAccessParserOptions (cfg , m .methodAlg )... )
198+ m .accessKeyFunc = m .accessTokenKeyFunc
189199
190200 // Pre-parse and cache the signing key to avoid per-call parsing.
191201 switch cfg .SigningMethod {
@@ -418,71 +428,106 @@ func (j *Manager) createAccessLegacy(
418428// Performance: single signature verify + claim decode.
419429// Docs: docs/jwt.md
420430func (j * Manager ) ParseAccess (tokenStr string ) (* AccessClaims , error ) {
421- options := []jwt.ParserOption {
422- jwt .WithValidMethods ([]string {j .getMethod ().Alg ()}),
431+ plain := & plainAccessClaims {}
432+ token , err := j .accessParser .ParseWithClaims (tokenStr , plain , j .accessKeyFunc )
433+ if err == nil {
434+ claims := (* AccessClaims )(plain )
435+ if ! token .Valid {
436+ return nil , jwt .ErrTokenInvalidClaims
437+ }
438+ if err := j .validateParsedAccessClaims (claims ); err != nil {
439+ return nil , err
440+ }
441+ return claims , nil
423442 }
424- if j .config .Leeway > 0 {
425- options = append (options , jwt .WithLeeway (j .config .Leeway ))
443+
444+ if ! isLegacyTenantTypeError (err ) {
445+ return nil , err
426446 }
427- if j .config .RequireIAT {
447+
448+ legacyToken , legacyErr := j .accessParser .ParseWithClaims (tokenStr , & AccessClaims {}, j .accessKeyFunc )
449+ if legacyErr != nil {
450+ return nil , legacyErr
451+ }
452+
453+ claims , ok := legacyToken .Claims .(* AccessClaims )
454+ if ! ok || ! legacyToken .Valid {
455+ return nil , jwt .ErrTokenInvalidClaims
456+ }
457+ if err := j .validateParsedAccessClaims (claims ); err != nil {
458+ return nil , err
459+ }
460+
461+ return claims , nil
462+ }
463+
464+ func buildAccessParserOptions (cfg Config , methodAlg string ) []jwt.ParserOption {
465+ options := make ([]jwt.ParserOption , 0 , 5 )
466+ options = append (options , jwt .WithValidMethods ([]string {methodAlg }))
467+ if cfg .Leeway > 0 {
468+ options = append (options , jwt .WithLeeway (cfg .Leeway ))
469+ }
470+ if cfg .RequireIAT {
428471 options = append (options , jwt .WithIssuedAt ())
429472 }
430- if j . config .Issuer != "" {
431- options = append (options , jwt .WithIssuer (j . config .Issuer ))
473+ if cfg .Issuer != "" {
474+ options = append (options , jwt .WithIssuer (cfg .Issuer ))
432475 }
433- if j . config .Audience != "" {
434- options = append (options , jwt .WithAudience (j . config .Audience ))
476+ if cfg .Audience != "" {
477+ options = append (options , jwt .WithAudience (cfg .Audience ))
435478 }
479+ return options
480+ }
436481
437- parser := jwt .NewParser (options ... )
438- token , err := parser .ParseWithClaims (tokenStr , & AccessClaims {}, func (t * jwt.Token ) (interface {}, error ) {
439- if t .Method .Alg () != j .getMethod ().Alg () {
440- return nil , fmt .Errorf ("unexpected signing algorithm: %s" , t .Method .Alg ())
441- }
482+ func (j * Manager ) accessTokenKeyFunc (t * jwt.Token ) (interface {}, error ) {
483+ if t .Method .Alg () != j .methodAlg {
484+ return nil , fmt .Errorf ("unexpected signing algorithm: %s" , t .Method .Alg ())
485+ }
442486
443- if len (j .config .VerifyKeys ) > 0 {
444- kid , _ := t .Header ["kid" ].(string )
445- if kid == "" {
446- return nil , errors .New ("missing kid" )
447- }
448- key , ok := j .config .VerifyKeys [kid ]
449- if ! ok {
450- return nil , errors .New ("unknown kid" )
451- }
452- return j .keyBytesToVerifyKey (key )
487+ if len (j .config .VerifyKeys ) > 0 {
488+ kid , _ := t .Header ["kid" ].(string )
489+ if kid == "" {
490+ return nil , errors .New ("missing kid" )
453491 }
454-
455- if j .config .KeyID != "" {
456- kid , _ := t .Header ["kid" ].(string )
457- if kid == "" {
458- return nil , errors .New ("missing kid" )
459- }
460- if kid != j .config .KeyID {
461- return nil , errors .New ("unknown kid" )
462- }
492+ key , ok := j .config .VerifyKeys [kid ]
493+ if ! ok {
494+ return nil , errors .New ("unknown kid" )
463495 }
464-
465- return j .getVerifyKey ()
466- })
467- if err != nil {
468- return nil , err
496+ return j .keyBytesToVerifyKey (key )
469497 }
470498
471- claims , ok := token .Claims .(* AccessClaims )
472- if ! ok || ! token .Valid {
473- return nil , jwt .ErrTokenInvalidClaims
499+ if j .config .KeyID != "" {
500+ kid , _ := t .Header ["kid" ].(string )
501+ if kid == "" {
502+ return nil , errors .New ("missing kid" )
503+ }
504+ if kid != j .config .KeyID {
505+ return nil , errors .New ("unknown kid" )
506+ }
474507 }
508+
509+ return j .getVerifyKey ()
510+ }
511+
512+ func (j * Manager ) validateParsedAccessClaims (claims * AccessClaims ) error {
475513 if j .config .RequireIAT && claims .IssuedAt == nil {
476- return nil , errors .New ("token missing required iat claim" )
514+ return errors .New ("token missing required iat claim" )
477515 }
478516 if claims .IssuedAt != nil && j .config .MaxFutureIAT > 0 {
479517 maxAllowed := time .Now ().Add (j .config .MaxFutureIAT )
480518 if claims .IssuedAt .Time .After (maxAllowed ) {
481- return nil , errors .New ("token iat too far in the future" )
519+ return errors .New ("token iat too far in the future" )
482520 }
483521 }
522+ return nil
523+ }
484524
485- return claims , nil
525+ func isLegacyTenantTypeError (err error ) bool {
526+ var typeErr * json.UnmarshalTypeError
527+ if ! errors .As (err , & typeErr ) {
528+ return false
529+ }
530+ return typeErr .Value == "number" && (typeErr .Field == "tid" || strings .HasSuffix (typeErr .Field , ".tid" ))
486531}
487532
488533func (j * Manager ) getMethod () jwt.SigningMethod {
0 commit comments