Signed-in users get their preferences (hidden feeds, weather location, weather toggle) stored server-side keyed by their OIDC subject and synced across devices. Anonymous visitors keep using browser localStorage, so the site stays public. First sign-in migrates existing localStorage prefs up. - config: [web.auth] section (issuer, client_id/secret, redirect, session_secret) - storage: user_preferences table + Get/PutUserPrefs - web/auth: OIDC code flow, HMAC-signed session cookie, CSRF state + nonce - web/prefs_api: GET/PUT /api/preferences (auth-gated, 64KB cap) - frontend: prefs.js sync layer seeds localStorage from server, pushes on write - header: sign-in / account control OIDC discovery is non-fatal at boot: if Authentik is down, Pete serves anonymously rather than refusing to start.
56 lines
1.5 KiB
Go
56 lines
1.5 KiB
Go
package web
|
|
|
|
import "testing"
|
|
|
|
func TestSignVerifyRoundTrip(t *testing.T) {
|
|
a := &Authenticator{secret: []byte("test-secret-key-at-least-16")}
|
|
payload := []byte(`{"sub":"abc","exp":123}`)
|
|
tok := a.sign(payload)
|
|
|
|
got, ok := a.verify(tok)
|
|
if !ok {
|
|
t.Fatal("verify rejected a freshly signed token")
|
|
}
|
|
if string(got) != string(payload) {
|
|
t.Fatalf("payload mismatch: got %q want %q", got, payload)
|
|
}
|
|
}
|
|
|
|
func TestVerifyRejectsTamperAndForeignKey(t *testing.T) {
|
|
a := &Authenticator{secret: []byte("test-secret-key-at-least-16")}
|
|
tok := a.sign([]byte(`{"sub":"abc"}`))
|
|
|
|
// Flip a byte in the payload segment.
|
|
bad := []byte(tok)
|
|
bad[0] ^= 0x01
|
|
if _, ok := a.verify(string(bad)); ok {
|
|
t.Error("verify accepted a tampered token")
|
|
}
|
|
|
|
// A different key must not validate the same token.
|
|
other := &Authenticator{secret: []byte("a-totally-different-key-16")}
|
|
if _, ok := other.verify(tok); ok {
|
|
t.Error("verify accepted a token signed with a different key")
|
|
}
|
|
|
|
if _, ok := a.verify("not-a-valid-token"); ok {
|
|
t.Error("verify accepted a malformed token")
|
|
}
|
|
}
|
|
|
|
func TestSafeNext(t *testing.T) {
|
|
cases := map[string]string{
|
|
"": "/",
|
|
"/tech": "/tech",
|
|
"/tech?page=2": "/tech?page=2",
|
|
"//evil.com": "/", // protocol-relative open redirect
|
|
"https://evil.com": "/", // absolute URL
|
|
"javascript:alert": "/", // not a path
|
|
}
|
|
for in, want := range cases {
|
|
if got := safeNext(in); got != want {
|
|
t.Errorf("safeNext(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|