A just good enough SAML client library written in Go. This library is by no means complete and has been developed to solve several specific integration efforts. However, it's a start, and it would be great to see it evolve into a more fleshed out implemention.
Inspired by the early work of Matt Baird.
The library supports:
- generating signed AuthnRequests
- validating signed AuthnRequests
- generating service provider metadata
- generating signed Responses
- validating signed Responses
$ go get github.com/RobotsAndPencils/go-saml
Here's a convenient way to generate a certificate:
curl -sSL https://raw.githubusercontent.com/frntn/x509-san/master/gencert.sh | CRT_CN="mycert" bash
Below are samples to show how you might use the library.
sp := saml.ServiceProviderSettings{
PublicCertPath: "../default.crt",
PrivateKeyPath: "../default.key",
IDPSSOURL: "http://idp/saml2",
IDPSSODescriptorURL: "http://idp/issuer",
IDPPublicCertPath: "idpcert.crt",
Id: "entityID", // can be SP hostname without slash
DisplayName: "name", //maybe it will be displayed in IDP login page
Description: "desc" //maybe it will be displayed in IDP login page
SPSignRequest: true,
IDPSignResponse: true,
AssertionConsumerServiceURL: "http://localhost:8000/saml_consume",
}
sp.Init()
// generate the AuthnRequest and then get a base64 encoded string of the XML
authnRequest := sp.GetAuthnRequest()
b64XML, err := authnRequest.EncodedSignedString(sp.PrivateKeyPath)
if err != nil {
panic(err)
}
// for convenience, get a URL formed with the SAMLRequest parameter
url, err := saml.GetAuthnRequestURL(sp.IDPSSOURL, b64XML)
if err != nil {
panic(err)
}
// below is bonus for how you might respond to a request with a form that POSTs to the IdP
data := struct {
Base64AuthRequest string
URL string
}{
Base64AuthRequest: b64XML,
URL: url,
}
t := template.New("saml")
t, err = t.Parse("<html><body style=\"display: none\" onload=\"document.frm.submit()\"><form method=\"post\" name=\"frm\" action=\"{{.URL}}\"><input type=\"hidden\" name=\"SAMLRequest\" value=\"{{.Base64AuthRequest}}\" /><input type=\"submit\" value=\"Submit\" /></form></body></html>")
// how you might respond to a request with the templated form that will auto post
t.Execute(w, data)
response = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
encodedXML := r.FormValue("SAMLResponse")
if encodedXML == "" {
httpcommon.SendBadRequest(w, "SAMLResponse form value missing")
return
}
response, err := saml.ParseEncodedResponse(encodedXML)
if err != nil {
httpcommon.SendBadRequest(w, "SAMLResponse parse: "+err.Error())
return
}
err = response.Validate(&sp)
if err != nil {
httpcommon.SendBadRequest(w, "SAMLResponse validation: "+err.Error())
return
}
samlID := response.GetAttribute("uid")
if samlID == "" {
httpcommon.SendBadRequest(w, "SAML attribute identifier uid missing")
return
}
//If is a encrypted response need decode
if response.IsEncrypted(){
err = response.Decrypt(sp.PrivateKeyPath)
if err != nil {
httpcommon.SendBadRequest(w, "SAMLResponse parse: "+err.Error())
return
}
}
// Print plain xml
//fmt.Printf(response.String())
//...
}
func samlMetadataHandler(sp *saml.ServiceProviderSettings) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
md, err := sp.GetEntityDescriptor()
if err != nil {
w.WriteHeader(500)
w.Write([]byte("Error: " + err.Error()))
return
}
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(md))
})
}
b64Request := r.URL.Query().Get("SAMLRequest")
if b64Request == "" {
w.WriteHeader(400)
w.Write([]byte("SAMLRequest parameter missing"))
return
}
defated, err := base64.StdEncoding.DecodeString(b64Request)
if err != nil {
w.WriteHeader(500)
w.Write([]byte("Error: " + err.Error()))
return
}
// enflate and unmarshal
var buffer bytes.Buffer
rdr := flate.NewReader(bytes.NewReader(defated))
io.Copy(&buffer, rdr)
var authnRequest saml.AuthnRequest
err = xml.Unmarshal(buffer.Bytes(), &authnRequest)
if err != nil {
w.WriteHeader(500)
w.Write([]byte("Error: " + err.Error()))
return
}
if authnRequest.Issuer.Url != issuerURL {
w.WriteHeader(500)
w.Write([]byte("unauthorized issuer "+authnRequest.Issuer.Url))
return
}
issuer := "http://localhost:8000/saml"
authnResponse := saml.NewSignedResponse()
authnResponse.Issuer.Url = issuer
authnResponse.Assertion.Issuer.Url = issuer
authnResponse.Signature.KeyInfo.X509Data.X509Certificate.Cert = stringValueOfCert
authnResponse.Assertion.Subject.NameID.Value = userIdThatYouAuthenticated
authnResponse.AddAttribute("uid", userIdThatYouAuthenticated)
authnResponse.AddAttribute("email", "someone@domain")
authnResponse.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.InResponseTo = authnRequestIdRespondingTo
authnResponse.InResponseTo = authnRequestIdRespondingTo
authnResponse.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient = issuer
// signed XML string
signed, err := authnResponse.SignedString("/path/to/private.key")
// or signed base64 encoded XML string
b64XML, err := authnResponse.EncodedSignedString("/path/to/private.key")
Would love any contributions you having including better documentation, tests, or more robust functionality.
git clone git@github.com:RobotsAndPencils/go-saml.git
make init
make test
Made with ❤️ by Robots & Pencils (@robotsNpencils)