Multiple SPs using one stanza in the saml.config file

I setup an IdP using the high-level API and based it on the ExampleIdentityProvider project in the WebForms directory. It’s pretty basic but has been working well.

Now I’ve been given a new requirement. We have developers working on sites that will be SPs using the IdP for SP-initiated SSO. We could have a large number of development SPs at any given time and they will all have a port number that follows a consistent hostname (i.e. www.example.com:55550, www.example.com:55551, www.example.com:55552, etc.). We’d rather not have to add each SP to the IdP’s saml.config file every time a new development SP is created.

I was thinking about having a stanza in the saml.config for the hostname (www.example.com) with the options that are common between the different sites, and then trying to pass the port number (either before or after InitiateSSO on the SP) to the SSOService on the IdP and then send it back to the AssertionConsumerService on the SP. I’ve tried to do this using
InitiateSSO(HttpResponse httpResponse, string relayState, string partnerIdP, SSOOptions ssoOptions, string assertionConsumerServiceUrl, string singleSignOnServiceUrl)
– but I don’t think I’m on the right track.

I read in another post on this forum that you can’t mix the saml.config with programmatically specified configuration which I suspect is what I’m trying to do.

I’m trying to figure out how to implement this and would like to know if this can be done using the high-level API?

If you’re supporting SP-initiated SSO only then there might be a simpler solution.
You can have a single entry in saml.config that will support all developer SPs. The only thing that would be different is that the assertion consumer URL specified in the would be overridden by each SP. The SAML authn request sent by the SP may include the assertion consumer URL. If it does, we use this rather than the configured URL. If the SPs are using our component then this URL is included automatically.

(Edit: Unfortunately, the URLs in the following examples are being truncated, but I’m not sure how to fix that. The main thing is that the port# is not included in the saml.config.)

Simpler is good! We are supporting SP-initiated SSO only. So, are you saying that the saml.config (on the IdP) can look something like the following:

<identityprovider name=“<a href=” http:=“” sso.example.com"“=”“><a href=“http://sso.example.com” “=””>
<IdentityProvider Name=<a href=“http://sso.example.com” “=”“>http://sso.example.com
LocalCertificateFile=“idp.pfx”
LocalCertificatePassword=“password”/>

<PartnerServiceProvider Name=http://dev.example.com

WantAuthnRequestSigned=“false”
SignSAMLResponse=“true”
SignAssertion=“false”
EncryptAssertion=“false”
AssertionConsumerServiceUrl=http://dev.example.com/SAML/AssertionConsumerService.aspx
SingleLogoutServiceUrl=http://dev.example.com/SAML/SLOService.aspx
PartnerCertificateFile=“sp.cer”/>


And then I can override the AssertionConsumerServiceUrl with something like the following (from the SP side):

SAMLServiceProvider.InitiateSSO(Response, null, partnerIdP, null, http://dev.example.com:55015/SAML/AssertionConsumerService.aspx”, null);

And that the AssertionConsumerServiceUrl specified in InitiateSSO() will override the setting in the “<a href=“http://webserverdev.example.com” “=””><a href=“http://webserverdev.example.com” “=”“><a href=“http://webserverdev.example.com” “=””><a href=“http://webserverdev.example.com” “=”“><a href=“http://webserverdev.example.com” “=””><a href=“http://webserverdev.example.com” “=”“><a href=“http://webserverdev.example.com” “=””><a href=“http://webserverdev.example.com” “=”“><a href=“http://webserverdev.example.com” “=””><a href=“http://webserverdev.example.com” “=”“><a href=“http://dev.example.com” “=””><a href=“http://dev.example.com”“>http://dev.example.com” entry in the IdP’s saml.config?

Correct.

SSO is now working, but to make it work, I also had to modify the SP’s saml.config (removed the port #), so that the ServiceProvider entry now looks like the following:
<ServiceProvider Name=“<a href=“http://new.example.org” “=””><a href=“http://new.example.org” “=”“><a href=“http://new.example.org” “=””><a href=“http://new.example.org”“>http://new.example.org
AssertionConsumerServiceUrl=“~/SAML/AssertionConsumerService.aspx”
LocalCertificateFile=“sp.pfx”
LocalCertificatePassword=“password”/>
Is that an expected change?

Also, I’m now having issues with SLO (initiated from the same SP) after changing the IdP’s saml.config (SLO was working before I made changes to the saml.config file). Will SLO work with a single entry in the IdP’s saml.config? I think I read somewhere that when the IdP gets an SLO request, it sends a logout request to the other SPs to logout locally. If there are no entries in the IdP’s saml.config for each development SP (with the port #), can the IdP still send the requests?

Update: Here is the error I’m getting when I try to SLO from the SP:
Exception Details: ComponentSpace.SAML2.Exceptions.SAMLProtocolException: A logout response was unexpectedly received.
Source File: e:\websitefolders\new.example.org\SAML\SLOService.aspx.cs Line: 16

So, when I attempt to SLO from the SP (which has a port #), it appears that the IdP is sending the response to the SP referenced in the entry in the saml.config (the entry without the port #).

I don’t see an overload for InitiateSLO() that I can use to send the SLOService Url as I did when I called InitiateSSO() and specified the AssertionConsumerService Url.

Unfortunately the SAML specification doesn’t support sending the logout response URL in the logout request.
If you want to support SLO you will have to got back to having a entry for each developer.
Could you simply create a large number of entries up front in saml.config to cover the expected number of developers?

Would there be a way to do accomplish SLO without having a entry for each developer if I were using the Low Level API? I suspect the answer is ‘no’, but I need to know for sure.

SLO is complicated and it’s not recommended mixing the low-level and high-level SAML APIs.
Another possibility is to make use of the ComponentSpace.SAML2.Notifications.ISAMLObserver interface.
This includes:

string OnSendSAMLMessage(string partnerName, XmlElement samlMessage, string destinationUrl);
The return value is the updated destination URL.
You would need to identify which port to use and then update the destination URL to include the port number.
You register a SAML observer by calling:

SAMLObservable.Subscribe(samlObserver);

Is the assertion consumer service URL in the SAML authn request the only piece of information with the port number?
If so, you could use the following ISAMLObserver method to receive the authn request and extract the assertion consumer service URL and, from that, the port number.

void OnAuthnRequestReceived(string partnerName, XmlElement authnRequest, string relayState);

You would need to store the port number of the ASP.NET session state, for example, and then use it to modify the destination URL in OnSendSAMLMessage.
This is all possible but I wonder whether setting up a saml.config with a range of entries would be simpler.

I appreciate your help with this issue. We will be creating new development sites somewhat frequently. Some will be discarded while others are created, and new port numbers will used every time a new one is created. We’ll also have consultants creating development sites that will have to work with the IdP. The thought is that we don’t want to have to add an entry to the IdP’s saml.config every time a new one is created or go back and clean up the old, unused entries. We are trying to automate some of the source control aspects, and that is also contributing to the desire to not have separate entries in the config file. We may have to manually maintain the config file, but we’re trying to investigate other options first.

I’m working on another idea since we can now SSO with individual entries (no port numbers) in the saml.config. I can pass the port number via the ProviderName in ssoOptions as a parameter to InitiateSSO. I can then retrieve it via ReceiveSSO on the IdP. From this I can build a list of sites that the user has SSO’d from, and then when the IdP receives an SLO request, it can consult the list to determine the sites from which to log the user out. The problem I’m having is that I’m not sure how to tell the IdP to send the requests to the other SPs. Is it possible to send such requests using the high-level API?

BTW, I’m not ignoring your last post, but I don’t fully understand how to implement it, and I’m already getting the port number from the SP to the IdP which I think is what is being accomplished by your code. Please correct me if I’m misunderstanding the intent.

EDIT: The developer guide reads as follows for SP-initiated SLO:

1. The user has already SSO’d to one or more service providers.
2. The user clicks a link at the SP site to initiate SLO.
3. The user is logged out of the SP site.
4. A logout request is sent to the IdP site.
5. The user is logged out of the IdP site.
6. A logout response is sent to the SP site.

Note that the identity provider sends a logout request and expects a logout response from every other service provider apart from the initiating service provider. This occurs between steps 5 and 6.

Looking at the code in the SLOService.aspx.cs for the IdP:
if (isRequest) {
// Logout locally.
FormsAuthentication.SignOut();

// Respond to the SP-initiated SLO request indicating successful logout.
SAMLIdentityProvider.SendSLO(Response, null);
Where in the code does the IdP send the logout request? If I’m reading correctly, this should happen between the FormsAuthentication.SignOut() and the SendSLO().

Will each developer's SP SSO to the IdP and be independent of other developer's SPs?
Will a developer at one SP wish to SSO to another developer's SP?
If for any developer there's a single SSO session between the developer's SP and the IdP then no other SPs will be involved in the logout.
If multiple SPs are involved then it does get more complicated as the IdP needs the logout URL for each of the SPs that the developer SSO'd to.
The SAMLIdentityProvider.SendSLO call sends a logout request to the next SP or a logout response to the initiating SP.
So, just to expand on the steps above.

1. The user has already SSO’d to three service providers (SP1, SP2, SP3).
2. The user clicks a link at SP1 to initiate SLO.
3. The user is logged out of the SP1.
4. A logout request is sent to the IdP.
5. The logout request is received by the IdP (SAMLIdentityProvider.ReceiveSLO).
5. The user is logged out of the IdP.
6. A logout request is sent to SP2 (SAMLIdentityProvider.SendSLO).
7. The user is logged out of SP2.
8. A logout response is sent to the IdP (SAMLIdentityProvider.ReceiveSLO).
9. A logout request is sent to SP3 (SAMLIdentityProvider.SendSLO).
10. The user is logged out of SP3.
11. A logout response is sent to the IdP (SAMLIdentityProvider.ReceiveSLO).
12. A logout response is sent to SP1 (SAMLIdentityProvider.SendSLO).

[quote]Will each developer’s SP SSO to the IdP and be independent of other developer’s SPs?[/quote]
Yes

[quote]Will a developer at one SP wish to SSO to another developer’s SP?[/quote]
Yes, but I don’t think that would be common.

[quote]If multiple SPs are involved then it does get more complicated as the IdP needs the logout URL for each of the SPs that the developer SSO’d to.[/quote]
That’s my issue. Even if I identify and store the URL of the SPs that the developer SSO’d to, there doesn’t seem to be a way for me to manually send logout requests to those SPs.

Based on what I’ve read, I think that during SP-initiated SSO the SP sends the session index as part of the request and the IdP seems to store the session index somewhere and when the IdP receives an SLO request, it determines which SPs it should send a logout request to by referencing this “list” of session indexes. Is that accurate?

Also, I’ve been looking at the low level API example application for SP-initiated SSO/SLO. Is there a way to manually send a logout request to a specific URL or endpoint using the low level API?

The logout request from the SP may include one or more session indexes. Information about the previous SSO as well as the current logout request are stored in the SAML SSO session for that user/browser session. By default the SSO session information is stored in the ASP.NET session.
We don’t use the session indexes for determining which SPs the IdP should send a logout message to. Instead, we construct a list of SPs as each SSO completes for any given user. We use this list when sending logout requests to the SPs.
The low-level API requires you to specify the destination URL for the logout request or response.
However, mixing the low-level and high-level APIs could be problematic and isn’t recommended.
If for any given user there’s only a single SSO session (ie one SP and one IdP) then I think you can use the OnSendSAMLMessage method I mentioned earlier in this thread.
If for any given user there’s more than one SSO session (ie multiple SPs and one IdP) then you would have to handle the fact a logout request would be sent to each SP for which the user has an SSO session. This would be complicated but doable.
Perhaps you could start with the simpler case of one SSO session for any given user.

I’m not ignoring the rest of your post, but I had a question about something. You said the following:
[quote]We don’t use the session indexes for determining which SPs the IdP should send a logout message to. Instead, we construct a list of SPs as each SSO completes for any given user. We use this list when sending logout requests to the SPs.[/quote]
I’ve been thinking that the URL (http://www.example.com:55100/SAML/AssertionConsumerService.aspx) that I send in the InitiateSSO method in the SSO_Login file of the SP should be added to the list of SPs that you construct as each SSO completes. But it occurs to me that the reason I was getting a “logout response was unexpectedly received (by <a href=“http://www.example.com)” during”=“”><a href=“http://www.example.com)” “=”“><a href=“http://www.example.com)” “=””><a href=“http://www.example.com)”“>http://www.example.com)” during SLO from http://www.example.com:55100 is because you’re adding the “Partner name” to the list, which in this case is http://www.example.com (without the port#) based on what is shown in the trace log. Is that correct? And it’s getting the “Partner name” from the saml.config file?

EDIT: I may have answered my own question. It occurred to me that I might be able to test this. It looks to me like the SP Name specified in the SP’s saml.config file is saved in the list you construct during SSO. Then, during SLO, you look for the SingleLogoutServiceUrl in the entry for the SP Name in the IdP’s saml.config. In my configuration, if I provide the port# in the SingleLogoutServiceUrl, it works as expected. If I do not provide the port#, then I get the “logout response was unexpectedly received” exception. Is that roughly how it works?

That’s correct. We keep track of SSO sessions by the partner name. The partner name corresponds to the issuer field in the SAML message and must match a partner provider configured in saml.config. When a logout message is to be sent we lookup the partner provider SAML configuration keyed by the partner name to get the logout service URL for that partner provider.
Your scenario is unusual which is why it’s a bit more involved.
However, if you register a SAML observer and handle the OnSendSAMLMessage event you can replace the logout service URL that was retrieved from the configuration with whatever URL you want.
As an experiment, remove the port number from the saml.config and register an observer as described earlier in this thread. In the OnSendSAMLMessage event update the URL to include the port number.

I wanted to close the loop on this discussion. I decided to use the low-level API to get SLO working the way we needed it to work. We may have gotten it to work with the high-level API, but I was made aware of some additional requirements for this project that I believe would have required us to use the low-level API anyway.

Now it is working very well (with the low-level API), thanks in part to the help you provided. The help you gave me in this thread gave me a good understanding of the process, which helped a great deal when I converted it to use the low-level API. Thank you.

Thanks for the update and kind words. You’re welcome to contact us at support@componentspace.com if you’d like to discuss the additional requirements that meant it was easier to use the low-level API. We’re always looking to improve the product.