Files
go-usertoken/verify.go
T
konradandCursor f082561cc6 Mint and verify short-lived RS256 user tokens.
ExApps sign after AppAPI auth; Microservices check a static public key or an OIDC issuer and forward the same bearer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 14:03:52 +02:00

85 lines
2.1 KiB
Go

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
}