-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
base: master
Are you sure you want to change the base?
Conversation
@ericchiang Please take a look... |
There was a problem hiding this 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
server/handlers.go
Outdated
</body> | ||
</html>`, ssoURL, value, authReqID) | ||
} else { | ||
// use HTTP redirect binding |
There was a problem hiding this comment.
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, ...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
Documentation/saml-connector.md
Outdated
# | ||
# Some identity provider only supports HTTP redirect binding | ||
# | ||
# redirectBinding: true |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gotcha
server/handlers.go
Outdated
@@ -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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
connector/saml/saml.go
Outdated
if p.redirectBinding { | ||
var buf bytes.Buffer | ||
zw, e := flate.NewWriter(&buf, flate.DefaultCompression) | ||
if e == nil { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
connector/saml/saml.go
Outdated
return "", "", fmt.Errorf("deflate request: %v", e) | ||
} | ||
|
||
redirectURL := fmt.Sprintf("%s?SAMLRequest=%s&RelayState=%s", |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
connector/saml/saml.go
Outdated
p.ssoURL, | ||
url.QueryEscape(base64.StdEncoding.EncodeToString(buf.Bytes())), | ||
url.QueryEscape(id)) | ||
return redirectURL, "", nil |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
0cd5141
to
3d9fb4d
Compare
@ericchiang PR updated. Please take a look when you have time. |
server/handlers.go
Outdated
</html>`, ssoURL, value, authReqID) | ||
case connector.SAMLBindingRedirect: | ||
query := make(url.Values) | ||
query["SAMLRequest"] = []string{value} |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
There was a problem hiding this 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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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 |
3d9fb4d
to
dd18a33
Compare
@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 |
@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? |
@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? |
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. |
@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. |
dd18a33
to
3cfdf6a
Compare
@ericchiang Updated. I separate the config for requestBinding and responseBinding and updated the handler. Please take a look. |
There was a problem hiding this 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.
connector/saml/saml.go
Outdated
@@ -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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
connector/saml/saml.go
Outdated
if binding == "" { | ||
return connector.SAMLBindingPOST, nil | ||
} | ||
if binding != connector.SAMLBindingPOST && binding != connector.SAMLBindingRedirect { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
connector/saml/saml.go
Outdated
if p.requestBinding, err = normalizeBinding(c.RequestBinding); err != nil { | ||
return nil, err | ||
} | ||
if p.responseBinding, err = normalizeBinding(c.ResponseBinding); err != nil { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
connector/saml/saml.go
Outdated
@@ -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) { |
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
server/handlers.go
Outdated
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
server/handlers.go
Outdated
var response string | ||
switch r.Method { | ||
case http.MethodGet: | ||
response = r.URL.Query().Get("SAMLResponse") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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".
3cfdf6a
to
b107d43
Compare
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") |
There was a problem hiding this comment.
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")
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
understand!
connector/saml/saml.go
Outdated
@@ -72,6 +79,17 @@ func init() { | |||
} | |||
} | |||
|
|||
// verifyBinding verifies binding from config | |||
func verifyBinding(binding string) (string, error) { | |||
if binding == "" { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
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. |
b107d43
to
0a77593
Compare
For all my efforts I'm having trouble finding a SAML provider to test this with. @easeway any suggestions? |
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. |
Solve issue: #1042