Skip to content

Commit

Permalink
Merge pull request #17 from goware/signed_saml_requests
Browse files Browse the repository at this point in the history
Signing SAML HTTP-POST requests.
  • Loading branch information
diogogmt authored Oct 16, 2018
2 parents e1dc677 + 31686d6 commit c3c3b8d
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 147 deletions.
24 changes: 5 additions & 19 deletions idp_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"encoding/base64"
"encoding/xml"
"io/ioutil"
"net/http"
"text/template"

Expand Down Expand Up @@ -44,18 +43,8 @@ func (idp *IdentityProvider) NewLoginRequest(spMetadataURL string, authFn Authen
}

func (idp *IdentityProvider) GenerateResponse(samlRequest, relayState string, sess *Session, address string) ([]byte, error) {
data, err := base64.StdEncoding.DecodeString(samlRequest)
if err != nil {
return nil, errors.Wrap(err, "failed to decode saml request")
}
buf, err := ioutil.ReadAll(bytes.NewBuffer(data))
if err != nil {
return nil, errors.Wrap(err, "failed to read saml request")
}

var authnRequest AuthnRequest
err = xml.Unmarshal(buf, &authnRequest)
if err != nil {
if err := xml.Unmarshal([]byte(samlRequest), &authnRequest); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal saml request")
}

Expand All @@ -65,22 +54,19 @@ func (idp *IdentityProvider) GenerateResponse(samlRequest, relayState string, se
Request: authnRequest,
}

err = idpAuthnRequest.MakeAssertion(sess)
if err != nil {
if err := idpAuthnRequest.MakeAssertion(sess); err != nil {
return nil, errors.Wrap(err, "failed to make assertion")
}

err = idpAuthnRequest.MarshalAssertion()
if err != nil {
if err := idpAuthnRequest.MarshalAssertion(); err != nil {
return nil, errors.Wrap(err, "failed to marshal assertion")
}

err = idpAuthnRequest.MakeResponse()
if err != nil {
if err := idpAuthnRequest.MakeResponse(); err != nil {
return nil, errors.Wrap(err, "failed to build response")
}

buf, err = xml.MarshalIndent(idpAuthnRequest.Response, "", "\t")
buf, err := xml.MarshalIndent(idpAuthnRequest.Response, "", "\t")
if err != nil {
return nil, errors.Wrap(err, "failed to format response")
}
Expand Down
6 changes: 0 additions & 6 deletions metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@ import (
"github.com/pkg/errors"
)

// HTTPPostBinding is the official URN for the HTTP-POST binding (transport)
const HTTPPostBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"

// HTTPRedirectBinding is the official URN for the HTTP-Redirect binding (transport)
const HTTPRedirectBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"

// EntitiesDescriptor represents the SAML object of the same name.
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf section 2.3.1
Expand Down
121 changes: 101 additions & 20 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,87 @@ import (
"github.com/goware/saml/xmlsec"
)

const (
// HTTPPostBinding is the official URN for the HTTP-POST binding (transport)
HTTPPostBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"

// HTTPRedirectBinding is the official URN for the HTTP-Redirect binding (transport)
HTTPRedirectBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
)

const (
ProtocolNamespace = "urn:oasis:names:tc:SAML:2.0:protocol"

NameIDEntityFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"

NameIDEmailAddressFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
)

const (
CryptoSHA256 = "http://www.w3.org/2001/04/xmlenc#sha256"
)

// AuthnRequest represents the SAML object of the same name, a request from a service provider
// to authenticate a user.
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf sec 3.4.1 Element <AuthnRequest>
type AuthnRequest struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol AuthnRequest"`
AssertionConsumerServiceURL string `xml:",attr"`
Destination string `xml:",attr"`
ID string `xml:",attr"`
IssueInstant time.Time `xml:",attr"`
ProtocolBinding string `xml:",attr"`
Version string `xml:",attr"`
Issuer Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
Signature *xmlsec.Signature `xml:"http://www.w3.org/2000/09/xmldsig# Signature"`
NameIDPolicy NameIDPolicy `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"`
// Since multiple namespaces can be used, don't hardcode in the element
XMLName xml.Name
// Spec lists that the xmlns also needs to be namespaced: https://docs.oasis-open.org/security/saml/v2.0/saml-schema-protocol-2.0.xsd
// TODO: create custom marshaler
XMLNamespace string `xml:"xmlns:samlp,attr,omitempty"`

Signature *xmlsec.Signature `xml:"http://www.w3.org/2000/09/xmldsig# Signature"`

// Required attributes
//

// An identifier for the request.
// The values of the ID attribute in a request and the InResponseTo
// attribute in the corresponding response MUST match.
ID string `xml:",attr"`

// The version of this request.
// Only version 2.0 is supported by goware/saml
Version string `xml:",attr"`

// The time instant of issue of the request. The time value is encoded in UTC
IssueInstant time.Time `xml:",attr"`

// Optional attributes
//

// Identifies the entity that generated the request message
// By default, the value of the <Issuer> element is a URI of no more than 1024 characters.
// Changes from SAML version 1 to 2
// An <Issuer> element can now be present on requests and responses (in addition to appearing on assertions).
Issuer Issuer

// A URI reference indicating the address to which this request has been sent. This is useful to prevent
// malicious forwarding of requests to unintended recipients, a protection that is required by some
// protocol bindings. If it is present, the actual recipient MUST check that the URI reference identifies the
// location at which the message was received. If it does not, the request MUST be discarded. Some
// protocol bindings may require the use of this attribute (see [SAMLBind]).
Destination string `xml:",attr"`

// Specifies by value the location to which the <Response> message MUST be returned to the
// requester. The responder MUST ensure by some means that the value specified is in fact associated
// with the requester. [SAMLMeta] provides one possible mechanism; signing the enclosing
// <AuthnRequest> message is another. This attribute is mutually exclusive with the
// AssertionConsumerServiceIndex attribute and is typically accompanied by the ProtocolBinding attribute.
AssertionConsumerServiceURL string `xml:",attr"`

// A URI reference that identifies a SAML protocol binding to be used when returning the <Response>
// message. See [SAMLBind] for more information about protocol bindings and URI references defined
// for them. This attribute is mutually exclusive with the AssertionConsumerServiceIndex attribute
// and is typically accompanied by the AssertionConsumerServiceURL attribute.
ProtocolBinding string `xml:",attr"`

// Specifies constraints on the name identifier to be used to represent the requested subject.
// If omitted, then any type of identifier supported by the identity provider for the requested
// subject can be used, constrained by any relevant deployment-specific policies, with respect to privacy.
NameIDPolicy NameIDPolicy
}

// Issuer represents the SAML object of the same name.
Expand All @@ -60,10 +126,26 @@ type Issuer struct {
// NameIDPolicy represents the SAML object of the same name.
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
// Also refer to Azure docs for their IdP supported values: https://msdn.microsoft.com/en-us/library/azure/dn195589.aspx
type NameIDPolicy struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"`
AllowCreate bool `xml:",attr"`
Format string `xml:",chardata"`
XMLName xml.Name

// Optional attributes
//

// A Boolean value used to indicate whether the identity provider is allowed, in the course of fulfilling the
// request, to create a new identifier to represent the principal. Defaults to "false". When "false", the
// requester constrains the identity provider to only issue an assertion to it if an acceptable identifier for
// the principal has already been established. Note that this does not prevent the identity provider from
// creating such identifiers outside the context of this specific request (for example, in advance for a
// large number of principals)
AllowCreate bool `xml:",attr"`

// Specifies the URI reference corresponding to a name identifier format defined in this or another
// specification (see Section 8.3 for examples). The additional value of
// urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted is defined specifically for use
// within this attribute to indicate a request that the resulting identifier be encrypted
Format string `xml:",attr"`
}

// Response represents the SAML object of the same name.
Expand All @@ -88,7 +170,7 @@ type Response struct {
IssueInstant time.Time `xml:",attr"`

// A code representing the status of the corresponding reques
Status *Status `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"`
Status *Status

// Optional attributes
//
Expand All @@ -114,13 +196,11 @@ type Response struct {
// By default, the value of the <Issuer> element is a URI of no more than 1024 characters.
// Changes from SAML version 1 to 2
// An <Issuer> element can now be present on requests and responses (in addition to appearing on assertions).
Issuer *Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
Issuer *Issuer

EncryptedAssertion *EncryptedAssertion

Assertion *Assertion `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"`

XMLText []byte `xml:"-"`
Assertion *Assertion
}

// Status represents the SAML object of the same name.
Expand All @@ -147,6 +227,7 @@ var StatusSuccess = "urn:oasis:names:tc:SAML:2.0:status:Success"
//
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
type EncryptedAssertion struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion EncryptedAssertion"`
Assertion *Assertion
EncryptedData []byte `xml:",innerxml"`
}
Expand All @@ -159,7 +240,7 @@ type Assertion struct {
ID string `xml:",attr"`
IssueInstant time.Time `xml:",attr"`
Version string `xml:",attr"`
Issuer *Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
Issuer *Issuer
Signature *xmlsec.Signature
Subject *Subject
Conditions *Conditions
Expand Down
64 changes: 13 additions & 51 deletions sp.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package saml

import (
"bytes"
"compress/flate"
"encoding/base64"
"encoding/pem"
"encoding/xml"
"io/ioutil"
"net/http"
"net/url"
"os"
"sync/atomic"

Expand Down Expand Up @@ -71,6 +68,9 @@ type ServiceProvider struct {

// URL Target of the IdP where the SP will send the AuthnRequest message
IdPSSOServiceURL string

// Whether to sign the SAML Request sent to the IdP to initiate the SSO workflow
IdPSignSAMLRequest bool
}

// PrivkeyFile returns a physical path where the SP's key can be accessed.
Expand Down Expand Up @@ -221,49 +221,6 @@ func (sp *ServiceProvider) Metadata() (*Metadata, error) {
return metadata, nil
}

// NewSAMLRequest creates SAML 2.0 AuthnRequest
// The <AuthnRequest> XML element is deflate-compressed, base64 and URL encoded
func (sp *ServiceProvider) NewRedirectSAMLRequest() (string, error) {
authnRequest, err := sp.NewAuthnRequest()
if err != nil {
return "", errors.Wrap(err, "failed to create auth request")
}

buf, err := xml.MarshalIndent(authnRequest, "", "\t")
if err != nil {
return "", errors.Wrap(err, "failed to marshal auth request")
}

flateBuf := bytes.NewBuffer(nil)
flateWriter, err := flate.NewWriter(flateBuf, flate.DefaultCompression)
if err != nil {
return "", errors.Wrap(err, "failed to create flate writer")
}

_, err = flateWriter.Write(buf)
if err != nil {
return "", errors.Wrap(err, "failed to write to flate writer")
}
flateWriter.Close()
return url.QueryEscape(base64.StdEncoding.EncodeToString(flateBuf.Bytes())), nil
}

// NewSAMLRequest creates SAML 2.0 AuthnRequest
// The <AuthnRequest> XML element is base64 encoded
func (sp *ServiceProvider) NewPostSAMLRequest() (string, error) {
authnRequest, err := sp.NewAuthnRequest()
if err != nil {
return "", errors.Wrap(err, "failed to create auth request")
}

buf, err := xml.MarshalIndent(authnRequest, "", "\t")
if err != nil {
return "", errors.Wrap(err, "failed to marshal auth request")
}

return base64.StdEncoding.EncodeToString(buf), nil
}

// NewAuthnRequest creates a new AuthnRequest object for the given IdP URL.
func (sp *ServiceProvider) NewAuthnRequest() (*AuthnRequest, error) {
req := AuthnRequest{
Expand All @@ -272,17 +229,22 @@ func (sp *ServiceProvider) NewAuthnRequest() (*AuthnRequest, error) {
ID: NewID(),
IssueInstant: Now(),
Version: "2.0",
ProtocolBinding: HTTPPostBinding,
Issuer: Issuer{
Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
Format: NameIDEntityFormat,
Value: sp.MetadataURL,
},
NameIDPolicy: NameIDPolicy{
AllowCreate: true,
// TODO(ross): figure out exactly policy we need
// urn:mace:shibboleth:1.0:nameIdentifier
// urn:oasis:names:tc:SAML:2.0:nameid-format:transient
Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
Format: NameIDEmailAddressFormat,
},
}

// Spec lists that the xmlns also needs to be namespaced: https://docs.oasis-open.org/security/saml/v2.0/saml-schema-protocol-2.0.xsd
// TODO: create custom marshaler
req.XMLNamespace = ProtocolNamespace
req.XMLName.Local = "samlp:AuthnRequest"
req.NameIDPolicy.XMLName.Local = "samlp:NameIDPolicy"

return &req, nil
}
Loading

0 comments on commit c3c3b8d

Please sign in to comment.