86 lines
3.4 KiB
Go
86 lines
3.4 KiB
Go
package oidc
|
|
|
|
import (
|
|
"keycape/internal/domain"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRecoveryStripsCredentials(t *testing.T) {
|
|
h := &AuthorizeHandler{AccountPortalURL: "https://users.example"}
|
|
w := httptest.NewRecorder()
|
|
h.authenticationFailure(w, httptest.NewRequest("GET", "https://kc.example/authorize/callback?code=secret&state=secret&redirect=https://evil.example", nil))
|
|
if w.Code != 303 || w.Header().Get("Location") != "https://users.example/access-recovery" {
|
|
t.Fatal(w.Code, w.Header())
|
|
}
|
|
if w.Header().Get("Referrer-Policy") != "no-referrer" || strings.Contains(w.Body.String(), "secret") {
|
|
t.Fatal("callback leaked")
|
|
}
|
|
}
|
|
func TestSharedLogoutRequiresConfirmationAndFixedReturn(t *testing.T) {
|
|
h := &AccountLogoutHandler{PortalURL: "https://users.example", UpstreamLogoutURL: "https://auth.example/logout", Issuer: "https://kc.example", Logins: NewLoginSessionStore()}
|
|
session := h.Logins.Create("synthetic", domain.AssuranceAAL1)
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest("GET", "https://kc.example/account/logout", nil))
|
|
if _, ok := h.Logins.Get(session.ID); !ok {
|
|
t.Fatal("GET logged out")
|
|
}
|
|
if w.Code != 200 || w.Header().Get("Location") != "" {
|
|
t.Fatal("GET must only confirm")
|
|
}
|
|
page := w.Body.String()
|
|
if !strings.Contains(page, "<strong>NetKingdom Identity</strong>") || !strings.Contains(page, "<h1>Sign out of NetKingdom?</h1>") || !strings.Contains(page, "--paper:#f5f1e8") || !strings.Contains(page, "Georgia,serif") {
|
|
t.Fatal("logout page left the account-site design")
|
|
}
|
|
if !strings.Contains(w.Header().Get("Content-Security-Policy"), "style-src 'unsafe-inline'") {
|
|
t.Fatal("inline account-site style blocked")
|
|
}
|
|
if w.Header().Get("Referrer-Policy") != "same-origin" {
|
|
t.Fatal("browser POST would lose its Origin")
|
|
}
|
|
if !strings.Contains(w.Header().Get("Content-Security-Policy"), "form-action 'self' https://auth.example https://users.example;") {
|
|
t.Fatal("browser must follow registered sign-out destinations")
|
|
}
|
|
c := w.Result().Cookies()[0]
|
|
if !c.Secure || !c.HttpOnly || c.Domain != "" {
|
|
t.Fatal("unsafe CSRF cookie")
|
|
}
|
|
for _, origin := range []string{"", "https://evil.example", "https://kc.example"} {
|
|
form := url.Values{"csrf": {c.Value}, "rd": {"https://evil.example"}}
|
|
r := httptest.NewRequest("POST", "https://kc.example/account/logout", strings.NewReader(form.Encode()))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.Header.Set("Origin", origin)
|
|
r.AddCookie(c)
|
|
r.AddCookie(&http.Cookie{Name: loginCookieName, Value: session.ID})
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
if origin != "https://kc.example" {
|
|
if w.Code != 403 {
|
|
t.Fatal("bad origin accepted")
|
|
}
|
|
continue
|
|
}
|
|
if _, ok := h.Logins.Get(session.ID); ok {
|
|
t.Fatal("local session survived")
|
|
}
|
|
if w.Code != 303 {
|
|
t.Fatal(w.Code)
|
|
}
|
|
dest, _ := url.Parse(w.Header().Get("Location"))
|
|
if dest.Host != "auth.example" || dest.Query().Get("rd") != "https://users.example/logged-out" {
|
|
t.Fatal("untrusted return")
|
|
}
|
|
}
|
|
r := httptest.NewRequest("POST", "https://kc.example/account/logout", strings.NewReader("csrf=wrong"))
|
|
r.Header.Set("Origin", "https://kc.example")
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.AddCookie(c)
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatal("bad csrf accepted")
|
|
}
|
|
}
|