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>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user