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

support SAML HTTP redirect binding #1045

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

easeway
Copy link

@easeway easeway commented Aug 23, 2017

Solve issue: #1042

@easeway
Copy link
Author

easeway commented Aug 23, 2017

@ericchiang Please take a look...

Copy link
Contributor

@ericchiang ericchiang left a comment

Choose a reason for hiding this comment

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

Need to change the response handler too.

Almost feel like this should surface as a different interface value https://godoc.org/github.com/coreos/dex/connector#SAMLConnector

</body>
</html>`, ssoURL, value, authReqID)
} else {
// use HTTP redirect binding
Copy link
Contributor

Choose a reason for hiding this comment

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

We should just return an actual redirect here, not render a page. E.g.

http.Redirect(w, r, ...)

Copy link
Author

Choose a reason for hiding this comment

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

Yes.

#
# Some identity provider only supports HTTP redirect binding
#
# redirectBinding: true
Copy link
Contributor

Choose a reason for hiding this comment

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

this should probably be something like

binding: redirect # defaults to post

in case we want to support more bindings in the future.

Copy link
Author

Choose a reason for hiding this comment

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

gotcha

@@ -254,30 +254,47 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
s.logger.Errorf("Server template error: %v", err)
}
case connector.SAMLConnector:
action, value, err := conn.POSTData(scopes, authReqID)
ssoURL, value, err := conn.POSTData(scopes, authReqID)
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to rename POSTData or break it into a separate method.

Copy link
Author

@easeway easeway Aug 23, 2017

Choose a reason for hiding this comment

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

I think it's better to pass http.ResponseWriter and http.Request into provider to reduce provider specific code in server/handlers.go. Eventually we can eliminate this switch conn := conn.Connector.(type)

My proposal is to define a common Connector interface including a method:

HandleLogin(w http.ResponseWriter, r *http.Request)

And leave to the Connector implementation for all provider specific logic.

In the mean time, I will do the following change:

Rename PostData to AuthnRequest(scopes, authId, w http.ResponseWriter, r *http.Request) and perform the logic there.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm actually against that. The server package controls things like html error responses and I'd rather all of that logic continue to live there. Not have any connectors refer to the net/http package directly.

if p.redirectBinding {
var buf bytes.Buffer
zw, e := flate.NewWriter(&buf, flate.DefaultCompression)
if e == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

This block shouldn't keep going if err != nil

Copy link
Author

Choose a reason for hiding this comment

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

here e == nil and err != nil is handled in one place below.

I use e == nil because the sequence will go through 3 steps: NewWriter, Write and Flush. Each may fail and should bail out immediately. So use if e == nil { do...} will make the code simpler, and handle the error in one place.

return "", "", fmt.Errorf("deflate request: %v", e)
}

redirectURL := fmt.Sprintf("%s?SAMLRequest=%s&RelayState=%s",
Copy link
Contributor

Choose a reason for hiding this comment

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

use url.URL and an actual url.Values object https://golang.org/pkg/net/url/#Values

Copy link
Author

Choose a reason for hiding this comment

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

OK. I will construct a URL.

p.ssoURL,
url.QueryEscape(base64.StdEncoding.EncodeToString(buf.Bytes())),
url.QueryEscape(id))
return redirectURL, "", nil
Copy link
Contributor

Choose a reason for hiding this comment

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

This method now returns completely different kind of values depending on how it's configured. The URL creation and HTML creation should be done in the same place.

Copy link
Author

Choose a reason for hiding this comment

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

I will update the interface according to your comments below.

@easeway
Copy link
Author

easeway commented Aug 23, 2017

@ericchiang PR updated. Please take a look when you have time.

</html>`, ssoURL, value, authReqID)
case connector.SAMLBindingRedirect:
query := make(url.Values)
query["SAMLRequest"] = []string{value}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this is just query.Set("SAMLRequest", value)

Copy link
Author

Choose a reason for hiding this comment

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

yes

Copy link
Contributor

@ericchiang ericchiang left a comment

Choose a reason for hiding this comment

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

That worked out nicely. Thanks for the refactors.

When the provider redirects back to dex, that'll now be different though, right? E.g. this logic needs to change https://github.com/coreos/dex/blob/master/server/handlers.go#L327

@@ -65,18 +65,24 @@ type CallbackConnector interface {
HandleCallback(s Scopes, r *http.Request) (identity Identity, err error)
}

// SAMLConnector bindings
const (
SAMLBindingPOST = "post"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this should probably be httpPost and httpRedirect, right? Since the other ones are SOAP based.

Copy link
Author

Choose a reason for hiding this comment

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

Do you mean the const name should be "SAMLHTTPPOST" or the value should be "httpPost"? I'm using the same value from config option. I'm OK if we decide to use "http-post", "http-redirect" in values of config option "binding". But I'd like to use the same value internally in code.

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant the "post" and "redirect" values.

Actually, I think the way it currently is is fine. Let's keep it as is.

@easeway
Copy link
Author

easeway commented Aug 23, 2017

The handler doesn't need any change. When specifying "binding: redirect", it only specifies how dex opens login page on identity provider. The way identity provider calls back to dex can use a different http method, which is defined by the trusted service callback mechanism in identity provider (the pre-registered trusted service provider definition).

If identity provider respect AssertionConsumerServiceURL in AuthnRequest, it will use ProtocolBinding to send back response. Currently in dex, the ProtocolBinding is always POST.

I have verified the implement against vSphere 6.5

@ericchiang
Copy link
Contributor

@easeway ah I see that you can actually mix bindings.

It's good that vSphere works with this setup, but what about providers that don't support the POST binding for the SAMLResponse? Do we need explicit requestBinding and responseBinding values?

@easeway
Copy link
Author

easeway commented Aug 23, 2017

@ericchiang For providers don't support POST binding in response, we need to do further fixes. We should make it configurable, that when constructing AuthnRequest, we can specify ProtocolBinding from the configuration. Then we will have to fix the handler too.

What about making a separate PR for that?

@easeway
Copy link
Author

easeway commented Aug 24, 2017

@ericchiang I created a separate PR #1048 to support response redirect binding. However vSphere doesn't support redirect binding on response, so I can't verify that, and that's why I make it a separate PR.

Would you approve this PR if you think the change is good here?

@ericchiang
Copy link
Contributor

Would you approve this PR if you think the change is good here?

No we should just move to #1048. Since it's all the same feature I'd rather we design the full thing instead of doing it piecemeal.

@easeway
Copy link
Author

easeway commented Aug 24, 2017

@ericchiang All right. I will collapse the changes in #1048 into this PR and close #1048. And I make sure the features except response redirect binding is verified.

@easeway
Copy link
Author

easeway commented Aug 24, 2017

@ericchiang Updated. I separate the config for requestBinding and responseBinding and updated the handler. Please take a look.

Copy link
Contributor

@ericchiang ericchiang left a comment

Choose a reason for hiding this comment

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

Thanks, this is getting close!

Going to try to test this patch against another SAML provider that implements the HTTP redirect binding.

@@ -72,6 +75,18 @@ func init() {
}
}

// normalizeBinding verifies binding from config and normalize the string
func normalizeBinding(binding string) (string, error) {
binding = strings.ToLower(strings.TrimSpace(binding))
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd avoid any normalization. Keeps the configuration explicit.

Copy link
Author

Choose a reason for hiding this comment

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

OK. I will remove this ToLower line.

if binding == "" {
return connector.SAMLBindingPOST, nil
}
if binding != connector.SAMLBindingPOST && binding != connector.SAMLBindingRedirect {
Copy link
Contributor

Choose a reason for hiding this comment

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

probably just use a globally defined map + a lookup

Copy link
Author

Choose a reason for hiding this comment

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

only two string const comparisons. I'm trying to avoid a global var. Anyway, I will add a global map.

if p.requestBinding, err = normalizeBinding(c.RequestBinding); err != nil {
return nil, err
}
if p.responseBinding, err = normalizeBinding(c.ResponseBinding); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

the response binding should probably default to whatever the request binding is, right?

Copy link
Author

Choose a reason for hiding this comment

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

not sure. In most cases, responseBinding will be POST. Even requestBinding is redirect. Would it be simpler to define fixed default values in config?

Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be simpler to define fixed default values in config?

Works for me.

@@ -252,6 +281,11 @@ func (p *provider) POSTData(s connector.Scopes, id string) (action, value string
},
AssertionConsumerServiceURL: p.redirectURI,
}

if p.responseBinding == connector.SAMLBindingRedirect {
r.ProtocolBinding = bindingRedirect
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we not set this if it's a POST?

Copy link
Author

Choose a reason for hiding this comment

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

It's already set to POST in a few lines above when constructing the authnRequest.

@@ -598,3 +647,34 @@ func before(now, notBefore time.Time) bool {
func after(now, notOnOrAfter time.Time) bool {
return now.After(notOnOrAfter.Add(allowedClockDrift))
}

// deflate compresses the data
func deflate(data []byte) ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: just call this "compressRequest" and "decompressRequest"

Copy link
Author

Choose a reason for hiding this comment

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

Yes

s.renderError(w, http.StatusBadRequest, "Invalid request")
return
}
identity, err = conn.HandlePOST(parseScopes(authReq.Scopes), r.PostFormValue("SAMLResponse"), authReq.ID)
identity, err = conn.HandleResponse(parseScopes(authReq.Scopes), response, authReq.ID)
Copy link
Contributor

Choose a reason for hiding this comment

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

So in this case I could request responseBinding: post, get an HTTP redirect response, and we'd still process it even though we've explicitly stated that we don't.

I think we need to enforce that only the binding that the connector is configured with is actually used.

Copy link
Author

Choose a reason for hiding this comment

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

You are right. I will fix this.

Copy link
Author

Choose a reason for hiding this comment

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

The problem is at this layer, it doesn't have the knowledge about bindings. Anyway, let me figure it out...

var response string
switch r.Method {
case http.MethodGet:
response = r.URL.Query().Get("SAMLResponse")
Copy link
Contributor

Choose a reason for hiding this comment

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

With this logic I could use "state" to look up the authID and never actually inspect the "RelayState" even though it's required for this response. This seems wrong.

Copy link
Author

Choose a reason for hiding this comment

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

handleConnectorCallback is shared by multiple providers. OAuth2 uses "state" while SAML uses "RelayState". So if it's SAML, there's no "state", and we should use "RelayState".

if authID = r.URL.Query().Get("state"); authID == "" {
case http.MethodGet: // OAuth2 or SAML HTTP-Redirect callback
// try OAuth2
authID = r.URL.Query().Get("state")
Copy link
Contributor

Choose a reason for hiding this comment

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

The SAML connector can still use the value set by "state" here. We need to enforce:

var usedRelayState bool
authID = r.URL.Query().Get("state")
if authID == "" {
    usedRelayState = true
    authID = r.URL.Query.Get("RelayState")
}

Then verify this value later.

case connector.CallbackConnector:
    if usedRelayState {
        return nil, fmt.Errorf("no state parameter provided")
    }
case connector.SAMLConnector:
    if !userRelayState {
        return nil, fmt.Errorf("invalid url query value 'state' provided")
    }

Copy link
Author

Choose a reason for hiding this comment

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

understand!

@@ -72,6 +79,17 @@ func init() {
}
}

// verifyBinding verifies binding from config
func verifyBinding(binding string) (string, error) {
if binding == "" {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: just set defaults in openConnector

Copy link
Author

Choose a reason for hiding this comment

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

OK

@ericchiang
Copy link
Contributor

I need to grab an open source SAML provider and try to write up some instructions for how to test this before merging. We want to make sure we can debug future changes to this binding.

@ericchiang
Copy link
Contributor

For all my efforts I'm having trouble finding a SAML provider to test this with. @easeway any suggestions?

@easeway
Copy link
Author

easeway commented Oct 26, 2017

I've verified with vSphere with both POST and redirect binding for request, and POST binding for response. I believe okta works with POST binding for both request and response (as in original code). So we are good with bindings for requests. Unfortunately, I didn't find a provider which supports redirect binding for response. I can continue looking for a provider which allows a services to be registered with a redirect binding for response.

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

Successfully merging this pull request may close these issues.

2 participants