141 lines
3.7 KiB
Go
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
|
|
}
|