Fix form_post.js mistake from recent commit; Better CORS on callback

This commit is contained in:
Ryan Richard
2022-02-08 17:30:48 -08:00
parent f6f188565b
commit 5d79d4b9dc
5 changed files with 123 additions and 27 deletions

View File

@@ -835,6 +835,20 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req
var params url.Values
if h.useFormPost { // nolint:nestif
// Return HTTP 405 for anything that's not a POST or an OPTIONS request.
if r.Method != http.MethodPost && r.Method != http.MethodOptions {
h.logger.V(debugLogLevel).Info("Pinniped: Got unexpected request on callback listener", "method", r.Method)
w.WriteHeader(http.StatusMethodNotAllowed)
return nil // keep listening for more requests
}
// For POST and OPTIONS requests, calculate the allowed origin for CORS.
issuerURL, parseErr := url.Parse(h.issuer)
if parseErr != nil {
return httperr.Wrap(http.StatusInternalServerError, "invalid issuer url", parseErr)
}
allowOrigin := issuerURL.Scheme + "://" + issuerURL.Host
if r.Method == http.MethodOptions {
// Google Chrome decided that it should do CORS preflight checks for this Javascript form submission POST request.
// See https://developer.chrome.com/blog/private-network-access-preflight/
@@ -846,12 +860,9 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req
return nil // keep listening for more requests
}
h.logger.V(debugLogLevel).Info("Pinniped: Got CORS preflight request from browser", "origin", origin)
issuerURL, parseErr := url.Parse(h.issuer)
if parseErr != nil {
return httperr.Wrap(http.StatusInternalServerError, "invalid issuer url", parseErr)
}
// To tell the browser that it is okay to make the real POST request, return the following response.
w.Header().Set("Access-Control-Allow-Origin", issuerURL.Scheme+"://"+issuerURL.Host)
w.Header().Set("Access-Control-Allow-Origin", allowOrigin)
w.Header().Set("Vary", "*") // supposed to use Vary when Access-Control-Allow-Origin is a specific host
w.Header().Set("Access-Control-Allow-Credentials", "false")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Private-Network", "true")
@@ -864,20 +875,21 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req
}
w.WriteHeader(http.StatusNoContent)
return nil // keep listening for more requests
}
// Return HTTP 405 for anything that's not a POST.
if r.Method != http.MethodPost {
h.logger.V(debugLogLevel).Info("Pinniped: Got unexpected request on callback listener", "method", r.Method)
w.WriteHeader(http.StatusMethodNotAllowed)
return nil // keep listening for more requests
}
} // Otherwise, this is a POST request...
// Parse and pull the response parameters from an application/x-www-form-urlencoded request body.
if err := r.ParseForm(); err != nil {
return httperr.Wrap(http.StatusBadRequest, "invalid form", err)
}
params = r.Form
// Allow CORS requests for POST so in the future our Javascript code can be updated to use the fetch API's
// mode "cors", and still be compatible with older CLI versions starting with those that have this code
// for CORS headers. Updating to use CORS would allow our Javascript code (form_post.js) to see the true
// http response status from this endpoint. Note that the POST response does not need to set as many CORS
// headers as the OPTIONS preflight response.
w.Header().Set("Access-Control-Allow-Origin", allowOrigin)
w.Header().Set("Vary", "*") // supposed to use Vary when Access-Control-Allow-Origin is a specific host
} else {
// Return HTTP 405 for anything that's not a GET.
if r.Method != http.MethodGet {

View File

@@ -1885,6 +1885,7 @@ func TestHandleAuthCodeCallback(t *testing.T) {
method: http.MethodPost,
query: "",
wantNoCallbacks: true,
wantHeaders: map[string][]string{},
wantHTTPStatus: http.StatusMethodNotAllowed,
},
{
@@ -1893,6 +1894,7 @@ func TestHandleAuthCodeCallback(t *testing.T) {
query: "",
opt: withFormPostMode,
wantNoCallbacks: true,
wantHeaders: map[string][]string{},
wantHTTPStatus: http.StatusMethodNotAllowed,
},
{
@@ -1903,24 +1905,28 @@ func TestHandleAuthCodeCallback(t *testing.T) {
body: []byte(`%`),
opt: withFormPostMode,
wantErr: `invalid form: invalid URL escape "%"`,
wantHeaders: map[string][]string{},
wantHTTPStatus: http.StatusBadRequest,
},
{
name: "invalid state",
query: "state=invalid",
wantErr: "missing or invalid state parameter",
wantHeaders: map[string][]string{},
wantHTTPStatus: http.StatusForbidden,
},
{
name: "error code from provider",
query: "state=test-state&error=some_error",
wantErr: `login failed with code "some_error"`,
wantHeaders: map[string][]string{},
wantHTTPStatus: http.StatusBadRequest,
},
{
name: "error code with a description from provider",
query: "state=test-state&error=some_error&error_description=optional%20error%20description",
wantErr: `login failed with code "some_error": optional error description`,
wantHeaders: map[string][]string{},
wantHTTPStatus: http.StatusBadRequest,
},
{
@@ -1929,6 +1935,23 @@ func TestHandleAuthCodeCallback(t *testing.T) {
query: "",
headers: map[string][]string{"Origin": {"https://some-origin.com"}},
wantErr: `invalid issuer url: parse "://bad-url": missing protocol scheme`,
wantHeaders: map[string][]string{},
wantHTTPStatus: http.StatusInternalServerError,
opt: func(t *testing.T) Option {
return func(h *handlerState) error {
h.useFormPost = true
h.issuer = "://bad-url"
return nil
}
},
},
{
name: "in form post mode, invalid issuer url config during POST request returns an error",
method: http.MethodPost,
query: "",
headers: map[string][]string{"Origin": {"https://some-origin.com"}},
wantErr: `invalid issuer url: parse "://bad-url": missing protocol scheme`,
wantHeaders: map[string][]string{},
wantHTTPStatus: http.StatusInternalServerError,
opt: func(t *testing.T) Option {
return func(h *handlerState) error {
@@ -1944,6 +1967,7 @@ func TestHandleAuthCodeCallback(t *testing.T) {
query: "",
opt: withFormPostMode,
wantNoCallbacks: true,
wantHeaders: map[string][]string{},
wantHTTPStatus: http.StatusBadRequest,
},
{
@@ -1957,6 +1981,7 @@ func TestHandleAuthCodeCallback(t *testing.T) {
"Access-Control-Allow-Credentials": {"false"},
"Access-Control-Allow-Methods": {"POST, OPTIONS"},
"Access-Control-Allow-Origin": {"https://valid-issuer.com"},
"Vary": {"*"},
"Access-Control-Allow-Private-Network": {"true"},
},
opt: func(t *testing.T) Option {
@@ -1981,6 +2006,7 @@ func TestHandleAuthCodeCallback(t *testing.T) {
"Access-Control-Allow-Credentials": {"false"},
"Access-Control-Allow-Methods": {"POST, OPTIONS"},
"Access-Control-Allow-Origin": {"https://valid-issuer.com"},
"Vary": {"*"},
"Access-Control-Allow-Private-Network": {"true"},
"Access-Control-Allow-Headers": {"header1, header2, header3"},
},
@@ -1996,6 +2022,7 @@ func TestHandleAuthCodeCallback(t *testing.T) {
name: "invalid code",
query: "state=test-state&code=invalid",
wantErr: "could not complete code exchange: some exchange error",
wantHeaders: map[string][]string{},
wantHTTPStatus: http.StatusBadRequest,
opt: func(t *testing.T) Option {
return func(h *handlerState) error {
@@ -2015,6 +2042,7 @@ func TestHandleAuthCodeCallback(t *testing.T) {
name: "valid",
query: "state=test-state&code=valid",
wantHTTPStatus: http.StatusOK,
wantHeaders: map[string][]string{"Content-Type": {"text/plain; charset=utf-8"}},
opt: func(t *testing.T) Option {
return func(h *handlerState) error {
h.oauth2Config = &oauth2.Config{RedirectURL: testRedirectURI}
@@ -2030,10 +2058,44 @@ func TestHandleAuthCodeCallback(t *testing.T) {
},
},
{
name: "valid form_post",
method: http.MethodPost,
headers: map[string][]string{"Content-Type": {"application/x-www-form-urlencoded"}},
body: []byte(`state=test-state&code=valid`),
name: "valid form_post",
method: http.MethodPost,
headers: map[string][]string{"Content-Type": {"application/x-www-form-urlencoded"}},
body: []byte(`state=test-state&code=valid`),
wantHeaders: map[string][]string{
"Access-Control-Allow-Origin": {"https://valid-issuer.com"},
"Vary": {"*"},
"Content-Type": {"text/plain; charset=utf-8"},
},
wantHTTPStatus: http.StatusOK,
opt: func(t *testing.T) Option {
return func(h *handlerState) error {
h.useFormPost = true
h.oauth2Config = &oauth2.Config{RedirectURL: testRedirectURI}
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI {
mock := mockUpstream(t)
mock.EXPECT().
ExchangeAuthcodeAndValidateTokens(gomock.Any(), "valid", pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), testRedirectURI).
Return(&oidctypes.Token{IDToken: &oidctypes.IDToken{Token: "test-id-token"}}, nil)
return mock
}
return nil
}
},
},
{
name: "valid form_post made with the same origin headers that would be used by a Javascript fetch client using mode=cors",
method: http.MethodPost,
headers: map[string][]string{
"Content-Type": {"application/x-www-form-urlencoded"},
"Origin": {"https://some-origin.com"},
},
body: []byte(`state=test-state&code=valid`),
wantHeaders: map[string][]string{
"Access-Control-Allow-Origin": {"https://valid-issuer.com"},
"Vary": {"*"},
"Content-Type": {"text/plain; charset=utf-8"},
},
wantHTTPStatus: http.StatusOK,
opt: func(t *testing.T) Option {
return func(h *handlerState) error {
@@ -2062,6 +2124,7 @@ func TestHandleAuthCodeCallback(t *testing.T) {
pkce: pkce.Code("test-pkce"),
nonce: nonce.Nonce("test-nonce"),
logger: testlogger.New(t).Logger,
issuer: "https://valid-issuer.com/with/some/path",
}
if tt.opt != nil {
require.NoError(t, tt.opt(t)(h))