diff --git a/README.md b/README.md index 4967a94..828b6ce 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ For instance, New Relic allows you to configure a saml provider (https://newreli Ping Identity has a nice video for SAML here: https://www.pingidentity.com/resource-center/Introduction-to-SAML-Video.cfm +Shibboleth provides a test SAML IdP and SP endpoint at https://www.testshib.org/ + Installation ------------ diff --git a/authnrequest.go b/authnrequest.go index 904bae2..1a16ec1 100644 --- a/authnrequest.go +++ b/authnrequest.go @@ -15,8 +15,11 @@ package saml import ( + "bytes" + "compress/flate" "encoding/base64" "encoding/xml" + "errors" "fmt" "github.com/nu7hatch/gouuid" "io/ioutil" @@ -27,6 +30,11 @@ import ( "time" ) +const ( + BindingPOST = iota + BindingRedirect +) + func NewAuthorizationRequest(appSettings AppSettings, accountSettings AccountSettings) *AuthorizationRequest { myIdUUID, err := uuid.NewV4() if err != nil { @@ -39,9 +47,17 @@ func NewAuthorizationRequest(appSettings AppSettings, accountSettings AccountSet return &AuthorizationRequest{AccountSettings: accountSettings, AppSettings: appSettings, Id: "_" + myIdUUID.String(), IssueInstant: t} } -// GetRequest returns a string formatted XML document that represents the SAML document -// TODO: parameterize more parts of the request -func (ar AuthorizationRequest) GetRequest(base64Encode bool) (string, error) { +func (ar AuthorizationRequest) BuildRequest(binding int) (AuthnRequest, error) { + bindings := map[int]string{ + BindingPOST: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + BindingRedirect: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", + } + + bind, ok := bindings[binding] + if !ok { + return AuthnRequest{}, errors.New("invalid binding type requested") + } + d := AuthnRequest{ XMLName: xml.Name{ Local: "samlp:AuthnRequest", @@ -49,7 +65,7 @@ func (ar AuthorizationRequest) GetRequest(base64Encode bool) (string, error) { SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol", SAML: "urn:oasis:names:tc:SAML:2.0:assertion", ID: ar.Id, - ProtocolBinding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + ProtocolBinding: bind, Version: "2.0", AssertionConsumerServiceURL: ar.AppSettings.AssertionConsumerServiceURL, Issuer: Issuer{ @@ -82,68 +98,43 @@ func (ar AuthorizationRequest) GetRequest(base64Encode bool) (string, error) { }, }, } + + return d, nil +} + +// GetRequest returns a string formatted XML document that represents the SAML document +// TODO: parameterize more parts of the request +func (ar AuthorizationRequest) GetRequest(binding int) (string, error) { + d, err := ar.BuildRequest(binding) + if err != nil { + return "", err + } + b, err := xml.MarshalIndent(d, "", " ") if err != nil { return "", err } xmlAuthnRequest := fmt.Sprintf("\n%s", b) - - if base64Encode { - data := []byte(xmlAuthnRequest) - return base64.StdEncoding.EncodeToString(data), nil - } else { - return string(xmlAuthnRequest), nil - } + return string(xmlAuthnRequest), nil } // GetSignedRequest returns a string formatted XML document that represents the SAML document // TODO: parameterize more parts of the request -func (ar AuthorizationRequest) GetSignedRequest(base64Encode bool, publicCert string, privateCert string) (string, error) { +func (ar AuthorizationRequest) GetSignedRequest(binding int, publicCert string, privateCert string) (string, error) { cert, err := LoadCertificate(publicCert) if err != nil { return "", err } + base, err := ar.BuildRequest(binding) + if err != nil { + return "", err + } + d := AuthnSignedRequest{ - XMLName: xml.Name{ - Local: "samlp:AuthnRequest", - }, - SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol", - SAML: "urn:oasis:names:tc:SAML:2.0:assertion", - SAMLSIG: "http://www.w3.org/2000/09/xmldsig#", - ID: ar.Id, - ProtocolBinding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", - Version: "2.0", - AssertionConsumerServiceURL: ar.AppSettings.AssertionConsumerServiceURL, - Issuer: Issuer{ - XMLName: xml.Name{ - Local: "saml:Issuer", - }, - Url: ar.AppSettings.Issuer, - }, - IssueInstant: ar.IssueInstant, - NameIDPolicy: NameIDPolicy{ - XMLName: xml.Name{ - Local: "samlp:NameIDPolicy", - }, - AllowCreate: true, - Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", - }, - RequestedAuthnContext: RequestedAuthnContext{ - XMLName: xml.Name{ - Local: "samlp:RequestedAuthnContext", - }, - SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol", - Comparison: "exact", - }, - AuthnContextClassRef: AuthnContextClassRef{ - XMLName: xml.Name{ - Local: "saml:AuthnContextClassRef", - }, - SAML: "urn:oasis:names:tc:SAML:2.0:assertion", - Transport: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", - }, + AuthnRequest: base, + SAMLSIG: "http://www.w3.org/2000/09/xmldsig#", Signature: Signature{ XMLName: xml.Name{ Local: "samlsig:Signature", @@ -216,7 +207,15 @@ func (ar AuthorizationRequest) GetSignedRequest(base64Encode bool, publicCert st }, }, }, + AuthnContextClassRef: AuthnContextClassRef{ + XMLName: xml.Name{ + Local: "saml:AuthnContextClassRef", + }, + SAML: "urn:oasis:names:tc:SAML:2.0:assertion", + Transport: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", + }, } + b, err := xml.MarshalIndent(d, "", " ") if err != nil { return "", err @@ -252,29 +251,46 @@ func (ar AuthorizationRequest) GetSignedRequest(base64Encode bool, publicCert st return "", err } samlSignedRequestXml := strings.Trim(string(samlSignedRequest), "\n") - - if base64Encode { - data := []byte(samlSignedRequestXml) - return base64.StdEncoding.EncodeToString(data), nil - } else { - return string(samlSignedRequestXml), nil - } + return string(samlSignedRequestXml), nil } // String reqString = accSettings.getIdp_sso_target_url()+"?SAMLRequest=" + // AuthRequest.getRidOfCRLF(URLEncoder.encode(authReq.getRequest(AuthRequest.base64),"UTF-8")); -func (ar AuthorizationRequest) GetRequestUrl() (string, error) { +func (ar AuthorizationRequest) GetRequestUrl(binding int) (string, error) { u, err := url.Parse(ar.AccountSettings.IDP_SSO_Target_URL) if err != nil { return "", err } - base64EncodedUTF8SamlRequest, err := ar.GetRequest(true) + + request, err := ar.GetRequest(binding) if err != nil { return "", err } + // Deflate + var deflated bytes.Buffer + + fw, err := flate.NewWriter(&deflated, -1) + if err != nil { + return "", err + } + + _, err = fw.Write([]byte(request)) + if err != nil { + return "", err + } + + err = fw.Close() + if err != nil { + return "", err + } + + // base64 + b64str := base64.StdEncoding.EncodeToString(deflated.Bytes()) + + // urlencode q := u.Query() - q.Add("SAMLRequest", base64EncodedUTF8SamlRequest) + q.Add("SAMLRequest", b64str) u.RawQuery = q.Encode() return u.String(), nil diff --git a/authnrequestInterface.go b/authnrequestInterface.go index 96d4234..d008569 100644 --- a/authnrequestInterface.go +++ b/authnrequestInterface.go @@ -35,7 +35,7 @@ type AuthnRequest struct { ProtocolBinding string `xml:"ProtocolBinding,attr"` AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr,omitempty"` IssueInstant string `xml:"IssueInstant,attr"` - AttributeConsumingServiceIndex int `xml:"AttributeConsumingServiceIndex,attr"` + AttributeConsumingServiceIndex int `xml:"AttributeConsumingServiceIndex,attr,omitemtpy"` AssertionConsumerServiceIndex int `xml:"AssertionConsumerServiceIndex,attr,omitempty"` Issuer Issuer `xml:"Issuer"` NameIDPolicy NameIDPolicy `xml:"NameIDPolicy"` @@ -43,22 +43,10 @@ type AuthnRequest struct { } type AuthnSignedRequest struct { - XMLName xml.Name - SAMLP string `xml:"xmlns:samlp,attr"` - SAML string `xml:"xmlns:saml,attr"` - SAMLSIG string `xml:"xmlns:samlsig,attr"` - ID string `xml:"ID,attr"` - Version string `xml:"Version,attr"` - ProtocolBinding string `xml:"ProtocolBinding,attr"` - AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr"` - IssueInstant string `xml:"IssueInstant,attr"` - AssertionConsumerServiceIndex int `xml:"AssertionConsumerServiceIndex,attr"` - AttributeConsumingServiceIndex int `xml:"AttributeConsumingServiceIndex,attr"` - Issuer Issuer `xml:"Issuer"` - NameIDPolicy NameIDPolicy `xml:"NameIDPolicy"` - RequestedAuthnContext RequestedAuthnContext `xml:"RequestedAuthnContext"` - AuthnContextClassRef AuthnContextClassRef `xml:"AuthnContextClassRef"` - Signature Signature `xml:"Signature"` + AuthnRequest + AuthnContextClassRef AuthnContextClassRef `xml:"AuthnContextClassRef"` + Signature Signature `xml:"Signature"` + SAMLSIG string `xml:"xmlns:samlsig,attr"` } type Issuer struct {