*: wire up SAML POST binding
This commit is contained in:
parent
31dfb54b6f
commit
0f4a1f69c5
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