Add optional Authentik (OIDC) sign-in with server-side preference sync
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.
This commit is contained in:
55
internal/web/auth_test.go
Normal file
55
internal/web/auth_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user