From 0f4a1f69c5c3e70fa325f92bd6c125f7248977f6 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Tue, 20 Dec 2016 17:24:32 -0800 Subject: [PATCH] *: wire up SAML POST binding --- Documentation/saml-connector.md | 72 +++++++++++++++++++++++++++++++++ README.md | 1 + cmd/dex/config.go | 12 +++--- server/handlers.go | 72 ++++++++++++++++++++++++++------- 4 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 Documentation/saml-connector.md diff --git a/Documentation/saml-connector.md b/Documentation/saml-connector.md new file mode 100644 index 00000000..d78a6f2b --- /dev/null +++ b/Documentation/saml-connector.md @@ -0,0 +1,72 @@ +# Authentication through SAML 2.0 + +## Overview + +The experimental SAML provider allows authentication through the SAML 2.0 HTTP POST binding. + +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. + +## Caveats + +There are known issues with the XML signature validation for this connector. In addition work is still being done to ensure this connector implements best security practices for SAML 2.0. + +The connector doesn't support signed AuthnRequests or encrypted attributes. + +The connector doesn't support refresh tokens since the SAML 2.0 protocol doesn't provide a way to requery a provider without interaction. + +## Configuration + +```yaml +connectors: +- type: samlExperimental # will be changed to "saml" later without support for the "samlExperimental" value + id: saml + config: + # Issuer used for validating the SAML response. + issuer: https://saml.example.com + # SSO URL used for POST value. + ssoURL: https://saml.example.com/sso + + # CA to use when validating the SAML response. + ca: /path/to/ca.pem + + # CA's can also be provided inline as a base64'd blob. + # + # catData: ( RAW base64'd PEM encoded CA ) + + # To skip signature validation, uncomment the following field. This should + # only be used during testing and may be removed in the future. + # + # insucreSkipSignatureValidation: true + + # Dex's callback URL. Must match the "Destination" attribute of all responses + # exactly. + redirectURI: https://dex.example.com/callback + + # Name of attributes in the returned assertions to map to ID token claims. + usernameAttr: name + emailAttr: email + groupsAttr: groups # optional + + # By default, multiple groups are assumed to be represented as multiple + # attributes with the same name. + # + # If "groupsDelim" is provided groups are assumed to be represented as a + # single attribute and the delimiter is used to split the attribute's value + # into multiple groups. + # + # groupsDelim: ", " + + + # Requested format of the NameID. The NameID value is is mapped to the ID Token + # 'sub' claim. This can be an abbreviated form of the full URI with just the last + # component. For example, if this value is set to "emailAddress" the format will + # resolve to: + # + # urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + # + # If no value is specified, this value defaults to: + # + # urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + # + nameIDPolicyFormat: persistent +``` diff --git a/README.md b/README.md index 2812b2ac..ca3e08d7 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ More docs for running dex as a Kubernetes authenticator can be found [here](Docu * Identity provider logins * [LDAP](Documentation/ldap-connector.md) * [GitHub](Documentation/github-connector.md) + * [SAML 2.0 (experimental)](Documentation/saml-connector.md) * Client libraries * [Go][go-oidc] diff --git a/cmd/dex/config.go b/cmd/dex/config.go index 40a4fe61..19a87fc3 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -14,6 +14,7 @@ import ( "github.com/coreos/dex/connector/ldap" "github.com/coreos/dex/connector/mock" "github.com/coreos/dex/connector/oidc" + "github.com/coreos/dex/connector/saml" "github.com/coreos/dex/server" "github.com/coreos/dex/storage" "github.com/coreos/dex/storage/kubernetes" @@ -177,11 +178,12 @@ type ConnectorConfig interface { } var connectors = map[string]func() ConnectorConfig{ - "mockCallback": func() ConnectorConfig { return new(mock.CallbackConfig) }, - "mockPassword": func() ConnectorConfig { return new(mock.PasswordConfig) }, - "ldap": func() ConnectorConfig { return new(ldap.Config) }, - "github": func() ConnectorConfig { return new(github.Config) }, - "oidc": func() ConnectorConfig { return new(oidc.Config) }, + "mockCallback": func() ConnectorConfig { return new(mock.CallbackConfig) }, + "mockPassword": func() ConnectorConfig { return new(mock.PasswordConfig) }, + "ldap": func() ConnectorConfig { return new(ldap.Config) }, + "github": func() ConnectorConfig { return new(github.Config) }, + "oidc": func() ConnectorConfig { return new(oidc.Config) }, + "samlExperimental": func() ConnectorConfig { return new(saml.Config) }, } // UnmarshalJSON allows Connector to implement the unmarshaler interface to diff --git a/server/handlers.go b/server/handlers.go index c962265f..5a0b9b34 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -227,6 +227,31 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) { if err := s.templates.password(w, authReqID, r.URL.String(), "", false); err != nil { s.logger.Errorf("Server template error: %v", err) } + case connector.SAMLConnector: + action, value, err := conn.POSTData(scopes) + 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) default: s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.") } @@ -266,20 +291,24 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) { - // SAML redirect bindings use the "RelayState" URL query field. When we support - // SAML, we'll have to check that field too and possibly let callback connectors - // indicate which field is used to determine the state. - // - // See: - // https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf - // Section: "3.4.3 RelayState" - state := r.URL.Query().Get("state") - if state == "" { - s.renderError(w, http.StatusBadRequest, "User session error.") + var authID string + switch r.Method { + case "GET": // OAuth2 callback + if authID = r.URL.Query().Get("state"); authID == "" { + s.renderError(w, http.StatusBadRequest, "User session error.") + return + } + case "POST": // SAML POST binding + if authID = r.PostFormValue("RelayState"); authID == "" { + s.renderError(w, http.StatusBadRequest, "User session error.") + return + } + default: + s.renderError(w, http.StatusBadRequest, "Method not supported") return } - authReq, err := s.storage.GetAuthRequest(state) + authReq, err := s.storage.GetAuthRequest(authID) if err != nil { if err == storage.ErrNotFound { s.logger.Errorf("Invalid 'state' parameter provided: %v", err) @@ -296,13 +325,28 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") return } - callbackConnector, ok := conn.Connector.(connector.CallbackConnector) - if !ok { + + var identity connector.Identity + switch conn := conn.Connector.(type) { + case connector.CallbackConnector: + if r.Method != "GET" { + s.logger.Errorf("SAML request mapped to OAuth2 connector") + s.renderError(w, http.StatusBadRequest, "Invalid request") + return + } + identity, err = conn.HandleCallback(parseScopes(authReq.Scopes), r) + case connector.SAMLConnector: + if r.Method != "POST" { + s.logger.Errorf("OAuth2 request mapped to SAML connector") + s.renderError(w, http.StatusBadRequest, "Invalid request") + return + } + identity, err = conn.HandlePOST(parseScopes(authReq.Scopes), r.PostFormValue("SAMLResponse")) + default: s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") return } - identity, err := callbackConnector.HandleCallback(parseScopes(authReq.Scopes), r) if err != nil { s.logger.Errorf("Failed to authenticate: %v", err) s.renderError(w, http.StatusInternalServerError, "Failed to return user's identity.")