package auth import ( "context" "crypto/rsa" "testing" "time" ) func TestToken_IssueAndParseAccess(t *testing.T) { rdb, _ := newMiniRedis(t) tm := newTokenManager(t, rdb, time.Now) pair, err := tm.Issue(context.Background(), 42, "uuid-42") if err != nil { t.Fatalf("Issue: %v", err) } if pair.ExpiresIn != 900 { t.Errorf("ExpiresIn = %d, want 900", pair.ExpiresIn) } claims, err := tm.ParseAccess(pair.AccessToken) if err != nil { t.Fatalf("ParseAccess: %v", err) } if claims.UID != 42 || claims.Subject != "uuid-42" { t.Errorf("claims = %+v", claims) } if claims.Typ != typAccess { t.Errorf("typ = %q", claims.Typ) } } func TestToken_RefreshCannotBeUsedAsAccess(t *testing.T) { rdb, _ := newMiniRedis(t) tm := newTokenManager(t, rdb, time.Now) pair, _ := tm.Issue(context.Background(), 1, "u1") if _, err := tm.ParseAccess(pair.RefreshToken); err == nil { t.Fatal("refresh token must not parse as access") } } func TestToken_AccessExpiry(t *testing.T) { rdb, _ := newMiniRedis(t) base := time.Now() clock := base tm := newTokenManager(t, rdb, func() time.Time { return clock }) pair, _ := tm.Issue(context.Background(), 1, "u1") clock = base.Add(16 * time.Minute) // past 15-min access TTL if _, err := tm.ParseAccess(pair.AccessToken); err == nil { t.Fatal("expected expired access token to fail") } } func TestToken_RefreshRotationInvalidatesOld(t *testing.T) { rdb, _ := newMiniRedis(t) tm := newTokenManager(t, rdb, time.Now) ctx := context.Background() pair, _ := tm.Issue(ctx, 7, "u7") rotated, err := tm.Refresh(ctx, pair.RefreshToken) if err != nil { t.Fatalf("Refresh: %v", err) } if rotated.RefreshToken == pair.RefreshToken { t.Fatal("rotation should produce a new refresh token") } // Old refresh token is now invalid. if _, err := tm.Refresh(ctx, pair.RefreshToken); err == nil { t.Fatal("old refresh token must be rejected after rotation") } // New refresh token still works. if _, err := tm.Refresh(ctx, rotated.RefreshToken); err != nil { t.Fatalf("new refresh token should work: %v", err) } } func TestToken_RevokeRefresh(t *testing.T) { rdb, _ := newMiniRedis(t) tm := newTokenManager(t, rdb, time.Now) ctx := context.Background() pair, _ := tm.Issue(ctx, 3, "u3") claims, err := tm.parse(pair.RefreshToken, typRefresh) if err != nil { t.Fatalf("parse refresh: %v", err) } if err := tm.Revoke(ctx, claims.ID); err != nil { t.Fatalf("Revoke: %v", err) } if _, err := tm.Refresh(ctx, pair.RefreshToken); err == nil { t.Fatal("revoked refresh token must be rejected") } } func TestToken_KeyRotationAcceptsOldKey(t *testing.T) { rdb, _ := newMiniRedis(t) // Old manager signs with k1. oldKey := newRSAKey(t) oldTM, err := NewTokenManager(rdb, TokenConfig{SignKey: oldKey, SignKID: "k1"}) if err != nil { t.Fatal(err) } pair, _ := oldTM.Issue(context.Background(), 9, "u9") // New manager signs with k2 but still accepts k1 for verification. newKey := newRSAKey(t) newTM, err := NewTokenManager(rdb, TokenConfig{ SignKey: newKey, SignKID: "k2", VerifyKeys: map[string]*rsa.PublicKey{ "k1": &oldKey.PublicKey, "k2": &newKey.PublicKey, }, }) if err != nil { t.Fatal(err) } if _, err := newTM.ParseAccess(pair.AccessToken); err != nil { t.Fatalf("token signed with old key must still verify: %v", err) } }