Skip to content
This repository has been archived by the owner on Jun 6, 2023. It is now read-only.

allow encrypted assertions #5

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
go-saml
======

[![Build Status](https://travis-ci.org/RobotsAndPencils/go-saml.svg?branch=master)](https://travis-ci.org/RobotsAndPencils/go-saml)

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.
Expand All @@ -11,7 +9,7 @@ Inspired by the early work of [Matt Baird](https://github.com/mattbaird/gosaml).

The library supports:

* generating signed/unsigned AuthnRequests
* generating signed AuthnRequests
* validating signed AuthnRequests
* generating service provider metadata
* generating signed Responses
Expand Down Expand Up @@ -42,7 +40,11 @@ sp := saml.ServiceProviderSettings{
IDPSSOURL: "http://idp/saml2",
IDPSSODescriptorURL: "http://idp/issuer",
IDPPublicCertPath: "idpcert.crt",
SPSignRequest: "true",
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()
Expand Down Expand Up @@ -106,6 +108,18 @@ response = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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())


//...
}
```
Expand Down
5 changes: 4 additions & 1 deletion authnrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ func (r *AuthnRequest) Validate(publicCertPath string) error {
func (s *ServiceProviderSettings) GetAuthnRequest() *AuthnRequest {
r := NewAuthnRequest()
r.AssertionConsumerServiceURL = s.AssertionConsumerServiceURL
r.Issuer.Url = s.IDPSSODescriptorURL
r.Destination = s.IDPSSOURL
//r.Issuer.Url = s.IDPSSODescriptorURL
r.Issuer.Url = s.Id
r.Signature.KeyInfo.X509Data.X509Certificate.Cert = s.PublicCert()

return r
Expand Down Expand Up @@ -116,6 +118,7 @@ func NewAuthnRequest() *AuthnRequest {
ID: id,
ProtocolBinding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
Version: "2.0",
Destination: "", // caller must populate ar.AppSettings.Destination,
AssertionConsumerServiceURL: "", // caller must populate ar.AppSettings.AssertionConsumerServiceURL,
Issuer: Issuer{
XMLName: xml.Name{
Expand Down
113 changes: 95 additions & 18 deletions authnresponse.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import (
"encoding/base64"
"encoding/xml"
"errors"
"github.com/diego-araujo/go-saml/util"
"time"

"github.com/RobotsAndPencils/go-saml/util"
)

func ParseCompressedEncodedResponse(b64ResponseXML string) (*Response, error) {
Expand All @@ -31,21 +30,86 @@ func ParseCompressedEncodedResponse(b64ResponseXML string) (*Response, error) {
func ParseEncodedResponse(b64ResponseXML string) (*Response, error) {
response := Response{}
bytesXML, err := base64.StdEncoding.DecodeString(b64ResponseXML)
//dst := string(bytesXML[:])
if err != nil {
return nil, err
}
err = xml.Unmarshal(bytesXML, &response)
if err != nil {
return nil, err
}

//fmt.Printf("%+v\n", response)
// There is a bug with XML namespaces in Go that's causing XML attributes with colons to not be roundtrip
// marshal and unmarshaled so we'll keep the original string around for validation.
response.originalString = string(bytesXML)
// fmt.Println(response.originalString)
return &response, nil
}

func (r *Response) IsEncrypted() bool {

//Test if exits EncryptedAssertion tag
if r.EncryptedAssertion.EncryptedData.EncryptionMethod.Algorithm == "" {
return false
} else {
return true
}
}

func (r *Response) Decrypt(privateKeyPath string) error {
s := r.originalString

if r.IsEncrypted() == false {
return errors.New("missing EncryptedAssertion tag on SAML Response, is encrypted?")

}
plainXML, err := DecryptResponse(s, privateKeyPath)
if err != nil {
return err
}
err = xml.Unmarshal([]byte(plainXML), &r)
if err != nil {
return err
}

r.originalString = plainXML
return nil
}

func (r *Response) ValidateResponseSignature(s *ServiceProviderSettings) error {

assertion, err := r.getAssertion()
if err != nil {
return err
}

if len(assertion.Signature.SignatureValue.Value) == 0 {
return errors.New("no signature")
}

err = VerifyResponseSignature(r.originalString, s.IDPPublicCertPath)
if err != nil {
return err
}

return nil
}

func (r *Response) getAssertion() (Assertion, error) {

assertion := Assertion{}

if r.IsEncrypted() {
assertion = r.EncryptedAssertion.Assertion
} else {
assertion = r.Assertion
}

if len(assertion.ID) == 0 {
return assertion, errors.New("no Assertions")
}
return assertion, nil
}

func (r *Response) Validate(s *ServiceProviderSettings) error {
if r.Version != "2.0" {
return errors.New("unsupported SAML Version")
Expand All @@ -55,33 +119,35 @@ func (r *Response) Validate(s *ServiceProviderSettings) error {
return errors.New("missing ID attribute on SAML Response")
}

if len(r.Assertion.ID) == 0 {
return errors.New("no Assertions")
assertion, err := r.getAssertion()
if err != nil {
return err
}

if len(r.Signature.SignatureValue.Value) == 0 {
return errors.New("no signature")
if assertion.Subject.SubjectConfirmation.Method != "urn:oasis:names:tc:SAML:2.0:cm:bearer" {
return errors.New("assertion method exception")
}

if assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient != s.AssertionConsumerServiceURL {
return errors.New("subject recipient mismatch, expected: " + s.AssertionConsumerServiceURL + " not " + assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient)
}

if r.Destination != s.AssertionConsumerServiceURL {
return errors.New("destination mismath expected: " + s.AssertionConsumerServiceURL + " not " + r.Destination)
}

if r.Assertion.Subject.SubjectConfirmation.Method != "urn:oasis:names:tc:SAML:2.0:cm:bearer" {
return errors.New("assertion method exception")
}
return nil
}

if r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient != s.AssertionConsumerServiceURL {
return errors.New("subject recipient mismatch, expected: " + s.AssertionConsumerServiceURL + " not " + r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient)
}
func (r *Response) ValidateExpiredConfirmation(s *ServiceProviderSettings) error {

err := VerifyResponseSignature(r.originalString, s.IDPPublicCertPath)
assertion, err := r.getAssertion()
if err != nil {
return err
}

//CHECK TIMES
expires := r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.NotOnOrAfter
expires := assertion.Subject.SubjectConfirmation.SubjectConfirmationData.NotOnOrAfter
notOnOrAfter, e := time.Parse(time.RFC3339, expires)
if e != nil {
return e
Expand All @@ -92,7 +158,6 @@ func (r *Response) Validate(s *ServiceProviderSettings) error {

return nil
}

func NewSignedResponse() *Response {
return &Response{
XMLName: xml.Name{
Expand Down Expand Up @@ -277,6 +342,10 @@ func (r *Response) String() (string, error) {
return string(b), nil
}

func (r *Response) OriginalString() string {
return r.originalString
}

func (r *Response) SignedString(privateKeyPath string) (string, error) {
s, err := r.String()
if err != nil {
Expand Down Expand Up @@ -307,7 +376,15 @@ func (r *Response) CompressedEncodedSignedString(privateKeyPath string) (string,

// GetAttribute by Name or by FriendlyName. Return blank string if not found
func (r *Response) GetAttribute(name string) string {
for _, attr := range r.Assertion.AttributeStatement.Attributes {
attrStatement := AttributeStatement{}

if r.IsEncrypted() {
attrStatement = r.EncryptedAssertion.Assertion.AttributeStatement
} else {
attrStatement = r.Assertion.AttributeStatement
}

for _, attr := range attrStatement.Attributes {
if attr.Name == name || attr.FriendlyName == name {
return attr.AttributeValue.Value
}
Expand Down
60 changes: 51 additions & 9 deletions iDPEntityDescriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func (s *ServiceProviderSettings) GetEntityDescriptor() (string, error) {
DS: "http://www.w3.org/2000/09/xmldsig#",
XMLNS: "urn:oasis:names:tc:SAML:2.0:metadata",
MD: "urn:oasis:names:tc:SAML:2.0:metadata",
EntityId: s.AssertionConsumerServiceURL,
EntityId: s.Id,

Extensions: Extensions{
XMLName: xml.Name{
Expand All @@ -22,9 +22,50 @@ func (s *ServiceProviderSettings) GetEntityDescriptor() (string, error) {
Alg: "urn:oasis:names:tc:SAML:metadata:algsupport",
MDAttr: "urn:oasis:names:tc:SAML:metadata:attribute",
MDRPI: "urn:oasis:names:tc:SAML:metadata:rpi",

UIInfo: UIInfo{
XMLName: xml.Name{
Local: "mdui:UIInfo",
},
MDUI: "urn:oasis:names:tc:SAML:metadata:ui",
DisplayName: UIDisplayName{
Lang: "en",
Value: "",
},
Description: UIDescription{
Lang: "en",
Value: "",
},
},
},
SPSSODescriptor: SPSSODescriptor{
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
AuthnRequestsSigned: fmt.Sprintf("%t", s.SPSignRequest),
WantAssertionsSigned: fmt.Sprintf("%t", s.IDPSignResponse),

Extensions: Extensions{
XMLName: xml.Name{
Local: "md:Extensions",
},
Alg: "urn:oasis:names:tc:SAML:metadata:algsupport",
MDAttr: "urn:oasis:names:tc:SAML:metadata:attribute",
MDRPI: "urn:oasis:names:tc:SAML:metadata:rpi",

UIInfo: UIInfo{
XMLName: xml.Name{
Local: "mdui:UIInfo",
},
MDUI: "urn:oasis:names:tc:SAML:metadata:ui",
DisplayName: UIDisplayName{
Value: s.DisplayName,
Lang: "en",
},
Description: UIDescription{
Lang: "en",
Value: s.Description,
},
},
},
SigningKeyDescriptor: KeyDescriptor{
XMLName: xml.Name{
Local: "md:KeyDescriptor",
Expand Down Expand Up @@ -86,15 +127,16 @@ func (s *ServiceProviderSettings) GetEntityDescriptor() (string, error) {
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
Location: s.AssertionConsumerServiceURL,
Index: "0",
Default: true,
},
AssertionConsumerService{
XMLName: xml.Name{
Local: "md:AssertionConsumerService",
},
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact",
Location: s.AssertionConsumerServiceURL,
Index: "1",
},
// AssertionConsumerService{
// XMLName: xml.Name{
// Local: "md:AssertionConsumerService",
// },
// Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact",
// Location: s.AssertionConsumerServiceURL,
// Index: "1",
// },
},
},
}
Expand Down
Loading