Files
2026-10-19 16:04:17 +02:00

141 lines
3.7 KiB
Go

package auth
import (
"net/http"
"strings"
"testing"
"time"
)
func TestSignerSealOpen(t *testing.T) {
s := NewSigner("test-secret")
tx, err := NewLoginTx("/some/path")
if err != nil {
t.Fatalf("NewLoginTx: %v", err)
}
sealed, err := s.Seal(tx)
if err != nil {
t.Fatalf("Seal: %v", err)
}
var opened LoginTx
if err := s.Open(sealed, &opened); err != nil {
t.Fatalf("Open: %v", err)
}
// Compare fields: time.Time equality also compares the monotonic clock,
// which the JSON roundtrip drops.
if opened.State != tx.State || opened.Nonce != tx.Nonce ||
opened.Verifier != tx.Verifier || opened.ReturnTo != tx.ReturnTo ||
!opened.ExpiresAt.Equal(tx.ExpiresAt) {
t.Errorf("opened = %+v, want %+v", opened, tx)
}
}
func TestSignerRejectsTampering(t *testing.T) {
s := NewSigner("test-secret")
sess := Session{Sub: "user-1", ExpiresAt: time.Now().Add(time.Hour)}
sealed, err := s.Seal(sess)
if err != nil {
t.Fatalf("Seal: %v", err)
}
tests := []struct {
name string
value func(string) string
reason string
}{
{"flipped payload", func(v string) string {
return flipLastChar(strings.Split(v, ".")[0]) + "." + strings.Split(v, ".")[1]
}, "forged payload"},
{"flipped mac", func(v string) string {
return strings.Split(v, ".")[0] + "." + flipLastChar(strings.Split(v, ".")[1])
}, "forged mac"},
{"wrong key", func(v string) string {
other, _ := NewSigner("other-secret").Seal(sess)
return other
}, "signed with a different key"},
{"garbage", func(string) string { return "not-a-sealed-value" }, "garbage"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var out Session
if err := s.Open(tt.value(sealed), &out); err == nil {
t.Errorf("Open accepted %s", tt.reason)
}
})
}
}
func TestSessionAndTxCookies(t *testing.T) {
s := NewSigner("test-secret")
sessCookie, err := s.NewSessionCookie(Session{Sub: "u1"}, 2*time.Hour, true)
if err != nil {
t.Fatalf("NewSessionCookie: %v", err)
}
if sessCookie.Name != CookieSession || sessCookie.Path != "/" {
t.Errorf("session cookie = %+v", sessCookie)
}
if !sessCookie.HttpOnly || !sessCookie.Secure || sessCookie.SameSite != http.SameSiteLaxMode {
t.Errorf("session cookie attrs = %+v", sessCookie)
}
var sess Session
if err := s.Open(sessCookie.Value, &sess); err != nil {
t.Fatalf("Open session: %v", err)
}
if !sess.Valid() {
t.Error("session with future ExpiresAt should be valid")
}
sess.ExpiresAt = time.Now().Add(-time.Minute)
if sess.Valid() {
t.Error("expired session should be invalid")
}
tx, err := NewLoginTx("/")
if err != nil {
t.Fatalf("NewLoginTx: %v", err)
}
if !tx.Valid() {
t.Error("fresh login tx should be valid")
}
tx.ExpiresAt = time.Now().Add(-time.Second)
if tx.Valid() {
t.Error("expired login tx should be invalid")
}
tx, _ = NewLoginTx("/")
txCookie, err := s.NewLoginTxCookie(tx, false)
if err != nil {
t.Fatalf("NewLoginTxCookie: %v", err)
}
if txCookie.Name != CookieLoginTx || txCookie.Path != "/api/auth" {
t.Errorf("tx cookie = %+v", txCookie)
}
if !txCookie.HttpOnly || txCookie.Secure {
t.Errorf("tx cookie attrs = %+v", txCookie)
}
expired := Expired(CookieSession, true)
if expired.Name != CookieSession || expired.MaxAge != -1 {
t.Errorf("Expired cookie = %+v", expired)
}
}
func TestNewLoginTxUsesDistinctSecrets(t *testing.T) {
a, _ := NewLoginTx("/")
b, _ := NewLoginTx("/")
if a.State == b.State || a.Nonce == b.Nonce || a.Verifier == b.Verifier {
t.Error("state/nonce/verifier must be fresh per login")
}
}
// flipLastChar swaps the last character of a base64url value so any
// signature or payload check must fail.
func flipLastChar(v string) string {
last := "A"
if strings.HasSuffix(v, last) {
last = "B"
}
return v[:len(v)-1] + last
}