*: wire up SAML POST binding
This commit is contained in:
		
							
								
								
									
										72
									
								
								Documentation/saml-connector.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								Documentation/saml-connector.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | ``` | ||||||
| @@ -37,6 +37,7 @@ More docs for running dex as a Kubernetes authenticator can be found [here](Docu | |||||||
| * Identity provider logins | * Identity provider logins | ||||||
|   * [LDAP](Documentation/ldap-connector.md) |   * [LDAP](Documentation/ldap-connector.md) | ||||||
|   * [GitHub](Documentation/github-connector.md) |   * [GitHub](Documentation/github-connector.md) | ||||||
|  |   * [SAML 2.0 (experimental)](Documentation/saml-connector.md) | ||||||
| * Client libraries | * Client libraries | ||||||
|   * [Go][go-oidc] |   * [Go][go-oidc] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ import ( | |||||||
| 	"github.com/coreos/dex/connector/ldap" | 	"github.com/coreos/dex/connector/ldap" | ||||||
| 	"github.com/coreos/dex/connector/mock" | 	"github.com/coreos/dex/connector/mock" | ||||||
| 	"github.com/coreos/dex/connector/oidc" | 	"github.com/coreos/dex/connector/oidc" | ||||||
|  | 	"github.com/coreos/dex/connector/saml" | ||||||
| 	"github.com/coreos/dex/server" | 	"github.com/coreos/dex/server" | ||||||
| 	"github.com/coreos/dex/storage" | 	"github.com/coreos/dex/storage" | ||||||
| 	"github.com/coreos/dex/storage/kubernetes" | 	"github.com/coreos/dex/storage/kubernetes" | ||||||
| @@ -182,6 +183,7 @@ var connectors = map[string]func() ConnectorConfig{ | |||||||
| 	"ldap":             func() ConnectorConfig { return new(ldap.Config) }, | 	"ldap":             func() ConnectorConfig { return new(ldap.Config) }, | ||||||
| 	"github":           func() ConnectorConfig { return new(github.Config) }, | 	"github":           func() ConnectorConfig { return new(github.Config) }, | ||||||
| 	"oidc":             func() ConnectorConfig { return new(oidc.Config) }, | 	"oidc":             func() ConnectorConfig { return new(oidc.Config) }, | ||||||
|  | 	"samlExperimental": func() ConnectorConfig { return new(saml.Config) }, | ||||||
| } | } | ||||||
|  |  | ||||||
| // UnmarshalJSON allows Connector to implement the unmarshaler interface to | // UnmarshalJSON allows Connector to implement the unmarshaler interface to | ||||||
|   | |||||||
| @@ -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 { | 			if err := s.templates.password(w, authReqID, r.URL.String(), "", false); err != nil { | ||||||
| 				s.logger.Errorf("Server template error: %v", err) | 				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, `<!DOCTYPE html> | ||||||
|  | 			  <html lang="en"> | ||||||
|  | 			  <head> | ||||||
|  | 			    <meta http-equiv="content-type" content="text/html; charset=utf-8"> | ||||||
|  | 			    <title>SAML login</title> | ||||||
|  | 			  </head> | ||||||
|  | 			  <body> | ||||||
|  | 			    <form method="post" action="%s" > | ||||||
|  | 				    <input type="hidden" name="SAMLRequest" value="%s" /> | ||||||
|  | 				    <input type="hidden" name="RelayState" value="%s" /> | ||||||
|  | 			    </form> | ||||||
|  | 				<script> | ||||||
|  | 				    document.forms[0].submit(); | ||||||
|  | 				</script> | ||||||
|  | 			  </body> | ||||||
|  | 			  </html>`, action, value, authReqID) | ||||||
| 		default: | 		default: | ||||||
| 			s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.") | 			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) { | func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) { | ||||||
| 	// SAML redirect bindings use the "RelayState" URL query field. When we support | 	var authID string | ||||||
| 	// SAML, we'll have to check that field too and possibly let callback connectors | 	switch r.Method { | ||||||
| 	// indicate which field is used to determine the state. | 	case "GET": // OAuth2 callback | ||||||
| 	// | 		if authID = r.URL.Query().Get("state"); authID == "" { | ||||||
| 	// 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.") | 			s.renderError(w, http.StatusBadRequest, "User session error.") | ||||||
| 			return | 			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 != nil { | ||||||
| 		if err == storage.ErrNotFound { | 		if err == storage.ErrNotFound { | ||||||
| 			s.logger.Errorf("Invalid 'state' parameter provided: %v", err) | 			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.") | 		s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") | ||||||
| 		return | 		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.") | 		s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	identity, err := callbackConnector.HandleCallback(parseScopes(authReq.Scopes), r) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		s.logger.Errorf("Failed to authenticate: %v", err) | 		s.logger.Errorf("Failed to authenticate: %v", err) | ||||||
| 		s.renderError(w, http.StatusInternalServerError, "Failed to return user's identity.") | 		s.renderError(w, http.StatusInternalServerError, "Failed to return user's identity.") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user