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 }