package bouncer import ( "crypto/rand" "encoding/hex" "fmt" "strings" "time" "github.com/golang-jwt/jwt/v5" ) // prvHash is sha1("Golem15\User\Models\User"), the lock-subject PHP jwt-auth // stamps on every frontend user token. Backend tokens do not carry it. const prvHash = "a867434cbc213adfbe78a02bed7082a6bd99c883" const ( // AudienceUser is the frontend jwt-guard audience. AudienceUser = "user" // AudienceBackend is the admin jwt-guard audience. AudienceBackend = "backend" ) type registeredClaims struct { jwt.RegisteredClaims Prv string `json:"prv,omitempty"` } // Mint signs a frontend-audience HS256 token. iss is the minting endpoint URL. // The returned jti is the token's own jti claim. func Mint(secret, sub, issuerURL string, ttl time.Duration) (string, string, error) { return MintAudience(secret, sub, issuerURL, ttl, AudienceUser) } // MintAudience signs an HS256 token for audience. Frontend tokens keep the PHP // prv lock-subject; backend tokens omit it. func MintAudience(secret, sub, issuerURL string, ttl time.Duration, audience string) (string, string, error) { if strings.TrimSpace(secret) == "" { return "", "", fmt.Errorf("bouncer: jwt secret is empty") } if strings.TrimSpace(audience) == "" { return "", "", fmt.Errorf("bouncer: jwt audience is empty") } jti, err := newJTI() if err != nil { return "", "", err } now := time.Now() prv := "" if audience == AudienceUser { prv = prvHash } claims := registeredClaims{ RegisteredClaims: jwt.RegisteredClaims{ Issuer: issuerURL, Subject: sub, Audience: jwt.ClaimStrings{audience}, ExpiresAt: jwt.NewNumericDate(now.Add(ttl)), NotBefore: jwt.NewNumericDate(now), IssuedAt: jwt.NewNumericDate(now), ID: jti, }, Prv: prv, } signed, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret)) if err != nil { return "", "", err } return signed, jti, nil } func newJTI() (string, error) { buf := make([]byte, 16) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil }