Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass through certain forward auth negative response headers #2127

Merged
merged 10 commits into from
Nov 2, 2017

Conversation

wheresmysocks
Copy link
Contributor

This alters forward authentication allowing servers to set cookies and redirect clients to other locations if needed.

Description

The current version of httpClient automatically follows redirects, so if a forward auth server decides to redirect the original client to a different page to continue authentication, Go will simply follow the redirect rather than passing the redirection back to the original client.

This is problematic when the forward auth function returns an unexpected success if it was able to follow a redirection chain until it encountered a 200 response. This PR alters the httpClient to stop following redirects automatically and pass a Location header from a negative forward auth response, if one exists.

Additionally, the forward auth server is not able to set any cookies (other than including JavaScript in the response body, I suppose). This PR also passes the Set-Cookie header on a negative forward auth response.

Copy link
Member

@emilevauge emilevauge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design LGTM

@jkaberg
Copy link

jkaberg commented Oct 17, 2017

@emilevauge @wheresmysocks any ETA on this one❓

Conflicts:
	auth/forward.go
	middlewares/auth/authenticator_test.go
@wheresmysocks
Copy link
Contributor Author

Should now be back up to date with master.

Copy link
Contributor

@ldez ldez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ldez ldez added the kind/enhancement a new or improved feature. label Oct 26, 2017
Copy link
Contributor

@nmengin nmengin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👏 👏

Copy link
Contributor

@ldez ldez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks 👍


// Set the location in our response if one was sent back.

if err != http.ErrNoLocation && redirectURL.String() != "" {
Copy link
Contributor

@ldez ldez Oct 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain err != http.ErrNoLocation is this case?
I think you want to check: err == http.ErrNoLocation && redirectURL.String() != ""

If I'm true, the code can be rewrite like that:

if err != nil {
	if err != http.ErrNoLocation {
		log.Debugf("Error reading response location header %s. Cause: %s", config.Address, err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
        if redirectURL.String() != "" {
		// Set the location in our response if one was sent back.
		w.Header().Add("Location", redirectURL.String())
	}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are three things to handle with Response.Location() return values:

  1. Handle the case where a Location header was present but it did not have a parsable URL (some non-nil error).
  2. Handle the case where no Location header was present (err == http.ErrNoLocation).
  3. Handle the case where a Location header was present and it had a parsable URL.

The second case is common and not an error as far as we are concerned as any non-redirect response code would trigger this case, so it's important to ensure it passes through. Any other error was likely a non-parsable URL. With a small edit, this can be simplified in mainly the manner you describe:

redirectURL, err := forwardResponse.Location()

if err != nil {
	if err != http.ErrNoLocation { // Case #1
		log.Debugf("Error reading response location header %s. Cause: %s", config.Address, err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	} /* else {
		// Case #2, do nothing with http.ErrNoLocation
	} */
} else if redirectURL.String() != "" { // Case #3
	// Set the location in our response if one was sent back.
	w.Header().Add("Location", redirectURL.String())
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err != nil {
	if err != http.ErrNoLocation {
// ...
    }
} else if redirectURL.String() != "" {
// ...
}

It's not the same thing as:

if err != nil && err != http.ErrNoLocation {
// ...
}

if err != http.ErrNoLocation && redirectURL.String() != "" {
// ...
}

It's the same as:

if err != nil && err != http.ErrNoLocation {
// ...
}

if err == nil && redirectURL.String() != "" {
// ...
}

Are you sure of your changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the change is as intended. I believe that because the first if clause in each of the last two snippets you presented above should contain a return statement, the second if clause in both snippets are functionally equivalent (as the only value err may contain after the first if clause should be nil or http.ErrNoLocation).

I believe another way to write this would be with a switch statement:

redirectURL, err := forwardResponse.Location()

switch {

// Do nothing, as there was no Location header.
case err == http.ErrNoLocation:
    break

// Return an error, as we were (likely) unable to parse the Location header.
case err != nil:
    log.Debugf("Error reading response location header %s. Cause: %s", config.Address, err)
    w.WriteHeader(http.StatusInternalServerError)
    return

// Set the location in our response if one was sent back.
case redirectURL.String() != "":
    w.Header().Add("Location", redirectURL.String())

}


cookies := forwardResponse.Cookies()

for _, cookie := range cookies {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can rewrite this part like that:

// Pass any Set-Cookie headers the forward auth server provides
for _, cookie := range forwardResponse.Cookies() {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, was leftover from testing.

Copy link
Contributor

@ldez ldez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ldez ldez added this to the 1.5 milestone Nov 2, 2017
Copy link
Member

@mmatur mmatur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants