Skip to content

Commit

Permalink
*: wire up SAML POST binding
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Chiang committed Jan 10, 2017
1 parent 31dfb54 commit 0f4a1f6
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 19 deletions.
72 changes: 72 additions & 0 deletions 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
```
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -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]

Expand Down
12 changes: 7 additions & 5 deletions cmd/dex/config.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
72 changes: 58 additions & 14 deletions server/handlers.go
Expand Up @@ -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.")
}
Expand Down Expand Up @@ -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)
Expand All @@ -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.")
Expand Down

0 comments on commit 0f4a1f6

Please sign in to comment.