- MintAudience stamps auth_time equal to iat; refreshAudience copies the presented token's value and never renews it (absent stays absent) - bouncer.ClaimAuthTime and bouncer.VerifyAuthTime for plugins, with the same signature, audience and expiry checks as VerifyClaims - TestAuthTimeSmoke traces mint, refresh and read
97 lines
3.0 KiB
Go
97 lines
3.0 KiB
Go
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"
|
|
)
|
|
|
|
// ClaimAuthTime is the JWT claim holding the time of the sign-in that started
|
|
// the session (OIDC Core's auth_time, NumericDate seconds). Every direct mint
|
|
// is a sign-in and stamps it equal to iat; a refresh copies it unchanged, so
|
|
// a "fresh sign-in" check compares auth_time, never iat.
|
|
const ClaimAuthTime = "auth_time"
|
|
|
|
type registeredClaims struct {
|
|
jwt.RegisteredClaims
|
|
Prv string `json:"prv,omitempty"`
|
|
AuthTime *jwt.NumericDate `json:"auth_time,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. Every MintAudience call is a
|
|
// sign-in, so the token's auth_time (ClaimAuthTime) equals its iat.
|
|
func MintAudience(secret, sub, issuerURL string, ttl time.Duration, audience string) (string, string, error) {
|
|
now := time.Now()
|
|
return mintAudience(secret, sub, issuerURL, ttl, audience, now, now)
|
|
}
|
|
|
|
// mintAudience signs the token issued at now. authTime, when non-zero,
|
|
// becomes the auth_time claim; a zero authTime mints a token without one. A
|
|
// sign-in passes the same instant as now and authTime, so auth_time equals iat.
|
|
func mintAudience(secret, sub, issuerURL string, ttl time.Duration, audience string, now, authTime time.Time) (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
|
|
}
|
|
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,
|
|
}
|
|
if !authTime.IsZero() {
|
|
claims.AuthTime = jwt.NewNumericDate(authTime)
|
|
}
|
|
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
|
|
}
|