Skip to content

Commit a8bc8f9

Browse files
mmailhoscrewjam
authored andcommitted
feat(slo): add logout response request validation (#247)
1 parent cdaa4af commit a8bc8f9

File tree

1 file changed

+78
-15
lines changed

1 file changed

+78
-15
lines changed

service_provider.go

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -833,41 +833,104 @@ func (sp *ServiceProvider) nameIDFormat() string {
833833
return nameIDFormat
834834
}
835835

836-
// ValidateLogoutResponse returns a nil error iff the logout request is valid.
837-
func (sp *ServiceProvider) ValidateLogoutResponse(r *http.Request) error {
838-
r.ParseForm()
839-
rawResponseBuf, err := base64.StdEncoding.DecodeString(r.PostForm.Get("SAMLResponse"))
836+
// ValidateLogoutResponseRequest validates the LogoutResponse content from the request
837+
func (sp *ServiceProvider) ValidateLogoutResponseRequest(req *http.Request) error {
838+
if data := req.URL.Query().Get("SAMLResponse"); data != "" {
839+
return sp.ValidateLogoutResponseRedirect(data)
840+
}
841+
842+
err := req.ParseForm()
843+
if err != nil {
844+
return fmt.Errorf("unable to parse form: %v", err)
845+
}
846+
847+
return sp.ValidateLogoutResponseForm(req.PostForm.Get("SAMLResponse"))
848+
}
849+
850+
// ValidatePostLogoutResponse returns a nil error if the logout response is valid.
851+
func (sp *ServiceProvider) ValidateLogoutResponseForm(postFormData string) error {
852+
rawResponseBuf, err := base64.StdEncoding.DecodeString(postFormData)
840853
if err != nil {
841854
return fmt.Errorf("unable to parse base64: %s", err)
842855
}
843856

844-
resp := LogoutResponse{}
857+
var resp LogoutResponse
858+
845859
if err := xml.Unmarshal(rawResponseBuf, &resp); err != nil {
846860
return fmt.Errorf("cannot unmarshal response: %s", err)
847861
}
848-
if resp.Destination != sp.SloURL.String() {
849-
return fmt.Errorf("`Destination` does not match SloURL (expected %q)", sp.SloURL.String())
862+
863+
if err := sp.validateLogoutResponse(&resp); err != nil {
864+
return err
850865
}
851866

852-
now := time.Now()
853-
if resp.IssueInstant.Add(MaxIssueDelay).Before(now) {
854-
return fmt.Errorf("issueInstant expired at %s", resp.IssueInstant.Add(MaxIssueDelay))
867+
doc := etree.NewDocument()
868+
if err := doc.ReadFromBytes(rawResponseBuf); err != nil {
869+
return err
855870
}
856-
if resp.Issuer.Value != sp.IDPMetadata.EntityID {
857-
return fmt.Errorf("issuer does not match the IDP metadata (expected %q)", sp.IDPMetadata.EntityID)
871+
872+
responseEl := doc.Root()
873+
if err = sp.validateSigned(responseEl); err != nil {
874+
return err
858875
}
859-
if resp.Status.StatusCode.Value != StatusSuccess {
860-
return fmt.Errorf("status code was not %s", StatusSuccess)
876+
877+
return nil
878+
}
879+
880+
// ValidateRedirectLogoutResponse returns a nil error if the logout response is valid.
881+
// URL Binding appears to be gzip / flate encoded
882+
// See https://www.oasis-open.org/committees/download.php/20645/sstc-saml-tech-overview-2%200-draft-10.pdf 6.6
883+
func (sp *ServiceProvider) ValidateLogoutResponseRedirect(queryParameterData string) error {
884+
rawResponseBuf, err := base64.StdEncoding.DecodeString(queryParameterData)
885+
if err != nil {
886+
return fmt.Errorf("unable to parse base64: %s", err)
861887
}
862888

889+
gr := flate.NewReader(bytes.NewBuffer(rawResponseBuf))
890+
891+
decoder := xml.NewDecoder(gr)
892+
893+
var resp LogoutResponse
894+
895+
err = decoder.Decode(&resp)
896+
if err != nil {
897+
return fmt.Errorf("unable to flate decode: %s", err)
898+
}
899+
900+
if err := sp.validateLogoutResponse(&resp); err != nil {
901+
return err
902+
}
903+
863904
doc := etree.NewDocument()
864-
if err := doc.ReadFromBytes(rawResponseBuf); err != nil {
905+
if _, err := doc.ReadFrom(gr); err != nil {
865906
return err
866907
}
908+
867909
responseEl := doc.Root()
868910
if err = sp.validateSigned(responseEl); err != nil {
869911
return err
870912
}
871913

872914
return nil
873915
}
916+
917+
918+
// validateLogoutResponse validates the LogoutResponse fields. Returns a nil error if the LogoutResponse is valid.
919+
func (sp *ServiceProvider) validateLogoutResponse(resp *LogoutResponse) error {
920+
if resp.Destination != sp.SloURL.String() {
921+
return fmt.Errorf("`Destination` does not match SloURL (expected %q)", sp.SloURL.String())
922+
}
923+
924+
now := time.Now()
925+
if resp.IssueInstant.Add(MaxIssueDelay).Before(now) {
926+
return fmt.Errorf("issueInstant expired at %s", resp.IssueInstant.Add(MaxIssueDelay))
927+
}
928+
if resp.Issuer.Value != sp.IDPMetadata.EntityID {
929+
return fmt.Errorf("issuer does not match the IDP metadata (expected %q)", sp.IDPMetadata.EntityID)
930+
}
931+
if resp.Status.StatusCode.Value != StatusSuccess {
932+
return fmt.Errorf("status code was not %s", StatusSuccess)
933+
}
934+
935+
return nil
936+
}

0 commit comments

Comments
 (0)