Reputation: 141
I am working on a Blazor application that is a multitenant app.
I am using the Sustainsys.Saml2.AspNetCore2 package.
I have a test Blazor application working that can authorize against the test https://stubidp.sustainsys.com IdP or a test OKTA IdP account. All good so far!
I would like to get a code example of being able to use both IdPs at the same time. I am not sure how to configure 2 IdPs via services.AddAuthentication().AddSaml2(options => or by other means. I know how to configure one but not more than one.
This will then help me understand how to setup different IdPs for our multitenant application.
My follow-up question would be whether it is possible to add IdPs to my application at runtime vs. when the application starts and runs startup.cs.
Thanks!
Upvotes: 5
Views: 2587
Reputation: 141
After doing lots of digging, I have found the answers to my questions. I will put a complete Blazor and WebForms test application up on GitHub in the future but for now, here are some key pieces of information.
When creating a Blazor application, make sure you allow logins with Individual User Accounts to get the proper scaffolding for logins and External Logins (Saml). If you have a current Blazor application without the Identity Scaffolding, follow the steps online to add the scaffolding to your app.
In Blazor, if you want to have separate SAML modules per IdP, you would call .AddSaml2 multiple times and have one IdP (options.IdentityProviders.Add...) per .AddSaml2. The key to this config is to name each Saml2 provider in the constructor such as .AddSaml2("Client1", "Client1", options => and .AddSaml2("Client2", "Client2", options =>. When you do this, you will see each of these module names appear as buttons on the Login page on the right side.
If you want multiple IdPs within one module, then you would call .AddSaml2 once with a config such as this:
// OKTA example that works
//options.SPOptions.EntityId = new EntityId("https://localhost:44312/Okta"); // MSJ - Change to the current URL (proper port) -https://localhost:44312/Saml2
options.SPOptions.EntityId = new EntityId("https://example.com/"); // Dummy entry
// options.SPOptions.ModulePath = "/Okta"; // Don't need this, default is "Saml2"
options.SPOptions.ReturnUrl = new Uri("/counter", UriKind.Relative); // Note used if RelayStateUsedAsReturnUrl set below is true
options.Notifications.AcsCommandResultCreated = AcsCommandResultCreated; // Needed to intercept the command and add the LoginProvider for the tenant if IdP initiated login
options.Notifications.GetIdentityProvider = GetIdentityProvider;
options.Notifications.SelectIdentityProvider = SelectIdentityProvider;
//options.Notifications.MetadataCreated = MetadataCreated;
options.Notifications.AuthenticationRequestCreated = AuthenticationRequestCreated;
options.Notifications.Unsafe.TokenValidationParametersCreated = TokenValidationParametersCreated;
options.SPOptions.ServiceCertificates.Add(new X509Certificate2("Sustainsys.Saml2.Tests.pfx", "")); // Sustainsys.Saml2.Tests.pfx - no password on this pfx file
// For now, turned off Assertion encryption
options.IdentityProviders.Add(new IdentityProvider(new EntityId("http://www.okta.com/exk2edycw57Obmc5i5d7"), options.SPOptions)
{
MetadataLocation = "https://dev-60124262.okta.com/app/exk2edycw57Obmc5i5d7/sso/saml/metadata", // Need this since EntityID is different than metadata location
LoadMetadata = true,
AllowUnsolicitedAuthnResponse = true, // Need this for IdP initiated login
// Need this as well for IdP initiated login to tell SAML2 to redirect to the ExternalLogin page in the Account folder
RelayStateUsedAsReturnUrl = true, // In OKTA or others, ensure the relay state is set to /Identity/Account/ExternalLogin?returnUrl=%2F&handler=Callback to ensure proper routing
});
options.IdentityProviders.Add(new IdentityProvider(
new EntityId("https://stubidp.sustainsys.com/Metadata"), options.SPOptions)
{
LoadMetadata = true,
AllowUnsolicitedAuthnResponse = true,
RelayStateUsedAsReturnUrl = true, // Setting this always gives an error that the ReturnURL is not relative
});
//idp.SigningKeys.AddConfiguredKey(new X509Certificate2("okta.cert")); // Not needed since the cert is in the meta data.
saml2Options = options; // External reference for the options object
I am working in a multi-tenant environment so I need to do a lot more stuff (handle notifications) to get things to work. If you just have one IdP, you don't need to do this extra stuff as it will just work. Note that I plan to load these IdPs dynamically so that they will not live as noted in this example. That is why I have the "saml2Options" object reference so I can add the IdPs later.
Notification code for multi-tenant:
private void TokenValidationParametersCreated(TokenValidationParameters validationParameters, IdentityProvider idp, XmlElement xmlElement)
{
// ** Will need to replace the URL with the subdomain tenant code
// How do we get access to the Navigation Manager
validationParameters.ValidAudience = "https://localhost:44312/Saml2"; // Was /Okta
// validationParameters.ValidateAudience = false; // Could use this but set the ValidAudience
}
private void AuthenticationRequestCreated(Saml2AuthenticationRequest request, IdentityProvider a, IDictionary<string, string> dict)
{
// This does not seem to matter
// ** Will need to replace the URL with the subdomain tenant code
request.Issuer = new EntityId($"https://localhost:44312/Saml2"); // Was /Okta
}
private void MetadataCreated(EntityDescriptor request, Saml2Urls urls)
{
// ** Will need to replace the URL with the subdomain tenant code
// Not seeing this get called - do not worry about it
request.EntityId = new EntityId($"https://localhost:44312/Saml2");
}
private IdentityProvider SelectIdentityProvider(EntityId entityID, IDictionary<string, string> arg2)
{
return saml2Options.IdentityProviders[entityID];
}
private IdentityProvider GetIdentityProvider(EntityId entityID, IDictionary<string, string> arg2, IOptions arg3)
{
return saml2Options.IdentityProviders[entityID];
}
public void AcsCommandResultCreated(CommandResult arg1, Saml2Response arg2)
{
if (arg1.RelayData == null)
{
// Need to do this for IdP initiated logins. RelayData is null so we need to add the LoginProvider
var x = new Dictionary<string, string>();
x.Add("LoginProvider", "Saml2"); // Will always be "Saml2" if we stick with one Saml endpoint with multiple IdentityProviders
arg1.RelayData = x;
}
}
Note that for IdP initiated login to work, I needed to put this in the RelayState at the IdP: /Identity/Account/ExternalLogin?returnUrl=%2F&handler=Callback
The last piece of magic is to change the OnPost method in ExternalLogin.cshtml.cs to this:
public IActionResult OnPost(string provider, string returnUrl = null)
{
// This is used for SP initiated login. So for this case, we know the URL and thus the tenantCode
// Hard coded here for testing but extract the tenant code from the URL here or from the external login button on the
// login page (pass it in)
// We are assuming one scheme (SSO provider) per tenant
string tenantCode = "Dev";
// Request a redirect to the external login provider.
var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl, tenantCode });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
// This is what they were talking about - add idp and scheme and THEN you will get the proper entityID in SelectIdentityProvider
properties.Items.Add("idp", "http://www.okta.com/exk2edycw57Obmc5i5d7"); // Put in the actual idp EntityID here - hardcoded here for testing
properties.Items.Add("scheme", "Saml2");
properties.Items.Add("tenant", "localhost"); // Used in notifications
return new ChallengeResult(provider, properties);
}
Adding "idp" and "scheme" is a must to allow the notifications to report the actual IdP used in a multi-tenant environment. Again, you don't need to do all of this if you just need to handle one IdP.
After I get things cleaned up in Blazor, I need to then implement the same thing in the older version of the application which is in WebForms. Fun!
Upvotes: 4
Reputation: 69260
The IdentityProviders property on the options is a collection. Just add more IdentityProvider
objects to it. To select what Idp to invoke, put an item in the AuthProps with key idp
and set the value to the EntityId of the Idp you want to use. It's possible to alter the collection when running.
Alternatively the more Asp.Net-Core-way to do it is to set up one Authentication scheme for each Idp by calling AddSaml2 multiple times. This can also be done at runtime, but is more complicated.
Upvotes: 1