oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "net/url"
7 "strings"
8 "testing"
9)
10
11// TestRPIDIsExactlyTheCredentialHost is rulings 2, 13 and 16: "The RP ID
12// is the full host, asserted by a test."
13//
14// There are exactly two doors, and both open quietly:
15//
16// - passkey.Config has NO RPID field. The RP ID is always
17// url.Parse(Origin).Hostname(), so widening it means widening Origin
18// — or bypassing passkey and building a webauthn.Config directly.
19// - passkey.Config.LegacyRPID is passed straight through as a SECOND
20// accepted RP ID, for assertions only; registration never takes it.
21// Set to "oficina.works" it collapses the origin split §4 exists to
22// make, and nothing else would notice.
23//
24// This lives in package home, not internal/guardstest, because it reads
25// the config the APP builds through a helper over the unexported App. A
26// guards-package test cannot reach one, and a test that declares its own
27// config asserts a constant against itself.
28func TestRPIDIsExactlyTheCredentialHost(t *testing.T) {
29 app := newTestApp(t)
30 cfg := app.passkeyCfg
31
32 u, err := url.Parse(cfg.Origin)
33 if err != nil {
34 t.Fatalf("parse Origin %q: %v", cfg.Origin, err)
35 }
36 if u.Hostname() != "id.oficina.works" {
37 t.Errorf("RP ID is %q, want id.oficina.works: passkey derives the RP ID "+
38 "from Origin's hostname, so widening Origin widens it (rulings 13, 16)",
39 u.Hostname())
40 }
41 if u.Scheme != "https" {
42 t.Errorf("Origin scheme is %q; a passkey origin must be https", u.Scheme)
43 }
44
45 // Empty, or a full host under the suite domain, with the §5.7 drill
46 // that justifies it named in a comment beside it. HasSuffix and not
47 // Contains: Contains accepts "x.oficina.works.attacker.example",
48 // which is a domain somebody else can own.
49 if cfg.LegacyRPID != "" && !strings.HasSuffix(cfg.LegacyRPID, ".oficina.works") {
50 t.Errorf("LegacyRPID %q widens the RP ID (§4, ruling 2)", cfg.LegacyRPID)
51 }
52 // It is empty today, and that is the state this asserts. A hostname
53 // move is the only reason to set it, and setting it is a decision
54 // somebody should have to make deliberately.
55 if cfg.LegacyRPID != "" {
56 t.Errorf("LegacyRPID is %q; it is set with no hostname move in progress", cfg.LegacyRPID)
57 }
58
59 // The RP ID the pages hand to the browser must be the same one, or
60 // the ceremony and the server disagree about who the relying party
61 // is and every assertion fails — or worse, succeeds under a wider
62 // name.
63 if app.credentialHost != u.Hostname() {
64 t.Errorf("the pages advertise RP ID %q but the server derives %q", app.credentialHost, u.Hostname())
65 }
66}
67
68// Ruling 2's other half: the session cookie on the credential origin is
69// __Host- and host-only. A Domain attribute would share it with every
70// sibling host under the suite domain, which is the same widening one
71// level down.
72func TestNoCookieOnTheCredentialOriginCarriesADomain(t *testing.T) {
73 app := newTestApp(t)
74
75 // A full sign-in, so every cookie this app sets has been set.
76 rec := signIn(t, app, "person@example.invalid")
77 redeemed := redeem(t, app, capturedLink(t, app))
78
79 for _, r := range []*httptest.ResponseRecorder{rec, redeemed} {
80 for _, c := range (&http.Response{Header: r.Header()}).Cookies() {
81 if c.Domain != "" {
82 t.Errorf("cookie %q carries Domain=%q; it must be host-only", c.Name, c.Domain)
83 }
84 if strings.HasPrefix(c.Name, "__Host-") && c.Path != "/" {
85 t.Errorf("cookie %q has the __Host- prefix but Path=%q", c.Name, c.Path)
86 }
87 }
88 }
89}
90