*: 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 | ||||
|   * [LDAP](Documentation/ldap-connector.md) | ||||
|   * [GitHub](Documentation/github-connector.md) | ||||
|   * [SAML 2.0 (experimental)](Documentation/saml-connector.md) | ||||
| * Client libraries | ||||
|   * [Go][go-oidc] | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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, `<!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: | ||||
| 			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.") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user