Align session codec max-age with the 7-day cap (closes #108)
All checks were successful
check / check (push) Superseded by a newer commit; never tested
All checks were successful
check / check (push) Superseded by a newer commit; never tested
This commit was merged in pull request #132.
This commit is contained in:
150
internal/session/codec_test.go
Normal file
150
internal/session/codec_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package session_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/webhooker/internal/session"
|
||||
)
|
||||
|
||||
// The tests below exercise the securecookie codecs underneath the
|
||||
// store and nothing else: Session.Get only decodes, so no server-side
|
||||
// expiry check takes part in the result. They exist because
|
||||
// NewCookieStore gives its codecs a 30-day max age that assigning
|
||||
// store.Options does not override, which would let the codec accept a
|
||||
// cookie weeks past the cap the cookie attribute advertises.
|
||||
|
||||
// issuedCookie returns a session cookie the store itself wrote.
|
||||
func issuedCookie(t *testing.T, s *session.Session) string {
|
||||
t.Helper()
|
||||
|
||||
req := httptest.NewRequestWithContext(
|
||||
context.Background(), http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
sess, err := s.Get(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
sess.Values["probe"] = "value"
|
||||
require.NoError(t, s.Save(req, w, sess))
|
||||
|
||||
cookies := w.Result().Cookies()
|
||||
require.Len(t, cookies, 1)
|
||||
|
||||
return cookies[0].Value
|
||||
}
|
||||
|
||||
// restamp rewrites the timestamp inside an encoded session cookie and
|
||||
// re-signs it, yielding the cookie the store would have written at
|
||||
// that instant. securecookie stamps the encoding time itself and
|
||||
// exposes no seam to move it, so its wire format is reproduced here:
|
||||
// the base64url payload is "date|value|mac", where mac is HMAC-SHA256
|
||||
// of "name|date|value" under the store's key.
|
||||
func restamp(
|
||||
t *testing.T,
|
||||
encoded string,
|
||||
at time.Time,
|
||||
) string {
|
||||
t.Helper()
|
||||
|
||||
raw, err := base64.URLEncoding.DecodeString(encoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
parts := strings.SplitN(string(raw), "|", 3)
|
||||
require.Len(t, parts, 3)
|
||||
|
||||
stamped := fmt.Sprintf("%d|%s", at.Unix(), parts[1])
|
||||
|
||||
mac := hmac.New(sha256.New, testKey())
|
||||
_, err = mac.Write([]byte(session.SessionName + "|" + stamped))
|
||||
require.NoError(t, err)
|
||||
|
||||
payload := append([]byte(stamped+"|"), mac.Sum(nil)...)
|
||||
|
||||
return base64.URLEncoding.EncodeToString(payload)
|
||||
}
|
||||
|
||||
// decodeCookie feeds value back through the store's decode path.
|
||||
func decodeCookie(
|
||||
t *testing.T,
|
||||
s *session.Session,
|
||||
value string,
|
||||
) (*sessions.Session, error) {
|
||||
t.Helper()
|
||||
|
||||
req := httptest.NewRequestWithContext(
|
||||
context.Background(), http.MethodGet, "/", nil)
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: session.SessionName,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
sess, err := s.Get(req)
|
||||
require.NotNil(t, sess)
|
||||
|
||||
return sess, err
|
||||
}
|
||||
|
||||
func TestCodec_AcceptsCookieInsideAbsoluteCap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := testSession(t)
|
||||
|
||||
sess, err := decodeCookie(t, s, restamp(
|
||||
t,
|
||||
issuedCookie(t, s),
|
||||
time.Now().Add(-(testAbsoluteMaxAge-time.Hour)),
|
||||
))
|
||||
require.NoError(t, err)
|
||||
assert.False(
|
||||
t, sess.IsNew,
|
||||
"a cookie inside the cap must still decode",
|
||||
)
|
||||
assert.Equal(
|
||||
t, "value", sess.Values["probe"],
|
||||
"decoding must yield the values that were saved",
|
||||
)
|
||||
}
|
||||
|
||||
func TestCodec_RejectsCookiePastAbsoluteCap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := testSession(t)
|
||||
|
||||
sess, err := decodeCookie(t, s, restamp(
|
||||
t,
|
||||
issuedCookie(t, s),
|
||||
time.Now().Add(-(testAbsoluteMaxAge+time.Hour)),
|
||||
))
|
||||
require.Error(
|
||||
t, err,
|
||||
"the codec must refuse a cookie older than the cap",
|
||||
)
|
||||
assert.Contains(
|
||||
t, err.Error(), "expired timestamp",
|
||||
"rejection must come from the codec's age check",
|
||||
)
|
||||
assert.True(
|
||||
t, sess.IsNew,
|
||||
"a cookie past the cap must not populate a session",
|
||||
)
|
||||
assert.Nil(
|
||||
t, sess.Values["probe"],
|
||||
"a cookie past the cap must not yield its values",
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user