| 1 | package home |
| 2 | |
| 3 | import ( |
| 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. |
| 28 | func 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. |
| 72 | func 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 | |