Skip to content

Commit

Permalink
connector/saml: add redirect binding support
Browse files Browse the repository at this point in the history
This change adds support for using the "HTTP Redirect Binding" for
AuthnRequests. Note that it's uncommon to use the Redirect Binding for
consuming assertions.

To enable redirect, add

    redirectBinding: true

to your SAML connector's config section.

If not set, it will default to the current behaviour, HTTP POST Binding.

Signed-off-by: Stephan Renatus <[email protected]>
  • Loading branch information
srenatus committed Sep 6, 2019
1 parent 8427f0f commit 6bb255b
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 44 deletions.
5 changes: 4 additions & 1 deletion Documentation/connectors/saml.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## Overview

The SAML provider allows authentication through the SAML 2.0 HTTP POST binding. The connector maps attribute values in the SAML assertion to user info, such as username, email, and groups.
The SAML provider allows authentication through the SAML 2.0 HTTP POST Binding or HTTP Redirect Binding.
The callback acts as the "Assertion Consumer Service", expecting assertions via the HTTP POST Binding.
The connector maps attribute values in the SAML assertion to user info, such as username, email, and groups.

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.

Expand All @@ -26,6 +28,7 @@ connectors:
config:
# SSO URL used for POST value.
ssoURL: https://saml.example.com/sso
redirectBinding: true # use HTTP Redirect Binding (defaults to false)

# CA to use when validating the signature of the SAML response.
ca: /path/to/ca.pem
Expand Down
10 changes: 4 additions & 6 deletions connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,10 @@ type CallbackConnector interface {
// See: https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
// "3.5 HTTP POST Binding"
type SAMLConnector interface {
// POSTData returns an encoded SAML request and SSO URL for the server to
// render a POST form with.
//
// POSTData should encode the provided request ID in the returned serialized
// SAML request.
POSTData(s Scopes, requestID string) (ssoURL, samlRequest string, err error)
// AuthnRequest returns an encoded SAML request and SSO URL for the server to
// render a POST form with, or send via the Redirect Binding, depending on the
// connector's configuration.
AuthnRequest(s Scopes, requestID string) (sooURL, samlRequest string, post bool, err error)

// HandlePOST decodes, verifies, and maps attributes from the SAML response.
// It passes the expected value of the "InResponseTo" response field, which
Expand Down
74 changes: 55 additions & 19 deletions connector/saml/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package saml

import (
"bytes"
"compress/flate"
"crypto/x509"
"encoding/base64"
"encoding/pem"
Expand Down Expand Up @@ -84,6 +85,17 @@ type Config struct {
SSOIssuer string `json:"ssoIssuer"`
SSOURL string `json:"ssoURL"`

// Use HTTP Redirect Binding instead of HTTP Post Binding for AuthnRequest
// See: https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
// "3.4.4 Message Encoding" for HTTP Redirect Binding
// "3.5.4 Message Encoding" for HTTP POST Binding
//
// If not provided, will use HTTP Post Binding.
// If set to true, this will use the DEFLATE encoding specified in the spec
// referenced above. It will NOT pass SAMLEncoding set to
// urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE
RedirectBinding bool `json:"redirectBinding"`

// X509 CA file or raw data to verify XML signatures.
CA string `json:"ca"`
CAData []byte `json:"caData"`
Expand Down Expand Up @@ -154,16 +166,17 @@ func (c *Config) openConnector(logger log.Logger) (*provider, error) {
}

p := &provider{
entityIssuer: c.EntityIssuer,
ssoIssuer: c.SSOIssuer,
ssoURL: c.SSOURL,
now: time.Now,
usernameAttr: c.UsernameAttr,
emailAttr: c.EmailAttr,
groupsAttr: c.GroupsAttr,
groupsDelim: c.GroupsDelim,
redirectURI: c.RedirectURI,
logger: logger,
entityIssuer: c.EntityIssuer,
ssoIssuer: c.SSOIssuer,
ssoURL: c.SSOURL,
redirectBinding: c.RedirectBinding,
now: time.Now,
usernameAttr: c.UsernameAttr,
emailAttr: c.EmailAttr,
groupsAttr: c.GroupsAttr,
groupsDelim: c.GroupsDelim,
redirectURI: c.RedirectURI,
logger: logger,

nameIDPolicyFormat: c.NameIDPolicyFormat,
}
Expand Down Expand Up @@ -226,6 +239,10 @@ type provider struct {
ssoIssuer string
ssoURL string

// use HTTP Redirect Binding for AuthnRequest
// if false, HTTP Post Binding is used
redirectBinding bool

now func() time.Time

// If nil, don't do signature validation.
Expand All @@ -244,19 +261,24 @@ type provider struct {
logger log.Logger
}

func (p *provider) POSTData(s connector.Scopes, id string) (action, value string, err error) {

func (p *provider) AuthnRequest(s connector.Scopes, id string) (action, value string, post bool, err error) {
r := &authnRequest{
ProtocolBinding: bindingPOST,
ID: id,
IssueInstant: xmlTime(p.now()),
Destination: p.ssoURL,
ID: id,
IssueInstant: xmlTime(p.now()),
Destination: p.ssoURL,
NameIDPolicy: &nameIDPolicy{
AllowCreate: true,
Format: p.nameIDPolicyFormat,
},
AssertionConsumerServiceURL: p.redirectURI,
}

if p.redirectBinding {
r.ProtocolBinding = bindingRedirect
} else {
r.ProtocolBinding = bindingPOST
}

if p.entityIssuer != "" {
// Issuer for the request is optional. For example, okta always ignores
// this value.
Expand All @@ -265,12 +287,26 @@ func (p *provider) POSTData(s connector.Scopes, id string) (action, value string

data, err := xml.MarshalIndent(r, "", " ")
if err != nil {
return "", "", fmt.Errorf("marshal authn request: %v", err)
return "", "", false, fmt.Errorf("marshal authn request: %v", err)
}

// Encode data according to specified binding
// See: https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
// "3.5.4 Message Encoding"
return p.ssoURL, base64.StdEncoding.EncodeToString(data), nil
var encoded string
if p.redirectBinding { // "3.4.4 Message Encoding"
b := new(bytes.Buffer)
compressor, err := flate.NewWriter(b, 0) // no compression
if err != nil {
return "", "", false, fmt.Errorf("encode authn request: %v", err)
}
compressor.Write(data)
compressor.Close()
encoded = base64.URLEncoding.EncodeToString(b.Bytes())
} else { // "3.5.4 Message Encoding"
encoded = base64.StdEncoding.EncodeToString(data)
}

return p.ssoURL, encoded, !p.redirectBinding, nil
}

// HandlePOST interprets a request from a SAML provider attempting to verify a
Expand Down
51 changes: 33 additions & 18 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ import (
"github.com/dexidp/dex/storage"
)

const samlPOSTFormFmt = `<!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>`

// newHealthChecker returns the healthz handler. The handler runs until the
// provided context is canceled.
func (s *Server) newHealthChecker(ctx context.Context) http.Handler {
Expand Down Expand Up @@ -333,30 +350,28 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
s.logger.Errorf("Server template error: %v", err)
}
case connector.SAMLConnector:
action, value, err := conn.POSTData(scopes, authReqID)
ssoURL, authnReq, post, err := conn.AuthnRequest(scopes, authReqID)
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)
if post {
fmt.Fprintf(w, samlPOSTFormFmt, ssoURL, authnReq, authReqID)
} else { // HTTP Redirect Binding
u, err := url.Parse(ssoURL)
if err != nil {
s.logger.Errorf("Parse SSO URL: %v", err)
s.renderError(w, http.StatusInternalServerError, "Connector Login error.")
return
}
v := url.Values{}
v.Set("SAMLRequest", authnReq)
v.Set("RelayState", authReqID)
u.RawQuery = v.Encode()
http.Redirect(w, r, u.String(), http.StatusFound)
}
default:
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.")
}
Expand Down

0 comments on commit 6bb255b

Please sign in to comment.