VerifyToken validates an e2a-minted JWT: RS256 signature against the local public key, iss == issuer, aud contains issuer, not expired (with skew), and typ == expectedType. On success returns the typed claims; every failure wraps ErrTokenInvalid. assertion_version freshness is the caller's job (it n
(token, expectedType, issuer string)
| 106 | // ErrTokenInvalid. assertion_version freshness is the caller's job (it needs the |
| 107 | // live agent row). |
| 108 | func (s *Signer) VerifyToken(token, expectedType, issuer string) (*VerifiedClaims, error) { |
| 109 | if !s.Enabled() { |
| 110 | return nil, ErrSigningDisabled |
| 111 | } |
| 112 | parsed, err := jwt.ParseSigned(token) |
| 113 | if err != nil { |
| 114 | return nil, fmt.Errorf("%w: parse: %v", ErrTokenInvalid, err) |
| 115 | } |
| 116 | var std jwt.Claims |
| 117 | var priv tokenPrivateClaims |
| 118 | if err := parsed.Claims(s.priv.Public(), &std, &priv); err != nil { |
| 119 | // Signature mismatch or malformed — invalid. |
| 120 | return nil, fmt.Errorf("%w: signature: %v", ErrTokenInvalid, err) |
| 121 | } |
| 122 | if err := std.ValidateWithLeeway(jwt.Expected{ |
| 123 | Issuer: issuer, |
| 124 | Audience: jwt.Audience{issuer}, |
| 125 | Time: time.Now(), |
| 126 | }, clockSkew); err != nil { |
| 127 | return nil, fmt.Errorf("%w: claims: %v", ErrTokenInvalid, err) |
| 128 | } |
| 129 | if priv.Type != expectedType { |
| 130 | return nil, fmt.Errorf("%w: typ %q, want %q", ErrTokenInvalid, priv.Type, expectedType) |
| 131 | } |
| 132 | if std.Subject == "" { |
| 133 | return nil, fmt.Errorf("%w: missing sub", ErrTokenInvalid) |
| 134 | } |
| 135 | var exp time.Time |
| 136 | if std.Expiry != nil { |
| 137 | exp = std.Expiry.Time() |
| 138 | } |
| 139 | return &VerifiedClaims{ |
| 140 | Subject: std.Subject, |
| 141 | Scope: priv.Scope, |
| 142 | AssertionVersion: priv.AssertionVersion, |
| 143 | Type: priv.Type, |
| 144 | Expiry: exp, |
| 145 | }, nil |
| 146 | } |
| 147 | |
| 148 | // generateJTI returns a unique token id. crypto/rand failure panics (an |
| 149 | // all-zero jti would collide across tokens) — same posture as the key RNG. |