Skip to content

Commit

Permalink
support SAML HTTP redirect binding
Browse files Browse the repository at this point in the history
  • Loading branch information
easeway committed Aug 23, 2017
1 parent e40c01e commit dd18a33
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 26 deletions.
9 changes: 9 additions & 0 deletions Documentation/saml-connector.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ connectors:
# urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
#
nameIDPolicyFormat: persistent

# Optional: Specify binding to use, default is HTTP-POST
#
# Some identity provider may require specific binding
# supported values:
# - post: use HTTP-POST binding (default)
# - redirect: use HTTP redirect binding
#
# binding: post
```

A minimal working configuration might look like:
Expand Down
14 changes: 10 additions & 4 deletions connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,24 @@ type CallbackConnector interface {
HandleCallback(s Scopes, r *http.Request) (identity Identity, err error)
}

// SAMLConnector bindings
const (
SAMLBindingPOST = "post"
SAMLBindingRedirect = "redirect"
)

// SAMLConnector represents SAML connectors which implement the HTTP POST binding.
// RelayState is handled by the server.
//
// 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.
// AuthnRequest builds an encoded SAML request and SSO URL for the server to
// render a POST form or reply a redirection depending on binding.
//
// POSTData should encode the provided request ID in the returned serialized
// AuthnRequest should encode the provided request ID in the returned serialized
// SAML request.
POSTData(s Scopes, requestID string) (sooURL, samlRequest string, err error)
AuthnRequest(s Scopes, requestID string) (binding, ssoURL, samlRequest string, 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
48 changes: 44 additions & 4 deletions connector/saml/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
package saml

import (
"bytes"
"compress/flate"
"crypto/x509"
"encoding/base64"
"encoding/pem"
Expand Down Expand Up @@ -113,6 +115,9 @@ type Config struct {
// urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
//
NameIDPolicyFormat string `json:"nameIDPolicyFormat"`

// Specify which binding to use, default is post
Binding string `json:"binding"`
}

type certStore struct {
Expand Down Expand Up @@ -165,6 +170,14 @@ func (c *Config) openConnector(logger logrus.FieldLogger) (*provider, error) {
logger: logger,

nameIDPolicyFormat: c.NameIDPolicyFormat,
binding: strings.ToLower(strings.TrimSpace(c.Binding)),
}

if p.binding == "" {
p.binding = connector.SAMLBindingPOST
} else if p.binding != connector.SAMLBindingPOST &&
p.binding != connector.SAMLBindingRedirect {
return nil, fmt.Errorf("unsupported binding: %s", p.binding)
}

if p.nameIDPolicyFormat == "" {
Expand Down Expand Up @@ -236,11 +249,12 @@ type provider struct {

nameIDPolicyFormat string

binding string

logger logrus.FieldLogger
}

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

func (p *provider) AuthnRequest(s connector.Scopes, id string) (binding, ssoURL, value string, err error) {
r := &authnRequest{
ProtocolBinding: bindingPOST,
ID: id,
Expand All @@ -252,6 +266,7 @@ func (p *provider) POSTData(s connector.Scopes, id string) (action, value string
},
AssertionConsumerServiceURL: p.redirectURI,
}

if p.entityIssuer != "" {
// Issuer for the request is optional. For example, okta always ignores
// this value.
Expand All @@ -260,12 +275,20 @@ 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 "", "", "", fmt.Errorf("marshal authn request: %v", err)
}

// for redirect binding, SAMLRequest must be deflated
if p.binding == connector.SAMLBindingRedirect {
data, err = deflate(data)
if err != nil {
return "", "", "", fmt.Errorf("deflate request: %v", err)
}
}

// 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
return p.binding, p.ssoURL, base64.StdEncoding.EncodeToString(data), nil
}

// HandlePOST interprets a request from a SAML provider attempting to verify a
Expand Down Expand Up @@ -598,3 +621,20 @@ func before(now, notBefore time.Time) bool {
func after(now, notOnOrAfter time.Time) bool {
return now.After(notOnOrAfter.Add(allowedClockDrift))
}

// deflate compresses the data
func deflate(data []byte) ([]byte, error) {
var buf bytes.Buffer
zw, err := flate.NewWriter(&buf, flate.DefaultCompression)
if err != nil {
return nil, err
}
defer zw.Close()
if _, err = zw.Write(data); err != nil {
return nil, err
}
if err = zw.Flush(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
47 changes: 29 additions & 18 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,30 +254,41 @@ 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)
binding, ssoURL, value, 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)
switch binding {
case connector.SAMLBindingPOST:
// 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>`, ssoURL, value, authReqID)
case connector.SAMLBindingRedirect:
query := make(url.Values)
query.Set("SAMLRequest", value)
query.Set("RelayState", authReqID)
redirectURL := ssoURL + "?" + query.Encode()
http.Redirect(w, r, redirectURL, http.StatusFound)
default:
s.renderError(w, http.StatusInternalServerError, "Invalid SAML configuration.")
}
default:
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.")
}
Expand Down

0 comments on commit dd18a33

Please sign in to comment.