2026-07-24 21:20:04 +02:00
|
|
|
package auth
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const testSecret = "test-secret-at-least-32-bytes-long!"
|
|
|
|
|
|
|
|
|
|
func TestMintAndParseSessionCookie(t *testing.T) {
|
2026-08-04 17:44:37 +02:00
|
|
|
claims := Claims{Sub: "u1", Name: "Alice", Email: "alice@example.com"}
|
|
|
|
|
cookie, err := MintSessionCookie(claims, "raw-id-token-jwt", []byte(testSecret), time.Hour, true)
|
2026-07-24 21:20:04 +02:00
|
|
|
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)
|
|
|
|
|
}
|
2026-08-04 17:44:37 +02:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
2026-07-24 21:20:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestParseSessionCookie_Expired(t *testing.T) {
|
2026-08-04 17:44:37 +02:00
|
|
|
cookie, err := MintSessionCookie(Claims{Sub: "u1"}, "", []byte(testSecret), -time.Hour, false)
|
2026-07-24 21:20:04 +02:00
|
|
|
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) {
|
2026-08-04 17:44:37 +02:00
|
|
|
cookie, err := MintSessionCookie(Claims{Sub: "u1"}, "", []byte(testSecret), time.Hour, false)
|
2026-07-24 21:20:04 +02:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("mint: %v", err)
|
|
|
|
|
}
|
2026-07-27 10:59:49 +02:00
|
|
|
cookie.Value = flipSignatureChar(cookie.Value)
|
2026-07-24 21:20:04 +02:00
|
|
|
if _, err := ParseSessionCookie(cookie, []byte(testSecret)); err == nil {
|
|
|
|
|
t.Fatal("expected error for tampered cookie")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 10:59:49 +02:00
|
|
|
// 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]
|
2026-07-25 19:33:15 +02:00
|
|
|
replacement := byte('x')
|
2026-07-27 10:59:49 +02:00
|
|
|
if orig == replacement {
|
2026-07-25 19:33:15 +02:00
|
|
|
replacement = 'y'
|
|
|
|
|
}
|
2026-07-27 10:59:49 +02:00
|
|
|
return s[:pos] + string(replacement) + s[pos+1:]
|
2026-07-25 19:33:15 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-24 21:20:04 +02:00
|
|
|
func TestParseSessionCookie_WrongSecret(t *testing.T) {
|
2026-08-04 17:44:37 +02:00
|
|
|
cookie, err := MintSessionCookie(Claims{Sub: "u1"}, "", []byte(testSecret), time.Hour, false)
|
2026-07-24 21:20:04 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|