package auth import ( "testing" "time" ) const testSecret = "test-secret-at-least-32-bytes-long!" func TestMintAndParseSessionCookie(t *testing.T) { claims := Claims{Sub: "u1", Name: "Alice", Email: "alice@example.com"} cookie, err := MintSessionCookie(claims, "raw-id-token-jwt", []byte(testSecret), time.Hour, true) if err != nil { t.Fatalf("mint: %v", err) } if cookie.Name != SessionCookieName || !cookie.HttpOnly || !cookie.Secure { t.Fatalf("cookie = %+v, want name=%s HttpOnly+Secure", cookie, SessionCookieName) } got, err := ParseSessionCookie(cookie, []byte(testSecret)) if err != nil { t.Fatalf("parse: %v", err) } if got != claims { t.Fatalf("got %+v, want %+v", got, claims) } // The ID token rides in the cookie apart from Claims, retrievable only // through the dedicated logout-path helper. idToken, err := IDTokenFromSessionCookie(cookie, []byte(testSecret)) if err != nil { t.Fatalf("IDTokenFromSessionCookie: %v", err) } if idToken != "raw-id-token-jwt" { t.Fatalf("idToken = %q, want raw-id-token-jwt", idToken) } } func TestParseSessionCookie_Expired(t *testing.T) { cookie, err := MintSessionCookie(Claims{Sub: "u1"}, "", []byte(testSecret), -time.Hour, false) if err != nil { t.Fatalf("mint: %v", err) } if _, err := ParseSessionCookie(cookie, []byte(testSecret)); err == nil { t.Fatal("expected error for expired cookie") } } func TestParseSessionCookie_Tampered(t *testing.T) { cookie, err := MintSessionCookie(Claims{Sub: "u1"}, "", []byte(testSecret), time.Hour, false) if err != nil { t.Fatalf("mint: %v", err) } cookie.Value = flipSignatureChar(cookie.Value) if _, err := ParseSessionCookie(cookie, []byte(testSecret)); err == nil { t.Fatal("expected error for tampered cookie") } } // flipSignatureChar corrupts a signed JWT for tamper tests by changing the // second-to-last character of its base64url signature, guaranteeing the // decoded signature bytes actually change. Two pitfalls to avoid here: // 1. Blindly overwriting a character with a fixed replacement (e.g. "x") // would occasionally be a no-op if that character was already there -- // it's derived from the token's embedded timestamp, so this isn't as // rare as it sounds. // 2. Flipping the *last* character of the signature specifically (as this // helper used to) is flaky in a subtler way: HMAC-SHA256 produces a // 32-byte digest, which base64url-encodes to 43 characters with a // final 3-character group covering only a 2-byte remainder -- the // true last character encodes 4 real bits plus 2 unused padding bits. // Go's encoding/base64 ignores those padding bits when decoding // (non-strict by default), so about 1 in 4 replacement characters for // that position decode to byte-identical signature bytes, silently // passing the test without having tampered with anything. The // second-to-last character of that final group has no such unused // bits, so corrupting it is deterministic. func flipSignatureChar(s string) string { pos := len(s) - 2 orig := s[pos] replacement := byte('x') if orig == replacement { replacement = 'y' } return s[:pos] + string(replacement) + s[pos+1:] } func TestParseSessionCookie_WrongSecret(t *testing.T) { cookie, err := MintSessionCookie(Claims{Sub: "u1"}, "", []byte(testSecret), time.Hour, false) if err != nil { t.Fatalf("mint: %v", err) } if _, err := ParseSessionCookie(cookie, []byte("a-completely-different-secret!!")); err == nil { t.Fatal("expected error for wrong secret") } } func TestMintAndParseTxnCookie(t *testing.T) { txn := TxnState{State: "abc123", CodeVerifier: "verifier-xyz"} cookie, err := MintTxnCookie(txn, []byte(testSecret), false) if err != nil { t.Fatalf("mint: %v", err) } if cookie.Name != TxnCookieName { t.Fatalf("cookie name = %q, want %q", cookie.Name, TxnCookieName) } got, err := ParseTxnCookie(cookie, []byte(testSecret)) if err != nil { t.Fatalf("parse: %v", err) } if got != txn { t.Fatalf("got %+v, want %+v", got, txn) } } func TestClearCookie(t *testing.T) { c := ClearCookie(SessionCookieName, true) if c.Value != "" || c.MaxAge >= 0 { t.Fatalf("ClearCookie = %+v, want empty value and negative MaxAge", c) } }