- 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
100 lines
3.4 KiB
Go
100 lines
3.4 KiB
Go
package bouncer
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// TestAuthTimeSmoke is the tracer for the auth_time claim: a sign-in mint
|
|
// stamps it, a refresh carries it over unchanged while renewing iat, a token
|
|
// without it refreshes into one without it, and VerifyAuthTime keeps the
|
|
// signature, audience and expiry checks of VerifyClaims.
|
|
func TestAuthTimeSmoke(t *testing.T) {
|
|
const iss = "https://app.test/_user/api/v1/login"
|
|
const refreshIss = "https://app.test/_user/api/v1/refresh"
|
|
|
|
// 1. A sign-in mint carries auth_time equal to iat.
|
|
signedIn, _, err := Mint(secret, "42", iss, time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sub, authTime, ok, err := VerifyAuthTime(signedIn, secret, AudienceUser)
|
|
if err != nil || !ok || sub != "42" {
|
|
t.Fatalf("VerifyAuthTime(minted) = %q, %v, %v, %v", sub, authTime, ok, err)
|
|
}
|
|
iat := claimUnix(t, decodeClaims(t, signedIn, secret), "iat")
|
|
if authTime.Unix() != iat.Unix() {
|
|
t.Fatalf("auth_time %d != iat %d", authTime.Unix(), iat.Unix())
|
|
}
|
|
if _, _, ok, err := VerifyAuthTime(mustMintBackend(t), secret, AudienceBackend); err != nil || !ok {
|
|
t.Fatalf("backend mint: ok=%v err=%v", ok, err)
|
|
}
|
|
|
|
// 2. A token signed in an hour ago refreshes into a token with a new
|
|
// iat and the original auth_time.
|
|
past := time.Now().Add(-time.Hour)
|
|
old, _, err := mintAudience(secret, "42", iss, 30*time.Minute, AudienceUser, past, past)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
next, err := Refresh(secret, old, 2*time.Hour, NewMemoryBlacklist(), 0, refreshIss)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, nextAuth, ok, err := VerifyAuthTime(next, secret, AudienceUser)
|
|
if err != nil || !ok {
|
|
t.Fatalf("VerifyAuthTime(refreshed): ok=%v err=%v", ok, err)
|
|
}
|
|
if nextAuth.Unix() != past.Unix() {
|
|
t.Fatalf("refreshed auth_time = %d, want %d", nextAuth.Unix(), past.Unix())
|
|
}
|
|
if nextIat := claimUnix(t, decodeClaims(t, next, secret), "iat"); !nextIat.After(past) {
|
|
t.Fatalf("refreshed iat %s not after %s", nextIat, past)
|
|
}
|
|
|
|
// 3. A token without auth_time refreshes into a token without it.
|
|
legacy := sign(t, jwt.SigningMethodHS256, jwt.MapClaims{
|
|
"sub": "42",
|
|
"aud": AudienceUser,
|
|
"jti": "legacy",
|
|
"iat": time.Now().Add(-10 * time.Minute).Unix(),
|
|
"exp": time.Now().Add(-time.Minute).Unix(),
|
|
}, []byte(secret))
|
|
rotated, err := Refresh(secret, legacy, time.Hour, NewMemoryBlacklist(), 0, refreshIss)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, ok, err := VerifyAuthTime(rotated, secret, AudienceUser); err != nil || ok {
|
|
t.Fatalf("legacy refresh: ok=%v err=%v, want ok=false and no error", ok, err)
|
|
}
|
|
if _, has := decodeClaims(t, rotated, secret)[ClaimAuthTime]; has {
|
|
t.Fatal("legacy refresh stamped auth_time")
|
|
}
|
|
|
|
// 4. Wrong audience, bad signature and past exp are errors.
|
|
if _, _, _, err := VerifyAuthTime(signedIn, secret, AudienceBackend); err == nil {
|
|
t.Fatal("frontend token accepted for the backend audience")
|
|
}
|
|
if _, _, _, err := VerifyAuthTime(signedIn, "other-secret", AudienceUser); err == nil {
|
|
t.Fatal("token accepted with a different secret")
|
|
}
|
|
expired, _, err := mintAudience(secret, "42", iss, time.Minute, AudienceUser, past, past)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, _, err := VerifyAuthTime(expired, secret, AudienceUser); err == nil {
|
|
t.Fatal("expired token accepted")
|
|
}
|
|
}
|
|
|
|
func mustMintBackend(t *testing.T) string {
|
|
t.Helper()
|
|
tok, _, err := MintAudience(secret, "7", "https://app.test/backend/login", time.Hour, AudienceBackend)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return tok
|
|
}
|