Skip to content

Commit

Permalink
Add support for extracting a base set of unverified attributes from a…
Browse files Browse the repository at this point in the history
… Response
  • Loading branch information
russellhaering committed May 7, 2018
1 parent 2fbfec5 commit 1f48417
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 11 deletions.
55 changes: 44 additions & 11 deletions decode_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,22 +277,55 @@ func (sp *SAMLServiceProvider) ValidateEncodedResponse(encodedResponse string) (
return decodedResponse, nil
}

// parseResponse is a helper function that was refactored out so that the XML parsing behavior can be isolated and unit tested
func parseResponse(xml []byte) (*etree.Document, *etree.Element, error) {
doc := etree.NewDocument()
err := doc.ReadFromBytes(xml)
// DecodeUnverifiedBaseResponse decodes several attributes from a SAML response for the purpose
// of determining how to validate the response. This is useful for Service Providers which
// expose a single Assertion Consumer Service URL but consume Responses from many IdPs.
func DecodeUnverifiedBaseResponse(encodedResponse string) (*types.UnverifiedBaseResponse, error) {
raw, err := base64.StdEncoding.DecodeString(encodedResponse)
if err != nil {
return nil, err
}

var response *types.UnverifiedBaseResponse

err = maybeDeflate(raw, func(maybeXML []byte) error {
response = &types.UnverifiedBaseResponse{}
return xml.Unmarshal(maybeXML, response)
})
if err != nil {
// Attempt to inflate the response in case it happens to be compressed (as with one case at saml.oktadev.com)
buf, err := ioutil.ReadAll(flate.NewReader(bytes.NewReader(xml)))
return nil, err
}

return response, nil
}

// maybeDeflate invokes the passed decoder over the passed data. If an error is
// returned, it then attempts to deflate the passed data before re-invoking
// the decoder over the deflated data.
func maybeDeflate(data []byte, decoder func([]byte) error) error {
err := decoder(data)
if err != nil {
deflated, err := ioutil.ReadAll(flate.NewReader(bytes.NewReader(data)))
if err != nil {
return nil, nil, err
return err
}

return decoder(deflated)
}

return nil
}

// parseResponse is a helper function that was refactored out so that the XML parsing behavior can be isolated and unit tested
func parseResponse(xml []byte) (*etree.Document, *etree.Element, error) {
var doc *etree.Document

err := maybeDeflate(xml, func(xml []byte) error {
doc = etree.NewDocument()
err = doc.ReadFromBytes(buf)
if err != nil {
return nil, nil, err
}
return doc.ReadFromBytes(xml)
})
if err != nil {
return nil, nil, err
}

el := doc.Root()
Expand Down
10 changes: 10 additions & 0 deletions providertests/exercise.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ package providertests
import (
"testing"

saml2 "github.com/russellhaering/gosaml2"
"github.com/stretchr/testify/require"
)

func ExerciseProviderTestScenarios(t *testing.T, scenarios []ProviderTestScenario) {
println("TESTING")
for _, scenario := range scenarios {
t.Run(scenario.ScenarioName, func(t *testing.T) {
_, err := saml2.DecodeUnverifiedBaseResponse(scenario.Response)
// DecodeUnverifiedBaseResponse is more permissive than RetrieveAssertionInfo.
// If an error _is_ returned it should match, but it is OK for no error to be
// returned even when one is expected during full validation.
if err != nil {
scenario.CheckError(t, err)
}

assertionInfo, err := scenario.ServiceProvider.RetrieveAssertionInfo(scenario.Response)
if scenario.CheckError != nil {
scenario.CheckError(t, err)
Expand Down
7 changes: 7 additions & 0 deletions providertests/exercise_go_1_6.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import (

func ExerciseProviderTestScenarios(t *testing.T, scenarios []ProviderTestScenario) {
for _, scenario := range scenarios {
// DecodeUnverifiedBaseResponse is more permissive than RetrieveAssertionInfo.
// If an error _is_ returned it should match, but it is OK for no error to be
// returned even when one is expected during full validation.
if err != nil {
scenario.CheckError(t, err)
}

assertionInfo, err := scenario.ServiceProvider.RetrieveAssertionInfo(scenario.Response)
if scenario.CheckError != nil {
scenario.CheckError(t, err)
Expand Down
14 changes: 14 additions & 0 deletions types/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ import (
"time"
)

// UnverifiedBaseResponse extracts several basic attributes of a SAML Response
// which may be useful in deciding how to validate the Response. An UnverifiedBaseResponse
// is parsed by this library prior to any validation of the Response, so the
// values it contains may have been supplied by an attacker and should not be
// trusted as authoritative from the IdP.
type UnverifiedBaseResponse struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"`
ID string `xml:"ID,attr"`
InResponseTo string `xml:"InResponseTo,attr"`
Destination string `xml:"Destination,attr"`
Version string `xml:"Version,attr"`
Issuer *Issuer `xml:"Issuer"`
}

type Response struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"`
ID string `xml:"ID,attr"`
Expand Down

0 comments on commit 1f48417

Please sign in to comment.