forked from mystiq/dex
*: wire up SAML POST binding
This commit is contained in:
parent
31dfb54b6f
commit
0f4a1f69c5
4 changed files with 138 additions and 19 deletions
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.")
|
||||
|
|
Loading…
Reference in a new issue