*: wire up SAML POST binding

This commit is contained in:
Eric Chiang 2016-12-20 17:24:32 -08:00
parent 31dfb54b6f
commit 0f4a1f69c5
4 changed files with 138 additions and 19 deletions

View 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
```

View file

@ -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]

View file

@ -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

View file

@ -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.")