Exercise Home account connection through the actual Docs router
1 file changed,
+487
−0
internal/docs/home_router_integration_test.go+487 −0
diff --git a/internal/docs/home_router_integration_test.go b/internal/docs/home_router_integration_test.go| new file mode 100644 |
| index 0000000..dacda5a |
| --- /dev/null |
| +++ b/internal/docs/home_router_integration_test.go |
| @@ -0,0 +1,487 @@ |
| +package docs |
| + |
| +import ( |
| + "bytes" |
| + "crypto/sha256" |
| + "encoding/base64" |
| + "encoding/hex" |
| + "encoding/json" |
| + "net/http" |
| + "net/http/httptest" |
| + "net/url" |
| + "reflect" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "amadan.net/rastrillo/rastrillo/assertion" |
| + assertioncrypto "amadan.net/rastrillo/rastrillo/crypto" |
| + "github.com/carlosframework/rastrillo/sessions" |
| +) |
| + |
| +type routerHomeFixture struct { |
| + app *App |
| + key *assertioncrypto.Keypair |
| + keyID string |
| + sessions *sessions.Sessions |
| +} |
| +type routerBrowser struct { |
| + f *routerHomeFixture |
| + cookies map[string]*http.Cookie |
| +} |
| + |
| +func newRouterHomeFixture(t *testing.T) *routerHomeFixture { |
| + t.Helper() |
| + key, err := assertioncrypto.Generate() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + digest := sha256.Sum256(key.SignPub()) |
| + kid := hex.EncodeToString(digest[:]) |
| + manifest, err := json.Marshal(map[string]string{kid: base64.StdEncoding.EncodeToString(key.SignPub())}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + cfg := HomeConfig{Issuer: bridgeIssuer, ConsumerID: "router-docs", KeysBase64: base64.StdEncoding.EncodeToString(manifest), DEKBase64: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{83}, 32))} |
| + app, err := NewForTestWith(t, Config{Origin: bridgeOrigin, Home: &cfg, AllowedAddresses: "alice@example.com,bob@example.com", SampleDocuments: "0", Mailer: navigationMail{}}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + sess, err := sessions.New(sessions.Config{DB: app.db.Writer(), Origin: bridgeOrigin}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return &routerHomeFixture{app: app, key: key, keyID: kid, sessions: sess} |
| +} |
| +func (f *routerHomeFixture) browser() *routerBrowser { |
| + return &routerBrowser{f: f, cookies: map[string]*http.Cookie{}} |
| +} |
| +func (b *routerBrowser) request(method, path, body string) *http.Request { |
| + r := httptest.NewRequest(method, bridgeOrigin+path, strings.NewReader(body)) |
| + for _, c := range b.cookies { |
| + r.AddCookie(c) |
| + } |
| + if method == "POST" { |
| + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
| + r.Header.Set("Origin", bridgeOrigin) |
| + r.Header.Set("Sec-Fetch-Site", "same-origin") |
| + } |
| + return r |
| +} |
| +func (b *routerBrowser) serve(r *http.Request) *httptest.ResponseRecorder { |
| + rec := httptest.NewRecorder() |
| + b.f.app.Handler().ServeHTTP(rec, r) |
| + for _, c := range rec.Result().Cookies() { |
| + if c.MaxAge < 0 { |
| + delete(b.cookies, c.Name) |
| + } else { |
| + b.cookies[c.Name] = c |
| + } |
| + } |
| + return rec |
| +} |
| +func (b *routerBrowser) do(method, path, body string) *httptest.ResponseRecorder { |
| + return b.serve(b.request(method, path, body)) |
| +} |
| +func routerRedirect(t *testing.T, rec *httptest.ResponseRecorder, want string) string { |
| + t.Helper() |
| + location := rec.Header().Get("Location") |
| + if rec.Code != 303 || (want != "" && location != want) { |
| + t.Fatalf("router redirect status=%d target=%q want=%q", rec.Code, location, want) |
| + } |
| + return location |
| +} |
| +func (b *routerBrowser) localLink(t *testing.T, address string) string { |
| + t.Helper() |
| + before := b.f.app.mailRecorder.sends |
| + rec := b.do("POST", "/signin", url.Values{"address": {address}, "force": {"1"}}.Encode()) |
| + if rec.Code != 303 || !strings.Contains(rec.Header().Get("Location"), "sent=1") { |
| + t.Fatal("local mail begin failed", rec.Code) |
| + } |
| + b.f.app.mailRecorder.mu.Lock() |
| + to, body, after := b.f.app.mailRecorder.lastTo, b.f.app.mailRecorder.lastBody, b.f.app.mailRecorder.sends |
| + b.f.app.mailRecorder.mu.Unlock() |
| + if after != before+1 || to != address { |
| + t.Fatal("actual local mail not captured") |
| + } |
| + link := magicLinkPattern.FindString(body) |
| + path, err := linkPath(link) |
| + if err != nil || path == "" { |
| + t.Fatal("captured verification link missing") |
| + } |
| + return path |
| +} |
| +func (b *routerBrowser) localSignin(t *testing.T, address string) { |
| + t.Helper() |
| + path := b.localLink(t, address) |
| + rec := b.do("GET", path, "") |
| + routerRedirect(t, rec, localLinkCompletePath) |
| + routerRedirect(t, b.do("GET", localLinkCompletePath, ""), "/") |
| + if _, ok := b.cookies[b.f.sessions.CookieName()]; !ok { |
| + t.Fatal("local verification did not set real session") |
| + } |
| +} |
| +func (b *routerBrowser) start(t *testing.T, path, maxAge string) string { |
| + t.Helper() |
| + rec := b.do("GET", path, "") |
| + target, err := url.Parse(routerRedirect(t, rec, "")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if target.Scheme+"://"+target.Host != bridgeIssuer || target.Path != "/authorize" || target.Query().Get("consumer") != "router-docs" || target.Query().Get("max_age") != maxAge || target.Query().Get("nonce") == "" { |
| + t.Fatal("router selected wrong issuer/freshness") |
| + } |
| + return target.Query().Get("nonce") |
| +} |
| +func (f *routerHomeFixture) token(t *testing.T, nonce, subject string, auth time.Time, change func(*assertion.Claims)) string { |
| + t.Helper() |
| + id, err := assertion.RandomID() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + now := time.Now().Unix() |
| + c := assertion.Claims{Version: 1, Issuer: bridgeIssuer, Audience: bridgeOrigin, Subject: subject, KeyID: f.keyID, ID: id, Nonce: nonce, IssuedAt: now, ExpiresAt: now + 120, AuthTime: auth.Unix()} |
| + if change != nil { |
| + change(&c) |
| + } |
| + token, err := assertion.Sign(f.key, c) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return token |
| +} |
| +func (b *routerBrowser) callback(token, origin string) *httptest.ResponseRecorder { |
| + // The issuer POST deliberately carries no browser cookies, as with Lax cookies. |
| + r := httptest.NewRequest("POST", bridgeOrigin+"/auth/assertion", strings.NewReader(url.Values{"assertion": {token}}.Encode())) |
| + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
| + r.Header.Set("Origin", origin) |
| + r.Header.Set("Sec-Fetch-Site", "cross-site") |
| + return b.serve(r) |
| +} |
| +func (b *routerBrowser) exchange(t *testing.T, nonce, subject string, auth time.Time) *httptest.ResponseRecorder { |
| + t.Helper() |
| + routerRedirect(t, b.callback(b.f.token(t, nonce, subject, auth, nil), bridgeIssuer), "/auth/finish") |
| + return b.do("GET", "/auth/finish", "") |
| +} |
| +func (b *routerBrowser) linkProof(t *testing.T, subject, address string) { |
| + t.Helper() |
| + page := b.do("GET", "/account/home", "") |
| + if page.Code != 200 || !strings.Contains(page.Body.String(), address) { |
| + t.Fatal("explicit initiation page did not identify local account", page.Code) |
| + } |
| + next := routerRedirect(t, b.do("POST", "/account/home", ""), "") |
| + nonce := b.start(t, next, "300") |
| + routerRedirect(t, b.exchange(t, nonce, subject, time.Now().Add(-time.Minute)), "/account/home/confirm") |
| + confirm := b.do("GET", "/account/home/confirm", "") |
| + if confirm.Code != 200 || !strings.Contains(confirm.Body.String(), address) { |
| + t.Fatal("confirmation did not identify local account", confirm.Code) |
| + } |
| +} |
| +func routerRows(t *testing.T, f *routerHomeFixture, table string) int { |
| + t.Helper() |
| + var n int |
| + if err := f.app.db.Writer().QueryRow("SELECT COUNT(*) FROM " + table).Scan(&n); err != nil { |
| + t.Fatal(err) |
| + } |
| + return n |
| +} |
| + |
| +func TestHomeRouterExistingAccountRoundTripPreservesOwnedAndSharedContent(t *testing.T) { |
| + f := newRouterHomeFixture(t) |
| + s := f.app.store |
| + ctx := t.Context() |
| + alice, err := s.EnsurePerson(ctx, "alice@example.com") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + bob, err := s.EnsurePerson(ctx, "bob@example.com") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + own, err := s.Create(ctx, alice, "Alice original") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + shared, err := s.Create(ctx, bob, "Bob shared") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + hidden, err := s.Create(ctx, bob, "Bob private") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = s.Grant(ctx, bob, shared.Sqid, "alice@example.com", RoleWriter); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, doc := range []*Document{own, shared} { |
| + owner := alice |
| + address := "alice@example.com" |
| + if doc.ID == shared.ID { |
| + owner = bob |
| + address = "bob@example.com" |
| + } |
| + if _, err = s.ApplyUpdate(ctx, doc, Actor{PersonID: owner, Subject: address}, ReadFixture(t, "hello")); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + var originalDocs []Document |
| + var originalUpdates []DocUpdate |
| + var originalGrants []Grant |
| + if err = s.db.Order("id").Find(&originalDocs).Error; err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = s.db.Order("id").Find(&originalUpdates).Error; err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = s.db.Order("id").Find(&originalGrants).Error; err != nil { |
| + t.Fatal(err) |
| + } |
| + b := f.browser() |
| + resource := "/d/" + own.Sqid |
| + signin := routerRedirect(t, b.do("GET", resource, ""), "") |
| + parsed, err := url.Parse(signin) |
| + if err != nil || parsed.Path != "/signin" || parsed.Query().Get("return_to") != resource { |
| + t.Fatal("unsigned document lost original destination") |
| + } |
| + start := routerRedirect(t, b.do("GET", signin, ""), "") |
| + nonce := b.start(t, start, "") |
| + unknown := b.exchange(t, nonce, "opaque-alice", time.Now().Add(-time.Hour)) |
| + if unknown.Code != 403 || !strings.Contains(unknown.Body.String(), "/signin/local") { |
| + t.Fatal("unknown Home subject did not show local guidance", unknown.Code) |
| + } |
| + if routerRows(t, f, "people") != 2 || routerRows(t, f, "documents") != 3 || routerRows(t, f, "docs_identity_links") != 0 || b.cookies[f.sessions.CookieName()] != nil { |
| + t.Fatal("unknown Home subject created an account or session") |
| + } |
| + b.localSignin(t, "alice@example.com") |
| + localCookie := b.cookies[f.sessions.CookieName()].Value |
| + b.linkProof(t, "opaque-alice", "alice@example.com") |
| + if b.cookies[f.sessions.CookieName()].Value != localCookie || routerRows(t, f, "docs_identity_links") != 0 { |
| + t.Fatal("proof replaced local session or linked before confirmation") |
| + } |
| + routerRedirect(t, b.do("POST", "/account/home/confirm", ""), resource) |
| + if b.cookies[f.sessions.CookieName()].Value != localCookie || routerRows(t, f, "docs_identity_links") != 1 { |
| + t.Fatal("confirmation failed local session retention") |
| + } |
| + if rec := b.do("GET", resource, ""); rec.Code != 200 { |
| + t.Fatal("linked local account cannot read original document", rec.Code) |
| + } |
| + routerRedirect(t, b.do("POST", "/signout", ""), "/signin") |
| + if b.cookies[f.sessions.CookieName()] != nil { |
| + t.Fatal("actual signout retained session cookie") |
| + } |
| + oldRequest := httptest.NewRequest("GET", bridgeOrigin+resource, nil) |
| + oldRequest.AddCookie(&http.Cookie{Name: f.sessions.CookieName(), Value: localCookie}) |
| + oldRec := httptest.NewRecorder() |
| + f.app.Handler().ServeHTTP(oldRec, oldRequest) |
| + if oldRec.Code != 303 { |
| + t.Fatal("actual signout did not revoke old session") |
| + } |
| + signin = routerRedirect(t, b.do("GET", resource, ""), "") |
| + start = routerRedirect(t, b.do("GET", signin, ""), "") |
| + nonce = b.start(t, start, "") |
| + originalAuth := time.Now().Add(-2 * time.Hour).Truncate(time.Second) |
| + routerRedirect(t, b.exchange(t, nonce, "opaque-alice", originalAuth), resource) |
| + session, ok := f.sessions.From(b.request("GET", resource, "")) |
| + if !ok || session.Method != homeAssertionMethod || session.Subject != "opaque-alice" || !session.AuthTime.Equal(originalAuth) { |
| + t.Fatal("ordinary Home session lost actual authentication time") |
| + } |
| + for _, doc := range []*Document{own, shared} { |
| + rec := b.do("GET", "/d/"+doc.Sqid, "") |
| + if rec.Code != 200 { |
| + t.Fatal("Home session lost owned/shared access", rec.Code) |
| + } |
| + } |
| + if rec := b.do("GET", "/d/"+hidden.Sqid, ""); rec.Code != 404 { |
| + t.Fatal("Home link widened another owner's private access", rec.Code) |
| + } |
| + var afterDocs []Document |
| + var afterUpdates []DocUpdate |
| + var afterGrants []Grant |
| + if err = s.db.Order("id").Find(&afterDocs).Error; err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = s.db.Order("id").Find(&afterUpdates).Error; err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = s.db.Order("id").Find(&afterGrants).Error; err != nil { |
| + t.Fatal(err) |
| + } |
| + if !reflect.DeepEqual(originalDocs, afterDocs) || !reflect.DeepEqual(originalUpdates, afterUpdates) || !reflect.DeepEqual(originalGrants, afterGrants) || routerRows(t, f, "people") != 2 { |
| + t.Fatal("account link changed owners/grants/content encoding or bytes") |
| + } |
| + // A different current local account cannot be silently replaced by Alice's SSO. |
| + other := f.browser() |
| + other.localSignin(t, "bob@example.com") |
| + before := other.cookies[f.sessions.CookieName()].Value |
| + nonce = other.start(t, "/auth/start?return_to=%2F", "") |
| + if rec := other.exchange(t, nonce, "opaque-alice", time.Now()); rec.Code != 403 { |
| + t.Fatal("different local account was replaced", rec.Code) |
| + } |
| + if other.cookies[f.sessions.CookieName()].Value != before { |
| + t.Fatal("conflict rotated local account") |
| + } |
| +} |
| + |
| +func TestHomeRouterCSRFGuardsBothLinkMutations(t *testing.T) { |
| + f := newRouterHomeFixture(t) |
| + b := f.browser() |
| + b.localSignin(t, "alice@example.com") |
| + if rec := b.do("GET", "/account/home", ""); rec.Code != 200 { |
| + t.Fatal("missing local initiation page") |
| + } |
| + for _, fetch := range []string{"cross-site", ""} { |
| + r := b.request("POST", "/account/home", "") |
| + r.Header.Set("Origin", "https://evil.example") |
| + r.Header.Set("Sec-Fetch-Site", fetch) |
| + rec := b.serve(r) |
| + if rec.Code != 403 || routerRows(t, f, "docs_link_flows") != 0 { |
| + t.Fatal("actual router permitted foreign link initiation", rec.Code) |
| + } |
| + } |
| + b.linkProof(t, "opaque-alice", "alice@example.com") |
| + for _, fetch := range []string{"cross-site", ""} { |
| + r := b.request("POST", "/account/home/confirm", "") |
| + r.Header.Set("Origin", "https://evil.example") |
| + r.Header.Set("Sec-Fetch-Site", fetch) |
| + rec := b.serve(r) |
| + if rec.Code != 403 || routerRows(t, f, "docs_identity_links") != 0 || routerRows(t, f, "docs_link_flows") != 1 { |
| + t.Fatal("actual router permitted foreign confirmation or consumed valid proof", rec.Code) |
| + } |
| + } |
| + routerRedirect(t, b.do("POST", "/account/home/confirm", ""), "/") |
| + if routerRows(t, f, "docs_identity_links") != 1 { |
| + t.Fatal("valid same-origin confirmation failed after refusal") |
| + } |
| +} |
| + |
| +func TestHomeRouterRejectsDuplicateCookiesBeforeActualEmailVerification(t *testing.T) { |
| + f := newRouterHomeFixture(t) |
| + b := f.browser() |
| + b.localSignin(t, "alice@example.com") |
| + old := b.cookies[f.sessions.CookieName()] |
| + path := b.localLink(t, "alice@example.com") |
| + for _, name := range []string{f.sessions.CookieName(), "__Host-rastrillo_pending"} { |
| + r := b.request("GET", path, "") |
| + if name == f.sessions.CookieName() { |
| + r.AddCookie(old) |
| + } else { |
| + r.AddCookie(&http.Cookie{Name: name, Value: "one"}) |
| + r.AddCookie(&http.Cookie{Name: name, Value: "two"}) |
| + } |
| + rec := b.serve(r) |
| + if rec.Code != 400 { |
| + t.Fatal("duplicate cookie reached actual Verify", name, rec.Code) |
| + } |
| + if b.cookies[f.sessions.CookieName()].Value != old.Value { |
| + t.Fatal("refused duplicate rotated browser session") |
| + } |
| + if _, ok := f.sessions.From(b.request("GET", "/", "")); !ok { |
| + t.Fatal("refused duplicate revoked actual session") |
| + } |
| + } |
| + // Both refusals happened before CompleteMagicLink consumed the token. |
| + routerRedirect(t, b.do("GET", path, ""), localLinkCompletePath) |
| + if b.cookies[f.sessions.CookieName()].Value == old.Value { |
| + t.Fatal("clean real Verify did not rotate cookie") |
| + } |
| + r := httptest.NewRequest("GET", bridgeOrigin+"/", nil) |
| + r.AddCookie(old) |
| + if _, ok := f.sessions.From(r); ok { |
| + t.Fatal("successful real Verify did not revoke old session") |
| + } |
| + routerRedirect(t, b.do("GET", localLinkCompletePath, ""), "/") |
| +} |
| + |
| +func TestHomeRouterStaleLocalGestureRestartsWithoutResumingProof(t *testing.T) { |
| + f := newRouterHomeFixture(t) |
| + b := f.browser() |
| + b.localSignin(t, "alice@example.com") |
| + b.linkProof(t, "opaque-alice", "alice@example.com") |
| + oldProof := b.cookies[homeProofCookie] |
| + oldSession := b.cookies[f.sessions.CookieName()] |
| + if oldProof == nil || routerRows(t, f, "docs_link_flows") != 1 { |
| + t.Fatal("pending proof fixture missing") |
| + } |
| + // Move only the actual stored local verification time into the past. The |
| + // old pending proof remains present so its post-rotation refusal is exercised. |
| + if _, err := f.app.db.Writer().Exec(`UPDATE sessions SET created_at=? WHERE token_hash=?`, time.Now().Add(-6*time.Minute).UTC().Format(time.RFC3339), sessions.HashToken(oldSession.Value)); err != nil { |
| + t.Fatal(err) |
| + } |
| + routerRedirect(t, b.do("GET", "/account/home", ""), "/signin/local") |
| + if b.cookies[localLinkNavigationCookie] == nil || b.cookies[homeInitCookie] != nil || b.cookies[homeProofCookie] != nil { |
| + t.Fatal("stale local restart retained link browser proof") |
| + } |
| + if rec := b.do("GET", "/signin/local", ""); rec.Code != 200 { |
| + t.Fatal("signed-in stale visitor could not see local signin") |
| + } |
| + path := b.localLink(t, "alice@example.com") |
| + routerRedirect(t, b.do("GET", path, ""), localLinkCompletePath) |
| + routerRedirect(t, b.do("GET", localLinkCompletePath, ""), "/account/home") |
| + if b.cookies[f.sessions.CookieName()].Value == oldSession.Value || b.cookies[homeProofCookie] != nil || b.cookies[homeInitCookie] != nil || routerRows(t, f, "docs_identity_links") != 0 { |
| + t.Fatal("navigation minted or resumed link authority") |
| + } |
| + if rec := b.do("GET", "/account/home", ""); rec.Code != 200 { |
| + t.Fatal("fresh local gesture did not reach initiation page") |
| + } |
| + // A copied old proof cannot attach to the freshly rotated local session. |
| + b.cookies[homeProofCookie] = oldProof |
| + if rec := b.do("POST", "/account/home/confirm", ""); rec.Code != 403 { |
| + t.Fatal("old proof resumed after actual session rotation", rec.Code) |
| + } |
| + if routerRows(t, f, "docs_identity_links") != 0 || routerRows(t, f, "docs_link_flows") != 0 { |
| + t.Fatal("old proof failure created link or retained reusable proof") |
| + } |
| + oldRequest := httptest.NewRequest("GET", bridgeOrigin+"/", nil) |
| + oldRequest.AddCookie(oldSession) |
| + if _, ok := f.sessions.From(oldRequest); ok { |
| + t.Fatal("old local browser token survived reverification") |
| + } |
| + // Only this new explicit POST creates a new initiation, not the email or GET. |
| + routerRedirect(t, b.do("POST", "/account/home", ""), "/auth/start?return_to=%2Faccount%2Fhome%2Fconfirm") |
| + if routerRows(t, f, "docs_link_flows") != 1 || routerRows(t, f, "docs_identity_links") != 0 || b.cookies[homeProofCookie] != nil { |
| + t.Fatal("new explicit initiation state wrong") |
| + } |
| +} |
| + |
| +func TestHomeRouterCallbackChecksExactOriginIssuerAndAudience(t *testing.T) { |
| + f := newRouterHomeFixture(t) |
| + b := f.browser() |
| + nonce := b.start(t, "/auth/start?return_to=%2F", "") |
| + for _, tc := range []struct { |
| + name, origin string |
| + change func(*assertion.Claims) |
| + }{ |
| + {"wrong origin", "https://evil.example", nil}, |
| + {"missing origin", "", nil}, |
| + {"wrong signed issuer", bridgeIssuer, func(c *assertion.Claims) { c.Issuer = "https://other-id.example" }}, |
| + {"wrong signed audience", bridgeIssuer, func(c *assertion.Claims) { c.Audience = "https://other.docs.oficina.works" }}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + rec := b.callback(f.token(t, nonce, "opaque", time.Now(), tc.change), tc.origin) |
| + if rec.Code != 400 || routerRows(t, f, "oficina_handoff_inbox") != 0 || b.cookies[f.sessions.CookieName()] != nil { |
| + t.Fatal("invalid callback created inbox/session", rec.Code) |
| + } |
| + }) |
| + } |
| + r := b.request("GET", "/auth/start?return_to=%2F", "") |
| + r.Host = "other.docs.oficina.works" |
| + if rec := b.serve(r); rec.Code != 404 { |
| + t.Fatal("router accepted another team host", rec.Code) |
| + } |
| + // A valid issuer callback still crosses the outer CSRF boundary and reaches |
| + // the shared consumer. Duplicate local cookies cannot poison that callback. |
| + token := f.token(t, nonce, "opaque", time.Now(), nil) |
| + r = httptest.NewRequest("POST", bridgeOrigin+"/auth/assertion", strings.NewReader(url.Values{"assertion": {token}}.Encode())) |
| + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
| + r.Header.Set("Origin", bridgeIssuer) |
| + r.Header.Set("Sec-Fetch-Site", "cross-site") |
| + r.AddCookie(&http.Cookie{Name: f.sessions.CookieName(), Value: "bad"}) |
| + r.AddCookie(&http.Cookie{Name: f.sessions.CookieName(), Value: "bad"}) |
| + routerRedirect(t, b.serve(r), "/auth/finish") |
| + if rec := b.do("GET", "/auth/finish", ""); rec.Code != 403 || !strings.Contains(rec.Body.String(), "/signin/local") { |
| + t.Fatal("unknown verified identity not guided after valid callback") |
| + } |
| + if routerRows(t, f, "people") != 0 || routerRows(t, f, "docs_identity_links") != 0 { |
| + t.Fatal("unknown callback created person/link") |
| + } |
| +} |