├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── authnrequest.go ├── authnrequest_test.go ├── authnresponse.go ├── default.crt ├── default.key ├── iDPEntityDescriptor.go ├── saml.go ├── types.go ├── util ├── absolutePath.go ├── compress.go ├── compress_test.go ├── id.go └── loadCertificate.go ├── xmlsec.go └── xmlsec_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | go: 5 | - tip 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Robots and Pencils 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NO_COLOR=\033[0m 2 | OK_COLOR=\033[32;01m 3 | ERROR_COLOR=\033[31;01m 4 | WARN_COLOR=\033[33;01m 5 | 6 | default: build 7 | 8 | build: vet 9 | @echo "$(OK_COLOR)==> Go Building(NO_COLOR)" 10 | go build ./... 11 | 12 | init: 13 | go get github.com/nu7hatch/gouuid 14 | go get github.com/kardianos/osext 15 | go get github.com/stretchr/testify/assert 16 | 17 | vet: init 18 | @echo "$(OK_COLOR)==> Go Vetting$(NO_COLOR)" 19 | go vet ./... 20 | 21 | test: vet 22 | @echo "$(OK_COLOR)==> Testing$(NO_COLOR)" 23 | go test ./... 24 | 25 | .PHONY: default build init test vet 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Unsupported 2 | 3 | Unfortunately, the decision has been made to sunset support for this project. We thank everyone for supporting and utilizing the project. 4 | 5 | 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. 6 | 7 | go-saml 8 | ====== 9 | 10 | [![Build Status](https://travis-ci.org/RobotsAndPencils/go-saml.svg?branch=master)](https://travis-ci.org/RobotsAndPencils/go-saml) 11 | 12 | A just good enough SAML client library written in Go. This library is by no means complete and has been developed 13 | to solve several specific integration efforts. However, it's a start, and it would be great to see 14 | it evolve into a more fleshed out implemention. 15 | 16 | Inspired by the early work of [Matt Baird](https://github.com/mattbaird/gosaml). 17 | 18 | The library supports: 19 | 20 | * generating signed/unsigned AuthnRequests 21 | * validating signed AuthnRequests 22 | * generating service provider metadata 23 | * generating signed Responses 24 | * validating signed Responses 25 | 26 | 27 | Installation 28 | ------------ 29 | 30 | $ go get github.com/RobotsAndPencils/go-saml 31 | 32 | Here's a convenient way to generate a certificate: 33 | 34 | curl -sSL https://raw.githubusercontent.com/frntn/x509-san/master/gencert.sh | CRT_CN="mycert" bash 35 | 36 | 37 | Usage 38 | ----- 39 | 40 | Below are samples to show how you might use the library. 41 | 42 | ### Generating Signed AuthnRequests 43 | 44 | ```go 45 | sp := saml.ServiceProviderSettings{ 46 | PublicCertPath: "../default.crt", 47 | PrivateKeyPath: "../default.key", 48 | IDPSSOURL: "http://idp/saml2", 49 | IDPSSODescriptorURL: "http://idp/issuer", 50 | IDPPublicCertPath: "idpcert.crt", 51 | SPSignRequest: "true", 52 | AssertionConsumerServiceURL: "http://localhost:8000/saml_consume", 53 | } 54 | sp.Init() 55 | 56 | // generate the AuthnRequest and then get a base64 encoded string of the XML 57 | authnRequest := sp.GetAuthnRequest() 58 | b64XML, err := authnRequest.EncodedSignedString(sp.PrivateKeyPath) 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | // for convenience, get a URL formed with the SAMLRequest parameter 64 | url, err := saml.GetAuthnRequestURL(sp.IDPSSOURL, b64XML) 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | // below is bonus for how you might respond to a request with a form that POSTs to the IdP 70 | data := struct { 71 | Base64AuthRequest string 72 | URL string 73 | }{ 74 | Base64AuthRequest: b64XML, 75 | URL: url, 76 | } 77 | 78 | t := template.New("saml") 79 | t, err = t.Parse("
") 80 | 81 | // how you might respond to a request with the templated form that will auto post 82 | t.Execute(w, data) 83 | ``` 84 | 85 | ### Validating a received SAML Response 86 | 87 | 88 | ```go 89 | response = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 90 | encodedXML := r.FormValue("SAMLResponse") 91 | 92 | if encodedXML == "" { 93 | httpcommon.SendBadRequest(w, "SAMLResponse form value missing") 94 | return 95 | } 96 | 97 | response, err := saml.ParseEncodedResponse(encodedXML) 98 | if err != nil { 99 | httpcommon.SendBadRequest(w, "SAMLResponse parse: "+err.Error()) 100 | return 101 | } 102 | 103 | err = response.Validate(&sp) 104 | if err != nil { 105 | httpcommon.SendBadRequest(w, "SAMLResponse validation: "+err.Error()) 106 | return 107 | } 108 | 109 | samlID := response.GetAttribute("uid") 110 | if samlID == "" { 111 | httpcommon.SendBadRequest(w, "SAML attribute identifier uid missing") 112 | return 113 | } 114 | 115 | //... 116 | } 117 | ``` 118 | 119 | ### Service provider metadata 120 | 121 | ```go 122 | func samlMetadataHandler(sp *saml.ServiceProviderSettings) http.Handler { 123 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 124 | md, err := sp.GetEntityDescriptor() 125 | if err != nil { 126 | w.WriteHeader(500) 127 | w.Write([]byte("Error: " + err.Error())) 128 | return 129 | } 130 | 131 | w.Header().Set("Content-Type", "application/xml") 132 | w.Write([]byte(md)) 133 | }) 134 | } 135 | ``` 136 | 137 | ### Receiving a authnRequest 138 | 139 | ```go 140 | b64Request := r.URL.Query().Get("SAMLRequest") 141 | if b64Request == "" { 142 | w.WriteHeader(400) 143 | w.Write([]byte("SAMLRequest parameter missing")) 144 | return 145 | } 146 | 147 | defated, err := base64.StdEncoding.DecodeString(b64Request) 148 | if err != nil { 149 | w.WriteHeader(500) 150 | w.Write([]byte("Error: " + err.Error())) 151 | return 152 | } 153 | 154 | // enflate and unmarshal 155 | var buffer bytes.Buffer 156 | rdr := flate.NewReader(bytes.NewReader(defated)) 157 | io.Copy(&buffer, rdr) 158 | var authnRequest saml.AuthnRequest 159 | 160 | err = xml.Unmarshal(buffer.Bytes(), &authnRequest) 161 | if err != nil { 162 | w.WriteHeader(500) 163 | w.Write([]byte("Error: " + err.Error())) 164 | return 165 | } 166 | 167 | if authnRequest.Issuer.Url != issuerURL { 168 | w.WriteHeader(500) 169 | w.Write([]byte("unauthorized issuer "+authnRequest.Issuer.Url)) 170 | return 171 | } 172 | 173 | ``` 174 | 175 | ### Creating a SAML Response (if acting as an IdP) 176 | 177 | ```go 178 | issuer := "http://localhost:8000/saml" 179 | authnResponse := saml.NewSignedResponse() 180 | authnResponse.Issuer.Url = issuer 181 | authnResponse.Assertion.Issuer.Url = issuer 182 | authnResponse.Signature.KeyInfo.X509Data.X509Certificate.Cert = stringValueOfCert 183 | authnResponse.Assertion.Subject.NameID.Value = userIdThatYouAuthenticated 184 | authnResponse.AddAttribute("uid", userIdThatYouAuthenticated) 185 | authnResponse.AddAttribute("email", "someone@domain") 186 | authnResponse.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.InResponseTo = authnRequestIdRespondingTo 187 | authnResponse.InResponseTo = authnRequestIdRespondingTo 188 | authnResponse.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient = issuer 189 | 190 | // signed XML string 191 | signed, err := authnResponse.SignedString("/path/to/private.key") 192 | 193 | // or signed base64 encoded XML string 194 | b64XML, err := authnResponse.EncodedSignedString("/path/to/private.key") 195 | 196 | ``` 197 | 198 | 199 | ### Contributing 200 | 201 | Would love any contributions you having including better documentation, tests, or more robust functionality. 202 | 203 | git clone git@github.com:RobotsAndPencils/go-saml.git 204 | make init 205 | make test 206 | 207 | ### Contact 208 | 209 | [![Robots & Pencils Logo](http://f.cl.ly/items/2W3n1r2R0j2p2b3n3j3c/rnplogo.png)](http://www.robotsandpencils.com) 210 | 211 | Made with :heart: by Robots & Pencils ([@robotsNpencils](https://twitter.com/robotsNpencils)) 212 | 213 | #### Maintainers 214 | 215 | - [Mike Brevoort](http://github.com/mbrevoort) ([@mbrevoort](https://twitter.com/mbrevoort)) 216 | -------------------------------------------------------------------------------- /authnrequest.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Matthew Baird, Andrew Mussey 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package saml 16 | 17 | import ( 18 | "encoding/base64" 19 | "encoding/xml" 20 | "errors" 21 | "net/url" 22 | "time" 23 | 24 | "github.com/RobotsAndPencils/go-saml/util" 25 | ) 26 | 27 | func ParseCompressedEncodedRequest(b64RequestXML string) (*AuthnRequest, error) { 28 | var authnRequest AuthnRequest 29 | compressedXML, err := base64.StdEncoding.DecodeString(b64RequestXML) 30 | if err != nil { 31 | return nil, err 32 | } 33 | bXML := util.Decompress(compressedXML) 34 | 35 | err = xml.Unmarshal(bXML, &authnRequest) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | // There is a bug with XML namespaces in Go that's causing XML attributes with colons to not be roundtrip 41 | // marshal and unmarshaled so we'll keep the original string around for validation. 42 | authnRequest.originalString = string(bXML) 43 | return &authnRequest, nil 44 | 45 | } 46 | 47 | func ParseEncodedRequest(b64RequestXML string) (*AuthnRequest, error) { 48 | authnRequest := AuthnRequest{} 49 | bytesXML, err := base64.StdEncoding.DecodeString(b64RequestXML) 50 | if err != nil { 51 | return nil, err 52 | } 53 | err = xml.Unmarshal(bytesXML, &authnRequest) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | // There is a bug with XML namespaces in Go that's causing XML attributes with colons to not be roundtrip 59 | // marshal and unmarshaled so we'll keep the original string around for validation. 60 | authnRequest.originalString = string(bytesXML) 61 | return &authnRequest, nil 62 | } 63 | 64 | func (r *AuthnRequest) Validate(publicCertPath string) error { 65 | if r.Version != "2.0" { 66 | return errors.New("unsupported SAML Version") 67 | } 68 | 69 | if len(r.ID) == 0 { 70 | return errors.New("missing ID attribute on SAML Response") 71 | } 72 | 73 | // TODO more validation 74 | 75 | err := VerifyRequestSignature(r.originalString, publicCertPath) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // GetSignedAuthnRequest returns a singed XML document that represents a AuthnRequest SAML document 84 | func (s *ServiceProviderSettings) GetAuthnRequest() *AuthnRequest { 85 | r := NewAuthnRequest() 86 | r.AssertionConsumerServiceURL = s.AssertionConsumerServiceURL 87 | r.Destination = s.IDPSSOURL 88 | r.Issuer.Url = s.IDPSSODescriptorURL 89 | r.Signature.KeyInfo.X509Data.X509Certificate.Cert = s.PublicCert() 90 | 91 | if !s.SPSignRequest { 92 | r.SAMLSIG = "" 93 | r.Signature = nil 94 | } 95 | 96 | return r 97 | } 98 | 99 | // GetAuthnRequestURL generate a URL for the AuthnRequest to the IdP with the SAMLRequst parameter encoded 100 | func GetAuthnRequestURL(baseURL string, b64XML string, state string) (string, error) { 101 | u, err := url.Parse(baseURL) 102 | if err != nil { 103 | return "", err 104 | } 105 | 106 | q := u.Query() 107 | q.Add("SAMLRequest", b64XML) 108 | q.Add("RelayState", state) 109 | u.RawQuery = q.Encode() 110 | return u.String(), nil 111 | } 112 | 113 | func NewAuthnRequest() *AuthnRequest { 114 | id := util.ID() 115 | return &AuthnRequest{ 116 | XMLName: xml.Name{ 117 | Local: "samlp:AuthnRequest", 118 | }, 119 | SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol", 120 | SAML: "urn:oasis:names:tc:SAML:2.0:assertion", 121 | SAMLSIG: "http://www.w3.org/2000/09/xmldsig#", 122 | ID: id, 123 | ProtocolBinding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", 124 | Version: "2.0", 125 | AssertionConsumerServiceURL: "", // caller must populate ar.AppSettings.AssertionConsumerServiceURL, 126 | Issuer: Issuer{ 127 | XMLName: xml.Name{ 128 | Local: "saml:Issuer", 129 | }, 130 | Url: "", // caller must populate ar.AppSettings.Issuer 131 | SAML: "urn:oasis:names:tc:SAML:2.0:assertion", 132 | }, 133 | IssueInstant: time.Now().UTC().Format(time.RFC3339Nano), 134 | NameIDPolicy: NameIDPolicy{ 135 | XMLName: xml.Name{ 136 | Local: "samlp:NameIDPolicy", 137 | }, 138 | AllowCreate: true, 139 | Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", 140 | }, 141 | RequestedAuthnContext: RequestedAuthnContext{ 142 | XMLName: xml.Name{ 143 | Local: "samlp:RequestedAuthnContext", 144 | }, 145 | SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol", 146 | Comparison: "exact", 147 | AuthnContextClassRef: AuthnContextClassRef{ 148 | XMLName: xml.Name{ 149 | Local: "saml:AuthnContextClassRef", 150 | }, 151 | SAML: "urn:oasis:names:tc:SAML:2.0:assertion", 152 | Transport: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", 153 | }, 154 | }, 155 | Signature: &Signature{ 156 | XMLName: xml.Name{ 157 | Local: "samlsig:Signature", 158 | }, 159 | Id: "Signature1", 160 | SignedInfo: SignedInfo{ 161 | XMLName: xml.Name{ 162 | Local: "samlsig:SignedInfo", 163 | }, 164 | CanonicalizationMethod: CanonicalizationMethod{ 165 | XMLName: xml.Name{ 166 | Local: "samlsig:CanonicalizationMethod", 167 | }, 168 | Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", 169 | }, 170 | SignatureMethod: SignatureMethod{ 171 | XMLName: xml.Name{ 172 | Local: "samlsig:SignatureMethod", 173 | }, 174 | Algorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", 175 | }, 176 | SamlsigReference: SamlsigReference{ 177 | XMLName: xml.Name{ 178 | Local: "samlsig:Reference", 179 | }, 180 | URI: "#" + id, 181 | Transforms: Transforms{ 182 | XMLName: xml.Name{ 183 | Local: "samlsig:Transforms", 184 | }, 185 | Transform: []Transform{Transform{ 186 | XMLName: xml.Name{ 187 | Local: "samlsig:Transform", 188 | }, 189 | Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature", 190 | }}, 191 | }, 192 | DigestMethod: DigestMethod{ 193 | XMLName: xml.Name{ 194 | Local: "samlsig:DigestMethod", 195 | }, 196 | Algorithm: "http://www.w3.org/2000/09/xmldsig#sha1", 197 | }, 198 | DigestValue: DigestValue{ 199 | XMLName: xml.Name{ 200 | Local: "samlsig:DigestValue", 201 | }, 202 | }, 203 | }, 204 | }, 205 | SignatureValue: SignatureValue{ 206 | XMLName: xml.Name{ 207 | Local: "samlsig:SignatureValue", 208 | }, 209 | }, 210 | KeyInfo: KeyInfo{ 211 | XMLName: xml.Name{ 212 | Local: "samlsig:KeyInfo", 213 | }, 214 | X509Data: X509Data{ 215 | XMLName: xml.Name{ 216 | Local: "samlsig:X509Data", 217 | }, 218 | X509Certificate: X509Certificate{ 219 | XMLName: xml.Name{ 220 | Local: "samlsig:X509Certificate", 221 | }, 222 | Cert: "", // caller must populate cert, 223 | }, 224 | }, 225 | }, 226 | }, 227 | } 228 | } 229 | 230 | func (r *AuthnRequest) String() (string, error) { 231 | b, err := xml.MarshalIndent(r, "", " ") 232 | if err != nil { 233 | return "", err 234 | } 235 | 236 | return string(b), nil 237 | } 238 | 239 | func (r *AuthnRequest) SignedString(privateKeyPath string) (string, error) { 240 | s, err := r.String() 241 | if err != nil { 242 | return "", err 243 | } 244 | 245 | return SignRequest(s, privateKeyPath) 246 | } 247 | 248 | // GetAuthnRequestURL generate a URL for the AuthnRequest to the IdP with the SAMLRequst parameter encoded 249 | func (r *AuthnRequest) EncodedSignedString(privateKeyPath string) (string, error) { 250 | signed, err := r.SignedString(privateKeyPath) 251 | if err != nil { 252 | return "", err 253 | } 254 | b64XML := base64.StdEncoding.EncodeToString([]byte(signed)) 255 | return b64XML, nil 256 | } 257 | 258 | func (r *AuthnRequest) CompressedEncodedSignedString(privateKeyPath string) (string, error) { 259 | signed, err := r.SignedString(privateKeyPath) 260 | if err != nil { 261 | return "", err 262 | } 263 | compressed := util.Compress([]byte(signed)) 264 | b64XML := base64.StdEncoding.EncodeToString(compressed) 265 | return b64XML, nil 266 | } 267 | 268 | func (r *AuthnRequest) EncodedString() (string, error) { 269 | saml, err := r.String() 270 | if err != nil { 271 | return "", err 272 | } 273 | b64XML := base64.StdEncoding.EncodeToString([]byte(saml)) 274 | return b64XML, nil 275 | } 276 | 277 | func (r *AuthnRequest) CompressedEncodedString() (string, error) { 278 | saml, err := r.String() 279 | if err != nil { 280 | return "", err 281 | } 282 | compressed := util.Compress([]byte(saml)) 283 | b64XML := base64.StdEncoding.EncodeToString(compressed) 284 | return b64XML, nil 285 | } 286 | -------------------------------------------------------------------------------- /authnrequest_test.go: -------------------------------------------------------------------------------- 1 | package saml 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetSignedRequest(t *testing.T) { 10 | assert := assert.New(t) 11 | sp := ServiceProviderSettings{ 12 | PublicCertPath: "./default.crt", 13 | PrivateKeyPath: "./default.key", 14 | IDPSSOURL: "http://www.onelogin.net", 15 | IDPSSODescriptorURL: "http://www.onelogin.net", 16 | IDPPublicCertPath: "./default.crt", 17 | AssertionConsumerServiceURL: "http://localhost:8000/auth/saml/name", 18 | SPSignRequest: true, 19 | } 20 | err := sp.Init() 21 | assert.NoError(err) 22 | 23 | // Construct an AuthnRequest 24 | authnRequest := sp.GetAuthnRequest() 25 | signedXML, err := authnRequest.SignedString(sp.PrivateKeyPath) 26 | assert.NoError(err) 27 | assert.NotEmpty(signedXML) 28 | 29 | err = VerifyRequestSignature(signedXML, sp.PublicCertPath) 30 | assert.NoError(err) 31 | } 32 | 33 | func TestGetUnsignedRequest(t *testing.T) { 34 | assert := assert.New(t) 35 | sp := ServiceProviderSettings{ 36 | IDPSSOURL: "http://www.onelogin.net", 37 | IDPSSODescriptorURL: "http://www.onelogin.net", 38 | IDPPublicCertPath: "./default.crt", 39 | AssertionConsumerServiceURL: "http://localhost:8000/auth/saml/name", 40 | SPSignRequest: false, 41 | } 42 | err := sp.Init() 43 | assert.NoError(err) 44 | 45 | // Construct an AuthnRequest 46 | authnRequest := sp.GetAuthnRequest() 47 | assert.NoError(err) 48 | assert.NotEmpty(authnRequest) 49 | } 50 | -------------------------------------------------------------------------------- /authnresponse.go: -------------------------------------------------------------------------------- 1 | package saml 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/xml" 6 | "errors" 7 | "time" 8 | 9 | "github.com/RobotsAndPencils/go-saml/util" 10 | ) 11 | 12 | func ParseCompressedEncodedResponse(b64ResponseXML string) (*Response, error) { 13 | authnResponse := Response{} 14 | compressedXML, err := base64.StdEncoding.DecodeString(b64ResponseXML) 15 | if err != nil { 16 | return nil, err 17 | } 18 | bXML := util.Decompress(compressedXML) 19 | err = xml.Unmarshal(bXML, &authnResponse) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | // There is a bug with XML namespaces in Go that's causing XML attributes with colons to not be roundtrip 25 | // marshal and unmarshaled so we'll keep the original string around for validation. 26 | authnResponse.originalString = string(bXML) 27 | return &authnResponse, nil 28 | 29 | } 30 | 31 | func ParseEncodedResponse(b64ResponseXML string) (*Response, error) { 32 | response := Response{} 33 | bytesXML, err := base64.StdEncoding.DecodeString(b64ResponseXML) 34 | if err != nil { 35 | return nil, err 36 | } 37 | err = xml.Unmarshal(bytesXML, &response) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | // There is a bug with XML namespaces in Go that's causing XML attributes with colons to not be roundtrip 43 | // marshal and unmarshaled so we'll keep the original string around for validation. 44 | response.originalString = string(bytesXML) 45 | // fmt.Println(response.originalString) 46 | return &response, nil 47 | } 48 | 49 | func (r *Response) Validate(s *ServiceProviderSettings) error { 50 | if r.Version != "2.0" { 51 | return errors.New("unsupported SAML Version") 52 | } 53 | 54 | if len(r.ID) == 0 { 55 | return errors.New("missing ID attribute on SAML Response") 56 | } 57 | 58 | if len(r.Assertion.ID) == 0 { 59 | return errors.New("no Assertions") 60 | } 61 | 62 | if len(r.Signature.SignatureValue.Value) == 0 { 63 | return errors.New("no signature") 64 | } 65 | 66 | if r.Destination != s.AssertionConsumerServiceURL { 67 | return errors.New("destination mismath expected: " + s.AssertionConsumerServiceURL + " not " + r.Destination) 68 | } 69 | 70 | if r.Assertion.Subject.SubjectConfirmation.Method != "urn:oasis:names:tc:SAML:2.0:cm:bearer" { 71 | return errors.New("assertion method exception") 72 | } 73 | 74 | if r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient != s.AssertionConsumerServiceURL { 75 | return errors.New("subject recipient mismatch, expected: " + s.AssertionConsumerServiceURL + " not " + r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient) 76 | } 77 | 78 | err := VerifyResponseSignature(r.originalString, s.IDPPublicCertPath) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | //CHECK TIMES 84 | expires := r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.NotOnOrAfter 85 | notOnOrAfter, e := time.Parse(time.RFC3339, expires) 86 | if e != nil { 87 | return e 88 | } 89 | if notOnOrAfter.Before(time.Now()) { 90 | return errors.New("assertion has expired on: " + expires) 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func NewSignedResponse() *Response { 97 | return &Response{ 98 | XMLName: xml.Name{ 99 | Local: "samlp:Response", 100 | }, 101 | SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol", 102 | SAML: "urn:oasis:names:tc:SAML:2.0:assertion", 103 | SAMLSIG: "http://www.w3.org/2000/09/xmldsig#", 104 | ID: util.ID(), 105 | Version: "2.0", 106 | IssueInstant: time.Now().UTC().Format(time.RFC3339Nano), 107 | Issuer: Issuer{ 108 | XMLName: xml.Name{ 109 | Local: "saml:Issuer", 110 | }, 111 | Url: "", // caller must populate ar.AppSettings.AssertionConsumerServiceURL, 112 | }, 113 | Signature: Signature{ 114 | XMLName: xml.Name{ 115 | Local: "samlsig:Signature", 116 | }, 117 | Id: util.ID(), 118 | SignedInfo: SignedInfo{ 119 | XMLName: xml.Name{ 120 | Local: "samlsig:SignedInfo", 121 | }, 122 | CanonicalizationMethod: CanonicalizationMethod{ 123 | XMLName: xml.Name{ 124 | Local: "samlsig:CanonicalizationMethod", 125 | }, 126 | Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", 127 | }, 128 | SignatureMethod: SignatureMethod{ 129 | XMLName: xml.Name{ 130 | Local: "samlsig:SignatureMethod", 131 | }, 132 | Algorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", 133 | }, 134 | SamlsigReference: SamlsigReference{ 135 | XMLName: xml.Name{ 136 | Local: "samlsig:Reference", 137 | }, 138 | URI: "", // caller must populate "#" + ar.Id, 139 | Transforms: Transforms{ 140 | XMLName: xml.Name{ 141 | Local: "samlsig:Transforms", 142 | }, 143 | Transform: []Transform{Transform{ 144 | XMLName: xml.Name{ 145 | Local: "samlsig:Transform", 146 | }, 147 | Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature", 148 | }}, 149 | }, 150 | DigestMethod: DigestMethod{ 151 | XMLName: xml.Name{ 152 | Local: "samlsig:DigestMethod", 153 | }, 154 | Algorithm: "http://www.w3.org/2000/09/xmldsig#sha1", 155 | }, 156 | DigestValue: DigestValue{ 157 | XMLName: xml.Name{ 158 | Local: "samlsig:DigestValue", 159 | }, 160 | }, 161 | }, 162 | }, 163 | SignatureValue: SignatureValue{ 164 | XMLName: xml.Name{ 165 | Local: "samlsig:SignatureValue", 166 | }, 167 | }, 168 | KeyInfo: KeyInfo{ 169 | XMLName: xml.Name{ 170 | Local: "samlsig:KeyInfo", 171 | }, 172 | X509Data: X509Data{ 173 | XMLName: xml.Name{ 174 | Local: "samlsig:X509Data", 175 | }, 176 | X509Certificate: X509Certificate{ 177 | XMLName: xml.Name{ 178 | Local: "samlsig:X509Certificate", 179 | }, 180 | Cert: "", // caller must populate cert, 181 | }, 182 | }, 183 | }, 184 | }, 185 | Status: Status{ 186 | XMLName: xml.Name{ 187 | Local: "samlp:Status", 188 | }, 189 | StatusCode: StatusCode{ 190 | XMLName: xml.Name{ 191 | Local: "samlp:StatusCode", 192 | }, 193 | // TODO unsuccesful responses?? 194 | Value: "urn:oasis:names:tc:SAML:2.0:status:Success", 195 | }, 196 | }, 197 | Assertion: Assertion{ 198 | XMLName: xml.Name{ 199 | Local: "saml:Assertion", 200 | }, 201 | XS: "http://www.w3.org/2001/XMLSchema", 202 | XSI: "http://www.w3.org/2001/XMLSchema-instance", 203 | SAML: "urn:oasis:names:tc:SAML:2.0:assertion", 204 | Version: "2.0", 205 | ID: util.ID(), 206 | IssueInstant: time.Now().UTC().Format(time.RFC3339Nano), 207 | Issuer: Issuer{ 208 | XMLName: xml.Name{ 209 | Local: "saml:Issuer", 210 | }, 211 | Url: "", // caller must populate ar.AppSettings.AssertionConsumerServiceURL, 212 | }, 213 | Subject: Subject{ 214 | XMLName: xml.Name{ 215 | Local: "saml:Subject", 216 | }, 217 | NameID: NameID{ 218 | XMLName: xml.Name{ 219 | Local: "saml:NameID", 220 | }, 221 | Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", 222 | Value: "", 223 | SPNameQualifier: "", 224 | }, 225 | SubjectConfirmation: SubjectConfirmation{ 226 | XMLName: xml.Name{ 227 | Local: "saml:SubjectConfirmation", 228 | }, 229 | Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer", 230 | SubjectConfirmationData: SubjectConfirmationData{ 231 | XMLName: xml.Name{ 232 | Local: "saml:SubjectConfirmationData", 233 | }, 234 | InResponseTo: "", 235 | NotOnOrAfter: time.Now().Add(time.Minute * 5).UTC().Format(time.RFC3339Nano), 236 | Recipient: "", 237 | }, 238 | }, 239 | }, 240 | Conditions: Conditions{ 241 | XMLName: xml.Name{ 242 | Local: "saml:Conditions", 243 | }, 244 | NotBefore: time.Now().Add(time.Minute * -5).UTC().Format(time.RFC3339Nano), 245 | NotOnOrAfter: time.Now().Add(time.Minute * 5).UTC().Format(time.RFC3339Nano), 246 | AudienceRestrictions: []AudienceRestriction{}, 247 | }, 248 | AttributeStatement: AttributeStatement{ 249 | XMLName: xml.Name{ 250 | Local: "saml:AttributeStatement", 251 | }, 252 | Attributes: []Attribute{}, 253 | }, 254 | }, 255 | } 256 | } 257 | 258 | // AddAttribute add strong attribute to the Response 259 | func (r *Response) AddAttribute(name, value string) { 260 | r.Assertion.AttributeStatement.Attributes = append(r.Assertion.AttributeStatement.Attributes, Attribute{ 261 | XMLName: xml.Name{ 262 | Local: "saml:Attribute", 263 | }, 264 | Name: name, 265 | NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", 266 | AttributeValues: []AttributeValue{ 267 | { 268 | XMLName: xml.Name{ 269 | Local: "saml:AttributeValue", 270 | }, 271 | Type: "xs:string", 272 | Value: value, 273 | }, 274 | }, 275 | }) 276 | } 277 | 278 | func (r *Response) AddAudienceRestriction(value string) { 279 | r.Assertion.Conditions.AudienceRestrictions = append(r.Assertion.Conditions.AudienceRestrictions, 280 | AudienceRestriction{XMLName: xml.Name{ 281 | Local: "saml:AudienceRestriction", 282 | }, 283 | Audiences: []Audience{Audience{XMLName: xml.Name{ 284 | Local: "saml:Audience", 285 | }, 286 | Value: value, 287 | }, 288 | }, 289 | }) 290 | } 291 | 292 | func (r *Response) AddAuthnStatement(transport string, sessionIndex string) { 293 | r.Assertion.AuthnStatements = append(r.Assertion.AuthnStatements, AuthnStatement{ 294 | XMLName: xml.Name{ 295 | Local: "saml:AuthnStatement", 296 | }, 297 | AuthnInstant: r.IssueInstant, 298 | SessionIndex: sessionIndex, 299 | SessionNotOnOrAfter: r.Assertion.Conditions.NotOnOrAfter, 300 | AuthnContext: AuthnContext{ 301 | XMLName: xml.Name{ 302 | Local: "saml:AuthnContext", 303 | }, 304 | AuthnContextClassRef: AuthnContextClassRef{ 305 | XMLName: xml.Name{ 306 | Local: "saml:AuthnContextClassRef", 307 | }, 308 | Transport: transport, 309 | }, 310 | }, 311 | }) 312 | } 313 | 314 | func (r *Response) String() (string, error) { 315 | b, err := xml.MarshalIndent(r, "", " ") 316 | if err != nil { 317 | return "", err 318 | } 319 | 320 | return string(b), nil 321 | } 322 | 323 | func (r *Response) SignedString(privateKeyPath string) (string, error) { 324 | s, err := r.String() 325 | if err != nil { 326 | return "", err 327 | } 328 | 329 | return SignResponse(s, privateKeyPath) 330 | } 331 | 332 | func (r *Response) EncodedSignedString(privateKeyPath string) (string, error) { 333 | signed, err := r.SignedString(privateKeyPath) 334 | if err != nil { 335 | return "", err 336 | } 337 | b64XML := base64.StdEncoding.EncodeToString([]byte(signed)) 338 | return b64XML, nil 339 | } 340 | 341 | func (r *Response) CompressedEncodedSignedString(privateKeyPath string) (string, error) { 342 | signed, err := r.SignedString(privateKeyPath) 343 | if err != nil { 344 | return "", err 345 | } 346 | compressed := util.Compress([]byte(signed)) 347 | b64XML := base64.StdEncoding.EncodeToString(compressed) 348 | return b64XML, nil 349 | } 350 | 351 | // GetAttribute by Name or by FriendlyName. Return blank string if not found 352 | func (r *Response) GetAttribute(name string) string { 353 | for _, attr := range r.Assertion.AttributeStatement.Attributes { 354 | if attr.Name == name || attr.FriendlyName == name { 355 | return attr.AttributeValues[0].Value 356 | } 357 | } 358 | return "" 359 | } 360 | 361 | func (r *Response) GetAttributeValues(name string) []string { 362 | var values []string 363 | for _, attr := range r.Assertion.AttributeStatement.Attributes { 364 | if attr.Name == name || attr.FriendlyName == name { 365 | for _, v := range attr.AttributeValues { 366 | values = append(values, v.Value) 367 | } 368 | } 369 | } 370 | return values 371 | } 372 | -------------------------------------------------------------------------------- /default.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /default.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /iDPEntityDescriptor.go: -------------------------------------------------------------------------------- 1 | package saml 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | ) 7 | 8 | func (s *ServiceProviderSettings) GetEntityDescriptor() (string, error) { 9 | d := EntityDescriptor{ 10 | XMLName: xml.Name{ 11 | Local: "md:EntityDescriptor", 12 | }, 13 | DS: "http://www.w3.org/2000/09/xmldsig#", 14 | XMLNS: "urn:oasis:names:tc:SAML:2.0:metadata", 15 | MD: "urn:oasis:names:tc:SAML:2.0:metadata", 16 | EntityId: s.AssertionConsumerServiceURL, 17 | 18 | Extensions: Extensions{ 19 | XMLName: xml.Name{ 20 | Local: "md:Extensions", 21 | }, 22 | Alg: "urn:oasis:names:tc:SAML:metadata:algsupport", 23 | MDAttr: "urn:oasis:names:tc:SAML:metadata:attribute", 24 | MDRPI: "urn:oasis:names:tc:SAML:metadata:rpi", 25 | }, 26 | SPSSODescriptor: SPSSODescriptor{ 27 | ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol", 28 | SigningKeyDescriptor: KeyDescriptor{ 29 | XMLName: xml.Name{ 30 | Local: "md:KeyDescriptor", 31 | }, 32 | 33 | Use: "signing", 34 | KeyInfo: KeyInfo{ 35 | XMLName: xml.Name{ 36 | Local: "ds:KeyInfo", 37 | }, 38 | X509Data: X509Data{ 39 | XMLName: xml.Name{ 40 | Local: "ds:X509Data", 41 | }, 42 | X509Certificate: X509Certificate{ 43 | XMLName: xml.Name{ 44 | Local: "ds:X509Certificate", 45 | }, 46 | Cert: s.PublicCert(), 47 | }, 48 | }, 49 | }, 50 | }, 51 | EncryptionKeyDescriptor: KeyDescriptor{ 52 | XMLName: xml.Name{ 53 | Local: "md:KeyDescriptor", 54 | }, 55 | 56 | Use: "encryption", 57 | KeyInfo: KeyInfo{ 58 | XMLName: xml.Name{ 59 | Local: "ds:KeyInfo", 60 | }, 61 | X509Data: X509Data{ 62 | XMLName: xml.Name{ 63 | Local: "ds:X509Data", 64 | }, 65 | X509Certificate: X509Certificate{ 66 | XMLName: xml.Name{ 67 | Local: "ds:X509Certificate", 68 | }, 69 | Cert: s.PublicCert(), 70 | }, 71 | }, 72 | }, 73 | }, 74 | // SingleLogoutService{ 75 | // XMLName: xml.Name{ 76 | // Local: "md:SingleLogoutService", 77 | // }, 78 | // Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", 79 | // Location: "---TODO---", 80 | // }, 81 | AssertionConsumerServices: []AssertionConsumerService{ 82 | { 83 | XMLName: xml.Name{ 84 | Local: "md:AssertionConsumerService", 85 | }, 86 | Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", 87 | Location: s.AssertionConsumerServiceURL, 88 | Index: "0", 89 | }, 90 | { 91 | XMLName: xml.Name{ 92 | Local: "md:AssertionConsumerService", 93 | }, 94 | Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", 95 | Location: s.AssertionConsumerServiceURL, 96 | Index: "1", 97 | }, 98 | }, 99 | }, 100 | } 101 | b, err := xml.MarshalIndent(d, "", " ") 102 | if err != nil { 103 | return "", err 104 | } 105 | 106 | newMetadata := fmt.Sprintf("\n%s", b) 107 | return string(newMetadata), nil 108 | } 109 | -------------------------------------------------------------------------------- /saml.go: -------------------------------------------------------------------------------- 1 | package saml 2 | 3 | import "github.com/RobotsAndPencils/go-saml/util" 4 | 5 | // ServiceProviderSettings provides settings to configure server acting as a SAML Service Provider. 6 | // Expect only one IDP per SP in this configuration. If you need to configure multipe IDPs for an SP 7 | // then configure multiple instances of this module 8 | type ServiceProviderSettings struct { 9 | PublicCertPath string 10 | PrivateKeyPath string 11 | IDPSSOURL string 12 | IDPSSODescriptorURL string 13 | IDPPublicCertPath string 14 | AssertionConsumerServiceURL string 15 | SPSignRequest bool 16 | 17 | hasInit bool 18 | publicCert string 19 | privateKey string 20 | iDPPublicCert string 21 | } 22 | 23 | type IdentityProviderSettings struct { 24 | } 25 | 26 | func (s *ServiceProviderSettings) Init() (err error) { 27 | if s.hasInit { 28 | return nil 29 | } 30 | s.hasInit = true 31 | 32 | if s.SPSignRequest { 33 | s.publicCert, err = util.LoadCertificate(s.PublicCertPath) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | s.privateKey, err = util.LoadCertificate(s.PrivateKeyPath) 39 | if err != nil { 40 | panic(err) 41 | } 42 | } 43 | 44 | s.iDPPublicCert, err = util.LoadCertificate(s.IDPPublicCertPath) 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (s *ServiceProviderSettings) PublicCert() string { 53 | if !s.hasInit { 54 | panic("Must call ServiceProviderSettings.Init() first") 55 | } 56 | return s.publicCert 57 | } 58 | 59 | func (s *ServiceProviderSettings) PrivateKey() string { 60 | if !s.hasInit { 61 | panic("Must call ServiceProviderSettings.Init() first") 62 | } 63 | return s.privateKey 64 | } 65 | 66 | func (s *ServiceProviderSettings) IDPPublicCert() string { 67 | if !s.hasInit { 68 | panic("Must call ServiceProviderSettings.Init() first") 69 | } 70 | return s.iDPPublicCert 71 | } 72 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package saml 2 | 3 | import "encoding/xml" 4 | 5 | type AuthnRequest struct { 6 | XMLName xml.Name 7 | SAMLP string `xml:"xmlns:samlp,attr"` 8 | SAML string `xml:"xmlns:saml,attr"` 9 | SAMLSIG string `xml:"xmlns:samlsig,attr,omitempty"` 10 | ID string `xml:"ID,attr"` 11 | Version string `xml:"Version,attr"` 12 | ProtocolBinding string `xml:"ProtocolBinding,attr"` 13 | AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr"` 14 | Destination string `xml:"Destination,attr"` 15 | IssueInstant string `xml:"IssueInstant,attr"` 16 | AssertionConsumerServiceIndex int `xml:"AssertionConsumerServiceIndex,attr"` 17 | AttributeConsumingServiceIndex int `xml:"AttributeConsumingServiceIndex,attr"` 18 | Issuer Issuer `xml:"Issuer"` 19 | NameIDPolicy NameIDPolicy `xml:"NameIDPolicy"` 20 | RequestedAuthnContext RequestedAuthnContext `xml:"RequestedAuthnContext"` 21 | Signature *Signature `xml:"Signature,omitempty"` 22 | originalString string 23 | } 24 | 25 | type Issuer struct { 26 | XMLName xml.Name 27 | SAML string `xml:"xmlns:saml,attr"` 28 | Url string `xml:",innerxml"` 29 | } 30 | 31 | type NameIDPolicy struct { 32 | XMLName xml.Name 33 | AllowCreate bool `xml:"AllowCreate,attr"` 34 | Format string `xml:"Format,attr"` 35 | } 36 | 37 | type RequestedAuthnContext struct { 38 | XMLName xml.Name 39 | SAMLP string `xml:"xmlns:samlp,attr"` 40 | Comparison string `xml:"Comparison,attr"` 41 | AuthnContextClassRef AuthnContextClassRef `xml:"AuthnContextClassRef"` 42 | } 43 | 44 | type AuthnContextClassRef struct { 45 | XMLName xml.Name 46 | SAML string `xml:"xmlns:saml,attr"` 47 | Transport string `xml:",innerxml"` 48 | } 49 | 50 | type Signature struct { 51 | XMLName xml.Name 52 | Id string `xml:"Id,attr"` 53 | SignedInfo SignedInfo 54 | SignatureValue SignatureValue 55 | KeyInfo KeyInfo 56 | } 57 | 58 | type SignedInfo struct { 59 | XMLName xml.Name 60 | CanonicalizationMethod CanonicalizationMethod 61 | SignatureMethod SignatureMethod 62 | SamlsigReference SamlsigReference 63 | } 64 | 65 | type SignatureValue struct { 66 | XMLName xml.Name 67 | Value string `xml:",innerxml"` 68 | } 69 | 70 | type KeyInfo struct { 71 | XMLName xml.Name 72 | X509Data X509Data `xml:",innerxml"` 73 | } 74 | 75 | type CanonicalizationMethod struct { 76 | XMLName xml.Name 77 | Algorithm string `xml:"Algorithm,attr"` 78 | } 79 | 80 | type SignatureMethod struct { 81 | XMLName xml.Name 82 | Algorithm string `xml:"Algorithm,attr"` 83 | } 84 | 85 | type SamlsigReference struct { 86 | XMLName xml.Name 87 | URI string `xml:"URI,attr"` 88 | Transforms Transforms `xml:",innerxml"` 89 | DigestMethod DigestMethod `xml:",innerxml"` 90 | DigestValue DigestValue `xml:",innerxml"` 91 | } 92 | 93 | type X509Data struct { 94 | XMLName xml.Name 95 | X509Certificate X509Certificate `xml:",innerxml"` 96 | } 97 | 98 | type Transforms struct { 99 | XMLName xml.Name 100 | Transform []Transform 101 | } 102 | 103 | type DigestMethod struct { 104 | XMLName xml.Name 105 | Algorithm string `xml:"Algorithm,attr"` 106 | } 107 | 108 | type DigestValue struct { 109 | XMLName xml.Name 110 | } 111 | 112 | type X509Certificate struct { 113 | XMLName xml.Name 114 | Cert string `xml:",innerxml"` 115 | } 116 | 117 | type Transform struct { 118 | XMLName xml.Name 119 | Algorithm string `xml:"Algorithm,attr"` 120 | } 121 | 122 | type EntityDescriptor struct { 123 | XMLName xml.Name 124 | DS string `xml:"xmlns:ds,attr"` 125 | XMLNS string `xml:"xmlns,attr"` 126 | MD string `xml:"xmlns:md,attr"` 127 | EntityId string `xml:"entityID,attr"` 128 | 129 | Extensions Extensions `xml:"Extensions"` 130 | SPSSODescriptor SPSSODescriptor `xml:"SPSSODescriptor"` 131 | } 132 | 133 | type Extensions struct { 134 | XMLName xml.Name 135 | Alg string `xml:"xmlns:alg,attr"` 136 | MDAttr string `xml:"xmlns:mdattr,attr"` 137 | MDRPI string `xml:"xmlns:mdrpi,attr"` 138 | 139 | EntityAttributes string `xml:"EntityAttributes"` 140 | } 141 | 142 | type SPSSODescriptor struct { 143 | XMLName xml.Name 144 | ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` 145 | SigningKeyDescriptor KeyDescriptor 146 | EncryptionKeyDescriptor KeyDescriptor 147 | // SingleLogoutService SingleLogoutService `xml:"SingleLogoutService"` 148 | AssertionConsumerServices []AssertionConsumerService 149 | } 150 | 151 | type EntityAttributes struct { 152 | XMLName xml.Name 153 | SAML string `xml:"xmlns:saml,attr"` 154 | 155 | EntityAttributes []Attribute `xml:"Attribute"` // should be array?? 156 | } 157 | 158 | type SPSSODescriptors struct { 159 | } 160 | 161 | type KeyDescriptor struct { 162 | XMLName xml.Name 163 | Use string `xml:"use,attr"` 164 | KeyInfo KeyInfo `xml:"KeyInfo"` 165 | } 166 | 167 | type SingleLogoutService struct { 168 | Binding string `xml:"Binding,attr"` 169 | Location string `xml:"Location,attr"` 170 | } 171 | 172 | type AssertionConsumerService struct { 173 | XMLName xml.Name 174 | Binding string `xml:"Binding,attr"` 175 | Location string `xml:"Location,attr"` 176 | Index string `xml:"index,attr"` 177 | } 178 | 179 | type Response struct { 180 | XMLName xml.Name 181 | SAMLP string `xml:"xmlns:samlp,attr"` 182 | SAML string `xml:"xmlns:saml,attr"` 183 | SAMLSIG string `xml:"xmlns:samlsig,attr"` 184 | Destination string `xml:"Destination,attr"` 185 | ID string `xml:"ID,attr"` 186 | Version string `xml:"Version,attr"` 187 | IssueInstant string `xml:"IssueInstant,attr"` 188 | InResponseTo string `xml:"InResponseTo,attr"` 189 | 190 | Issuer Issuer `xml:"Issuer"` 191 | Signature Signature `xml:"Signature"` 192 | Status Status `xml:"Status"` 193 | Assertion Assertion `xml:"Assertion"` 194 | 195 | originalString string 196 | } 197 | 198 | type Assertion struct { 199 | XMLName xml.Name 200 | ID string `xml:"ID,attr"` 201 | Version string `xml:"Version,attr"` 202 | XS string `xml:"xmlns:xs,attr"` 203 | XSI string `xml:"xmlns:xsi,attr"` 204 | SAML string `xml:"xmlns:saml,attr"` 205 | IssueInstant string `xml:"IssueInstant,attr"` 206 | Issuer Issuer `xml:"Issuer"` 207 | Subject Subject 208 | Conditions Conditions 209 | AuthnStatements []AuthnStatement `xml:"AuthnStatement,omitempty"` 210 | AttributeStatement AttributeStatement 211 | } 212 | 213 | type Conditions struct { 214 | XMLName xml.Name 215 | NotBefore string `xml:",attr"` 216 | NotOnOrAfter string `xml:",attr"` 217 | AudienceRestrictions []AudienceRestriction `xml:"AudienceRestriction,omitempty"` 218 | } 219 | 220 | type AudienceRestriction struct { 221 | XMLName xml.Name 222 | Audiences []Audience `xml:"Audience"` 223 | } 224 | 225 | type Audience struct { 226 | XMLName xml.Name 227 | Value string `xml:",innerxml"` 228 | } 229 | 230 | type Subject struct { 231 | XMLName xml.Name 232 | NameID NameID 233 | SubjectConfirmation SubjectConfirmation 234 | } 235 | 236 | type SubjectConfirmation struct { 237 | XMLName xml.Name 238 | Method string `xml:",attr"` 239 | SubjectConfirmationData SubjectConfirmationData 240 | } 241 | 242 | type Status struct { 243 | XMLName xml.Name 244 | StatusCode StatusCode `xml:"StatusCode"` 245 | } 246 | 247 | type SubjectConfirmationData struct { 248 | XMLName xml.Name 249 | InResponseTo string `xml:",attr"` 250 | NotOnOrAfter string `xml:",attr"` 251 | Recipient string `xml:",attr"` 252 | } 253 | 254 | type NameID struct { 255 | XMLName xml.Name 256 | Format string `xml:",attr"` 257 | SPNameQualifier string `xml:",attr,omitempty"` 258 | Value string `xml:",innerxml"` 259 | } 260 | 261 | type StatusCode struct { 262 | XMLName xml.Name 263 | Value string `xml:",attr"` 264 | } 265 | 266 | type AttributeValue struct { 267 | XMLName xml.Name 268 | Type string `xml:"xsi:type,attr"` 269 | Value string `xml:",innerxml"` 270 | } 271 | 272 | type Attribute struct { 273 | XMLName xml.Name 274 | Name string `xml:",attr"` 275 | FriendlyName string `xml:",attr,omitempty"` 276 | NameFormat string `xml:",attr"` 277 | AttributeValues []AttributeValue `xml:"AttributeValue"` 278 | } 279 | 280 | type AttributeStatement struct { 281 | XMLName xml.Name 282 | Attributes []Attribute `xml:"Attribute"` 283 | } 284 | 285 | type AuthnStatement struct { 286 | XMLName xml.Name 287 | AuthnInstant string `xml:",attr"` 288 | SessionNotOnOrAfter string `xml:",attr,omitempty"` 289 | SessionIndex string `xml:",attr,omitempty"` 290 | AuthnContext AuthnContext `xml:"AuthnContext"` 291 | } 292 | 293 | type AuthnContext struct { 294 | XMLName xml.Name 295 | AuthnContextClassRef AuthnContextClassRef `xml:"AuthnContextClassRef"` 296 | } 297 | -------------------------------------------------------------------------------- /util/absolutePath.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | 7 | "github.com/kardianos/osext" 8 | ) 9 | 10 | func AbsolutePath(aPath string) string { 11 | if path.IsAbs(aPath) { 12 | return aPath 13 | } 14 | wd, err := osext.ExecutableFolder() 15 | if err != nil { 16 | panic(err) 17 | } 18 | fmt.Println("Working directory", wd) 19 | return path.Join(wd, aPath) 20 | } 21 | -------------------------------------------------------------------------------- /util/compress.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "compress/flate" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | func CompressString(in string) string { 11 | buf := new(bytes.Buffer) 12 | compressor, _ := flate.NewWriter(buf, 9) 13 | compressor.Write([]byte(in)) 14 | compressor.Close() 15 | return buf.String() 16 | } 17 | 18 | func DecompressString(in string) string { 19 | buf := new(bytes.Buffer) 20 | decompressor := flate.NewReader(strings.NewReader(in)) 21 | io.Copy(buf, decompressor) 22 | decompressor.Close() 23 | return buf.String() 24 | } 25 | 26 | func Compress(in []byte) []byte { 27 | buf := new(bytes.Buffer) 28 | compressor, _ := flate.NewWriter(buf, 9) 29 | compressor.Write(in) 30 | compressor.Close() 31 | return buf.Bytes() 32 | } 33 | 34 | func Decompress(in []byte) []byte { 35 | buf := new(bytes.Buffer) 36 | decompressor := flate.NewReader(bytes.NewReader(in)) 37 | io.Copy(buf, decompressor) 38 | decompressor.Close() 39 | return buf.Bytes() 40 | } 41 | -------------------------------------------------------------------------------- /util/compress_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCompressString(t *testing.T) { 10 | expected := "This is the test string" 11 | compressed := CompressString(expected) 12 | decompressed := DecompressString(compressed) 13 | assert.Equal(t, expected, decompressed) 14 | assert.True(t, len(compressed) > len(decompressed)) 15 | } 16 | 17 | func TestCompress(t *testing.T) { 18 | expected := []byte("This is the test string") 19 | compressed := Compress(expected) 20 | decompressed := Decompress(compressed) 21 | assert.Equal(t, expected, decompressed) 22 | assert.True(t, len(compressed) > len(decompressed)) 23 | } 24 | -------------------------------------------------------------------------------- /util/id.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "github.com/nu7hatch/gouuid" 4 | 5 | // UUID generate a new V4 UUID 6 | func ID() string { 7 | u, err := uuid.NewV4() 8 | if err != nil { 9 | panic(err) 10 | } 11 | return "_" + u.String() 12 | } 13 | -------------------------------------------------------------------------------- /util/loadCertificate.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io/ioutil" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // LoadCertificate from file system 10 | func LoadCertificate(certPath string) (string, error) { 11 | b, err := ioutil.ReadFile(certPath) 12 | if err != nil { 13 | return "", err 14 | } 15 | cert := string(b) 16 | 17 | re := regexp.MustCompile("---(.*)CERTIFICATE(.*)---") 18 | cert = re.ReplaceAllString(cert, "") 19 | cert = strings.Trim(cert, " \n") 20 | cert = strings.Replace(cert, "\n", "", -1) 21 | 22 | return cert, nil 23 | } 24 | -------------------------------------------------------------------------------- /xmlsec.go: -------------------------------------------------------------------------------- 1 | package saml 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | xmlResponseID = "urn:oasis:names:tc:SAML:2.0:protocol:Response" 13 | xmlRequestID = "urn:oasis:names:tc:SAML:2.0:protocol:AuthnRequest" 14 | ) 15 | 16 | // SignRequest sign a SAML 2.0 AuthnRequest 17 | // `privateKeyPath` must be a path on the filesystem, xmlsec1 is run out of process 18 | // through `exec` 19 | func SignRequest(xml string, privateKeyPath string) (string, error) { 20 | return sign(xml, privateKeyPath, xmlRequestID) 21 | } 22 | 23 | // SignResponse sign a SAML 2.0 Response 24 | // `privateKeyPath` must be a path on the filesystem, xmlsec1 is run out of process 25 | // through `exec` 26 | func SignResponse(xml string, privateKeyPath string) (string, error) { 27 | return sign(xml, privateKeyPath, xmlResponseID) 28 | } 29 | 30 | func sign(xml string, privateKeyPath string, id string) (string, error) { 31 | 32 | samlXmlsecInput, err := ioutil.TempFile(os.TempDir(), "tmpgs") 33 | if err != nil { 34 | return "", err 35 | } 36 | defer deleteTempFile(samlXmlsecInput.Name()) 37 | samlXmlsecInput.WriteString("\n") 38 | samlXmlsecInput.WriteString(xml) 39 | samlXmlsecInput.Close() 40 | 41 | samlXmlsecOutput, err := ioutil.TempFile(os.TempDir(), "tmpgs") 42 | if err != nil { 43 | return "", err 44 | } 45 | defer deleteTempFile(samlXmlsecOutput.Name()) 46 | samlXmlsecOutput.Close() 47 | 48 | // fmt.Println("xmlsec1", "--sign", "--privkey-pem", privateKeyPath, 49 | // "--id-attr:ID", id, 50 | // "--output", samlXmlsecOutput.Name(), samlXmlsecInput.Name()) 51 | output, err := exec.Command("xmlsec1", "--sign", "--privkey-pem", privateKeyPath, 52 | "--id-attr:ID", id, 53 | "--output", samlXmlsecOutput.Name(), samlXmlsecInput.Name()).CombinedOutput() 54 | if err != nil { 55 | return "", errors.New(err.Error() + " : " + string(output)) 56 | } 57 | 58 | samlSignedRequest, err := ioutil.ReadFile(samlXmlsecOutput.Name()) 59 | if err != nil { 60 | return "", err 61 | } 62 | samlSignedRequestXML := strings.Trim(string(samlSignedRequest), "\n") 63 | return samlSignedRequestXML, nil 64 | } 65 | 66 | // VerifyResponseSignature verify signature of a SAML 2.0 Response document 67 | // `publicCertPath` must be a path on the filesystem, xmlsec1 is run out of process 68 | // through `exec` 69 | func VerifyResponseSignature(xml string, publicCertPath string) error { 70 | return verify(xml, publicCertPath, xmlResponseID) 71 | } 72 | 73 | // VerifyRequestSignature verify signature of a SAML 2.0 AuthnRequest document 74 | // `publicCertPath` must be a path on the filesystem, xmlsec1 is run out of process 75 | // through `exec` 76 | func VerifyRequestSignature(xml string, publicCertPath string) error { 77 | return verify(xml, publicCertPath, xmlRequestID) 78 | } 79 | 80 | func verify(xml string, publicCertPath string, id string) error { 81 | //Write saml to 82 | samlXmlsecInput, err := ioutil.TempFile(os.TempDir(), "tmpgs") 83 | if err != nil { 84 | return err 85 | } 86 | 87 | samlXmlsecInput.WriteString(xml) 88 | samlXmlsecInput.Close() 89 | defer deleteTempFile(samlXmlsecInput.Name()) 90 | 91 | //fmt.Println("xmlsec1", "--verify", "--pubkey-cert-pem", publicCertPath, "--id-attr:ID", id, samlXmlsecInput.Name()) 92 | _, err = exec.Command("xmlsec1", "--verify", "--pubkey-cert-pem", publicCertPath, "--id-attr:ID", id, samlXmlsecInput.Name()).CombinedOutput() 93 | if err != nil { 94 | return errors.New("error verifing signature: " + err.Error()) 95 | } 96 | return nil 97 | } 98 | 99 | // deleteTempFile remove a file and ignore error 100 | // Intended to be called in a defer after the creation of a temp file to ensure cleanup 101 | func deleteTempFile(filename string) { 102 | _ = os.Remove(filename) 103 | } 104 | -------------------------------------------------------------------------------- /xmlsec_test.go: -------------------------------------------------------------------------------- 1 | package saml 2 | 3 | import ( 4 | "encoding/xml" 5 | "testing" 6 | 7 | "github.com/RobotsAndPencils/go-saml/util" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRequest(t *testing.T) { 12 | assert := assert.New(t) 13 | cert, err := util.LoadCertificate("./default.crt") 14 | assert.NoError(err) 15 | 16 | // Construct an AuthnRequest 17 | authRequest := NewAuthnRequest() 18 | authRequest.Signature.KeyInfo.X509Data.X509Certificate.Cert = cert 19 | 20 | b, err := xml.MarshalIndent(authRequest, "", " ") 21 | assert.NoError(err) 22 | xmlAuthnRequest := string(b) 23 | 24 | signedXml, err := SignRequest(xmlAuthnRequest, "./default.key") 25 | assert.NoError(err) 26 | assert.NotEmpty(signedXml) 27 | 28 | err = VerifyRequestSignature(signedXml, "./default.crt") 29 | assert.NoError(err) 30 | } 31 | 32 | func TestResponse(t *testing.T) { 33 | assert := assert.New(t) 34 | cert, err := util.LoadCertificate("./default.crt") 35 | assert.NoError(err) 36 | 37 | // Construct an AuthnRequest 38 | response := NewSignedResponse() 39 | response.Signature.KeyInfo.X509Data.X509Certificate.Cert = cert 40 | 41 | b, err := xml.MarshalIndent(response, "", " ") 42 | assert.NoError(err) 43 | xmlResponse := string(b) 44 | 45 | signedXml, err := SignResponse(xmlResponse, "./default.key") 46 | assert.NoError(err) 47 | assert.NotEmpty(signedXml) 48 | 49 | err = VerifyRequestSignature(signedXml, "./default.crt") 50 | assert.NoError(err) 51 | } 52 | --------------------------------------------------------------------------------