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>
85 lines
2.1 KiB
Go
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
|
|
}
|