From 6bb255bef8c8892104ebf48a6ff5962f51c2fdae Mon Sep 17 00:00:00 2001 From: Stephan Renatus Date: Fri, 19 Jan 2018 09:49:48 +0100 Subject: [PATCH] connector/saml: add redirect binding support This change adds support for using the "HTTP Redirect Binding" for AuthnRequests. Note that it's uncommon to use the Redirect Binding for consuming assertions. To enable redirect, add redirectBinding: true to your SAML connector's config section. If not set, it will default to the current behaviour, HTTP POST Binding. Signed-off-by: Stephan Renatus --- Documentation/connectors/saml.md | 5 ++- connector/connector.go | 10 ++--- connector/saml/saml.go | 74 ++++++++++++++++++++++++-------- server/handlers.go | 51 ++++++++++++++-------- 4 files changed, 96 insertions(+), 44 deletions(-) diff --git a/Documentation/connectors/saml.md b/Documentation/connectors/saml.md index 62cf6a7ff5..60c9134d25 100644 --- a/Documentation/connectors/saml.md +++ b/Documentation/connectors/saml.md @@ -2,7 +2,9 @@ ## Overview -The SAML provider allows authentication through the SAML 2.0 HTTP POST binding. The connector maps attribute values in the SAML assertion to user info, such as username, email, and groups. +The SAML provider allows authentication through the SAML 2.0 HTTP POST Binding or HTTP Redirect Binding. +The callback acts as the "Assertion Consumer Service", expecting assertions via the HTTP POST Binding. +The connector maps attribute values in the SAML assertion to user info, such as username, email, and groups. The connector uses the value of the `NameID` element as the user's unique identifier which dex assumes is both unique and never changes. Use the `nameIDPolicyFormat` to ensure this is set to a value which satisfies these requirements. @@ -26,6 +28,7 @@ connectors: config: # SSO URL used for POST value. ssoURL: https://saml.example.com/sso + redirectBinding: true # use HTTP Redirect Binding (defaults to false) # CA to use when validating the signature of the SAML response. ca: /path/to/ca.pem diff --git a/connector/connector.go b/connector/connector.go index edd7fa5706..f1d7f27a23 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -74,12 +74,10 @@ type CallbackConnector interface { // See: https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf // "3.5 HTTP POST Binding" type SAMLConnector interface { - // POSTData returns an encoded SAML request and SSO URL for the server to - // render a POST form with. - // - // POSTData should encode the provided request ID in the returned serialized - // SAML request. - POSTData(s Scopes, requestID string) (ssoURL, samlRequest string, err error) + // AuthnRequest returns an encoded SAML request and SSO URL for the server to + // render a POST form with, or send via the Redirect Binding, depending on the + // connector's configuration. + AuthnRequest(s Scopes, requestID string) (sooURL, samlRequest string, post bool, err error) // HandlePOST decodes, verifies, and maps attributes from the SAML response. // It passes the expected value of the "InResponseTo" response field, which diff --git a/connector/saml/saml.go b/connector/saml/saml.go index 4dac2aca6f..62a7917438 100644 --- a/connector/saml/saml.go +++ b/connector/saml/saml.go @@ -3,6 +3,7 @@ package saml import ( "bytes" + "compress/flate" "crypto/x509" "encoding/base64" "encoding/pem" @@ -84,6 +85,17 @@ type Config struct { SSOIssuer string `json:"ssoIssuer"` SSOURL string `json:"ssoURL"` + // Use HTTP Redirect Binding instead of HTTP Post Binding for AuthnRequest + // See: https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf + // "3.4.4 Message Encoding" for HTTP Redirect Binding + // "3.5.4 Message Encoding" for HTTP POST Binding + // + // If not provided, will use HTTP Post Binding. + // If set to true, this will use the DEFLATE encoding specified in the spec + // referenced above. It will NOT pass SAMLEncoding set to + // urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE + RedirectBinding bool `json:"redirectBinding"` + // X509 CA file or raw data to verify XML signatures. CA string `json:"ca"` CAData []byte `json:"caData"` @@ -154,16 +166,17 @@ func (c *Config) openConnector(logger log.Logger) (*provider, error) { } p := &provider{ - entityIssuer: c.EntityIssuer, - ssoIssuer: c.SSOIssuer, - ssoURL: c.SSOURL, - now: time.Now, - usernameAttr: c.UsernameAttr, - emailAttr: c.EmailAttr, - groupsAttr: c.GroupsAttr, - groupsDelim: c.GroupsDelim, - redirectURI: c.RedirectURI, - logger: logger, + entityIssuer: c.EntityIssuer, + ssoIssuer: c.SSOIssuer, + ssoURL: c.SSOURL, + redirectBinding: c.RedirectBinding, + now: time.Now, + usernameAttr: c.UsernameAttr, + emailAttr: c.EmailAttr, + groupsAttr: c.GroupsAttr, + groupsDelim: c.GroupsDelim, + redirectURI: c.RedirectURI, + logger: logger, nameIDPolicyFormat: c.NameIDPolicyFormat, } @@ -226,6 +239,10 @@ type provider struct { ssoIssuer string ssoURL string + // use HTTP Redirect Binding for AuthnRequest + // if false, HTTP Post Binding is used + redirectBinding bool + now func() time.Time // If nil, don't do signature validation. @@ -244,19 +261,24 @@ type provider struct { logger log.Logger } -func (p *provider) POSTData(s connector.Scopes, id string) (action, value string, err error) { - +func (p *provider) AuthnRequest(s connector.Scopes, id string) (action, value string, post bool, err error) { r := &authnRequest{ - ProtocolBinding: bindingPOST, - ID: id, - IssueInstant: xmlTime(p.now()), - Destination: p.ssoURL, + ID: id, + IssueInstant: xmlTime(p.now()), + Destination: p.ssoURL, NameIDPolicy: &nameIDPolicy{ AllowCreate: true, Format: p.nameIDPolicyFormat, }, AssertionConsumerServiceURL: p.redirectURI, } + + if p.redirectBinding { + r.ProtocolBinding = bindingRedirect + } else { + r.ProtocolBinding = bindingPOST + } + if p.entityIssuer != "" { // Issuer for the request is optional. For example, okta always ignores // this value. @@ -265,12 +287,26 @@ func (p *provider) POSTData(s connector.Scopes, id string) (action, value string data, err := xml.MarshalIndent(r, "", " ") if err != nil { - return "", "", fmt.Errorf("marshal authn request: %v", err) + return "", "", false, fmt.Errorf("marshal authn request: %v", err) } + // Encode data according to specified binding // See: https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf - // "3.5.4 Message Encoding" - return p.ssoURL, base64.StdEncoding.EncodeToString(data), nil + var encoded string + if p.redirectBinding { // "3.4.4 Message Encoding" + b := new(bytes.Buffer) + compressor, err := flate.NewWriter(b, 0) // no compression + if err != nil { + return "", "", false, fmt.Errorf("encode authn request: %v", err) + } + compressor.Write(data) + compressor.Close() + encoded = base64.URLEncoding.EncodeToString(b.Bytes()) + } else { // "3.5.4 Message Encoding" + encoded = base64.StdEncoding.EncodeToString(data) + } + + return p.ssoURL, encoded, !p.redirectBinding, nil } // HandlePOST interprets a request from a SAML provider attempting to verify a diff --git a/server/handlers.go b/server/handlers.go index 9bff36ee84..6e47136ca8 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -23,6 +23,23 @@ import ( "github.com/dexidp/dex/storage" ) +const samlPOSTFormFmt = ` + + + + SAML login + + +
+ + +
+ + +` + // newHealthChecker returns the healthz handler. The handler runs until the // provided context is canceled. func (s *Server) newHealthChecker(ctx context.Context) http.Handler { @@ -333,30 +350,28 @@ 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, authnReq, post, err := conn.AuthnRequest(scopes, authReqID) if err != nil { s.logger.Errorf("Creating SAML data: %v", err) s.renderError(w, http.StatusInternalServerError, "Connector Login Error") return } - // TODO(ericchiang): Don't inline this. - fmt.Fprintf(w, ` - - - - SAML login - - -
- - -
- - - `, action, value, authReqID) + if post { + fmt.Fprintf(w, samlPOSTFormFmt, ssoURL, authnReq, authReqID) + } else { // HTTP Redirect Binding + u, err := url.Parse(ssoURL) + if err != nil { + s.logger.Errorf("Parse SSO URL: %v", err) + s.renderError(w, http.StatusInternalServerError, "Connector Login error.") + return + } + v := url.Values{} + v.Set("SAMLRequest", authnReq) + v.Set("RelayState", authReqID) + u.RawQuery = v.Encode() + http.Redirect(w, r, u.String(), http.StatusFound) + } default: s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.") }