| 1 | package connect |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "net/http/httptest" |
| 6 | "net/url" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | func oauthTarget() string { |
| 12 | return "https://keymail.example/oauth/authorize?" + url.Values{"client_id": {configOrigin}, "redirect_uri": {configOrigin + "/auth/callback"}, "scope": {"identify"}, "code_challenge_method": {"S256"}, "state": {"private-state"}, "code_challenge": {"challenge"}}.Encode() |
| 13 | } |
| 14 | func TestOAuthContinuationPreservesPolicyCookiesAndLocalResponses(t *testing.T) { |
| 15 | for _, destination := range []string{oauthTarget(), "/signin?sent=1"} { |
| 16 | w := httptest.NewRecorder() |
| 17 | w.Header().Set("Content-Security-Policy", "form-action 'self'") |
| 18 | rendered := "" |
| 19 | h := OAuthContinuation(configOrigin, func(w http.ResponseWriter, r *http.Request) { |
| 20 | w.Header().Add("Set-Cookie", "pending=private; Path=/; Secure; HttpOnly") |
| 21 | http.Redirect(w, r, destination, 303) |
| 22 | }, func(w http.ResponseWriter, r *http.Request, target string) { rendered = target; w.WriteHeader(200) }) |
| 23 | h(w, httptest.NewRequest("POST", configOrigin+"/signin", nil)) |
| 24 | if w.Header().Get("Content-Security-Policy") != "form-action 'self'" || len(w.Result().Cookies()) != 1 { |
| 25 | t.Fatal("policy or pending cookie changed") |
| 26 | } |
| 27 | if strings.HasPrefix(destination, "https:") { |
| 28 | if w.Code != 200 || rendered != destination || w.Header().Get("Location") != "" || w.Header().Get("Cache-Control") != "no-store" || w.Header().Get("Referrer-Policy") != "no-referrer" { |
| 29 | t.Fatal("OAuth POST did not terminate", w.Code, w.Header()) |
| 30 | } |
| 31 | } else if w.Code != 303 || rendered != "" || w.Header().Get("Location") != destination { |
| 32 | t.Fatal("local flow changed") |
| 33 | } |
| 34 | } |
| 35 | } |
| 36 | func TestOAuthContinuationRefusesUnboundDestinationsWithoutLeakingState(t *testing.T) { |
| 37 | for _, destination := range []string{"//keymail.example/oauth/authorize", strings.Replace(oauthTarget(), "https:", "http:", 1), strings.Replace(oauthTarget(), "scope=identify", "scope=other", 1), oauthTarget() + "&client_id=other", oauthTarget() + "#fragment", "https://user@keymail.example/oauth/authorize", "https://keymail.example/other", strings.Replace(oauthTarget(), "S256", "plain", 1)} { |
| 38 | w := httptest.NewRecorder() |
| 39 | h := OAuthContinuation(configOrigin, func(w http.ResponseWriter, r *http.Request) { |
| 40 | w.Header().Set("Set-Cookie", "private-cookie=private") |
| 41 | http.Redirect(w, r, destination, 303) |
| 42 | }, func(http.ResponseWriter, *http.Request, string) { t.Fatal("invalid redirect rendered") }) |
| 43 | h(w, httptest.NewRequest("POST", configOrigin+"/signin", nil)) |
| 44 | if w.Code != 502 || w.Header().Get("Location") != "" || len(w.Result().Cookies()) != 0 || strings.Contains(w.Body.String(), "private") { |
| 45 | t.Fatal("unsafe destination accepted or leaked", w.Code) |
| 46 | } |
| 47 | } |
| 48 | } |
| 49 | |