Skip to content

Commit 41a315c

Browse files
committed
support Scoping
1 parent 3465403 commit 41a315c

File tree

3 files changed

+66
-4
lines changed

3 files changed

+66
-4
lines changed

schema.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type AuthnRequest struct {
4848
NameIDPolicy *NameIDPolicy `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"`
4949
Conditions *Conditions
5050
RequestedAuthnContext *RequestedAuthnContext
51-
// Scoping *Scoping // TODO
51+
Scoping *Scoping
5252

5353
ForceAuthn *bool `xml:",attr"`
5454
IsPassive *bool `xml:",attr"`
@@ -209,9 +209,9 @@ func (r *AuthnRequest) Element() *etree.Element {
209209
if r.RequestedAuthnContext != nil {
210210
el.AddChild(r.RequestedAuthnContext.Element())
211211
}
212-
// if r.Scoping != nil {
213-
// el.AddChild(r.Scoping.Element())
214-
// }
212+
if r.Scoping != nil {
213+
el.AddChild(r.Scoping.Element())
214+
}
215215
if r.ForceAuthn != nil {
216216
el.CreateAttr("ForceAuthn", strconv.FormatBool(*r.ForceAuthn))
217217
}
@@ -321,6 +321,41 @@ func (a *NameIDPolicy) Element() *etree.Element {
321321
return el
322322
}
323323

324+
// Scoping represents the SAML object of the same name.
325+
//
326+
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf § 3.4.1.2
327+
type Scoping struct {
328+
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Scoping"`
329+
ProxyCount *int `xml:",attr"`
330+
IDPList []string `xml:"urn:oasis:names:tc:SAML:2.0:protocol IDPList"` // Only supports IDEntry, TODO support GetComplete{uri}
331+
RequesterIDs []string `xml:"urn:oasis:names:tc:SAML:2.0:protocol RequesterID"`
332+
}
333+
334+
// Element returns an etree.Element representing the object in XML form.
335+
func (a *Scoping) Element() *etree.Element {
336+
el := etree.NewElement("samlp:Scoping")
337+
if a.ProxyCount != nil {
338+
el.CreateAttr("ProxyCount", strconv.Itoa(*a.ProxyCount))
339+
}
340+
if len(a.IDPList) > 0 {
341+
idpList := etree.NewElement("samlp:IDPList")
342+
for _, idp := range a.IDPList {
343+
idpEntry := etree.NewElement("samlp:IDPEntry")
344+
idpEntry.CreateAttr("ProviderID", idp)
345+
idpList.AddChild(idpEntry)
346+
}
347+
el.AddChild(idpList)
348+
}
349+
if len(a.RequesterIDs) > 0 {
350+
for _, requesterID := range a.RequesterIDs {
351+
requesterIDEntry := etree.NewElement("samlp:RequesterIDEntry")
352+
requesterIDEntry.CreateAttr("ProviderID", requesterID)
353+
el.AddChild(requesterIDEntry)
354+
}
355+
}
356+
return el
357+
}
358+
324359
// ArtifactResolve represents the SAML object of the same name.
325360
type ArtifactResolve struct {
326361
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol ArtifactResolve"`

schema_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,20 @@ func TestLogoutRequestMarshalWithoutNotOnOrAfter(t *testing.T) {
277277
assert.Check(t, err)
278278
assert.Check(t, is.DeepEqual(expected, actual))
279279
}
280+
281+
func TestScopingElement(t *testing.T) {
282+
proxyCount := 2
283+
expected := AuthnRequest{Scoping: &Scoping{
284+
XMLName: xml.Name{Space: "urn:oasis:names:tc:SAML:2.0:protocol", Local: "Scoping"},
285+
IDPList: []string{"idp1", "idp2"},
286+
ProxyCount: &proxyCount,
287+
RequesterIDs: []string{"http://uri"},
288+
}}
289+
290+
doc := etree.NewDocument()
291+
doc.SetRoot(expected.Element())
292+
x, err := doc.WriteToBytes()
293+
assert.Check(t, err)
294+
assert.Check(t, is.Equal(`<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="" Version="" IssueInstant="0001-01-01T00:00:00Z"><samlp:Scoping ProxyCount="2"><samlp:IDPList><samlp:IDPEntry ProviderID="idp1"/><samlp:IDPEntry ProviderID="idp2"/></samlp:IDPList><samlp:RequesterIDEntry ProviderID="http://uri"/></samlp:Scoping></samlp:AuthnRequest>`,
295+
string(x)))
296+
}

service_provider.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ type ServiceProvider struct {
113113
// authentication requests
114114
AuthnNameIDFormat NameIDFormat
115115

116+
// IDPList is a list of identity providers that are allowed to authenticate users. Send as part of AuthnRequest.Scoping.
117+
// If empty, any delegate IDP can be used.
118+
IDPList []string
119+
116120
// MetadataValidDuration is a duration used to calculate validUntil
117121
// attribute in the metadata endpoint
118122
MetadataValidDuration time.Duration
@@ -536,6 +540,7 @@ func (sp *ServiceProvider) MakeAuthenticationRequest(idpURL string, binding stri
536540
Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
537541
Value: firstSet(sp.EntityID, sp.MetadataURL.String()),
538542
},
543+
539544
NameIDPolicy: &NameIDPolicy{
540545
AllowCreate: &allowCreate,
541546
// TODO(ross): figure out exactly policy we need
@@ -546,6 +551,11 @@ func (sp *ServiceProvider) MakeAuthenticationRequest(idpURL string, binding stri
546551
ForceAuthn: sp.ForceAuthn,
547552
RequestedAuthnContext: sp.RequestedAuthnContext,
548553
}
554+
if len(sp.IDPList) > 0 {
555+
req.Scoping = &Scoping{
556+
IDPList: sp.IDPList,
557+
}
558+
}
549559
// We don't need to sign the XML document if the IDP uses HTTP-Redirect binding
550560
if len(sp.SignatureMethod) > 0 && binding == HTTPPostBinding {
551561
if err := sp.SignAuthnRequest(&req); err != nil {

0 commit comments

Comments
 (0)