package usertoken import ( "context" "crypto/rsa" "errors" "fmt" "strings" "time" "github.com/golang-jwt/jwt/v5" ) const defaultSkew = time.Minute type tokenVerifier interface { verify(ctx context.Context, raw string) (Caller, error) } type staticVerifier struct { key *rsa.PublicKey iss string aud string skew time.Duration } func newStaticVerifier(key *rsa.PublicKey, iss, aud string, skew time.Duration) (tokenVerifier, error) { if key == nil { return nil, fmt.Errorf("public key is nil") } iss = strings.TrimSpace(iss) aud = strings.TrimSpace(aud) if iss == "" || aud == "" { return nil, fmt.Errorf("issuer and audience are required") } if skew <= 0 { skew = defaultSkew } return &staticVerifier{key: key, iss: iss, aud: aud, skew: skew}, nil } func (v *staticVerifier) verify(_ context.Context, raw string) (Caller, error) { if strings.TrimSpace(raw) == "" { return Caller{}, unauthorized("empty token") } parser := jwt.NewParser( jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}), jwt.WithIssuer(v.iss), jwt.WithAudience(v.aud), jwt.WithLeeway(v.skew), jwt.WithExpirationRequired(), jwt.WithIssuedAt(), ) var claims tokenClaims tok, err := parser.ParseWithClaims(raw, &claims, func(t *jwt.Token) (any, error) { if t.Method == nil || t.Method.Alg() != jwt.SigningMethodRS256.Alg() { return nil, unauthorized("unexpected alg") } return v.key, nil }) if err != nil || tok == nil || !tok.Valid { if err == nil { err = errors.New("invalid token") } return Caller{}, fmt.Errorf("verify: %w", errors.Join(err, ErrUnauthorized)) } return callerFromClaims(claims.Subject, claims.PreferredUsername, claims.Groups) } func callerFromClaims(subject, username string, groups *[]string) (Caller, error) { subject = strings.TrimSpace(subject) if subject == "" { return Caller{}, unauthorized("empty sub") } username = strings.TrimSpace(username) if username == "" { username = subject } c := Caller{Subject: subject, Username: username} if groups != nil { c.Groups = append([]string{}, (*groups)...) } return c, nil }