Thanks @Michael Taylor
The app should display a third party page that is related to single sign on. So the controller should trigger that third party page. After the authentication happens I will need to handle the response, but for now.....in order to complete the fist step to get that page displayed, I need to be able to make a code reference that will trigger the controller.
A working example is calling the controller that I need from a razor page. In my case, I don't want the razor page. Please see code example below. I will list the following:
- Index.cshtml file that I don't want, but it's working from the example.
- The controller that I need.
- The startup class. @Anonymous
@默 IndexModel
@{
ViewData["Title"] = "Home";
} @using Microsoft.AspNetCore.Identity @inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager <div class="jumbotron">
<h1 class="display-4">Example Service Provider</h1>
<p class="lead">
This example demonstrates using the ComponentSpace SAML v2.0 library to enable SAML single sign-on
as the service provider.
</p>
@if (!SignInManager.IsSignedIn(User))
{
<p><a asp-area="" asp-controller="Saml" asp-action="InitiateSingleSignOn" class="btn btn-primary btn-lg">SSO to the Identity Provider</a></p>
}
</div>
// CONTROLLER:
using ComponentSpace.Saml2;
using ComponentSpace.Saml2.Metadata.Export;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using System;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
namespace ExampleServiceProvider.Controllers
{
[Route("[controller]/[action]")]
public class SamlController : Controller
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly ISamlServiceProvider _samlServiceProvider;
private readonly IConfigurationToMetadata _configurationToMetadata;
private readonly IConfiguration _configuration;
public SamlController(
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager,
ISamlServiceProvider samlServiceProvider,
IConfigurationToMetadata configurationToMetadata,
IConfiguration configuration)
{
_userManager = userManager;
_signInManager = signInManager;
_samlServiceProvider = samlServiceProvider;
_configurationToMetadata = configurationToMetadata;
_configuration = configuration;
}
public async Task<IActionResult> InitiateSingleSignOn(string returnUrl = null)
{
var partnerName = _configuration["PartnerName"];
// To login automatically at the service provider,
// initiate single sign-on to the identity provider (SP-initiated SSO).
// The return URL is remembered as SAML relay state.
await _samlServiceProvider.InitiateSsoAsync(partnerName, returnUrl);
return new EmptyResult();
}
public async Task<IActionResult> InitiateSingleLogout(string returnUrl = null)
{
// Request logout at the identity provider.
await _samlServiceProvider.InitiateSloAsync(relayState: returnUrl);
return new EmptyResult();
}
public async Task<IActionResult> AssertionConsumerService()
{
// Receive and process the SAML assertion contained in the SAML response.
// The SAML response is received either as part of IdP-initiated or SP-initiated SSO.
var ssoResult = await _samlServiceProvider.ReceiveSsoAsync();
// Automatically provision the user.
// If the user doesn't exist locally then create the user.
// Automatic provisioning is an optional step.
var user = await _userManager.FindByNameAsync(ssoResult.UserID);
if (user == null)
{
user = new IdentityUser { UserName = ssoResult.UserID, Email = ssoResult.UserID };
var result = await _userManager.CreateAsync(user);
if (!result.Succeeded)
{
throw new Exception($"The user {ssoResult.UserID} couldn't be created - {result}");
}
// For demonstration purposes, create some additional claims.
if (ssoResult.Attributes != null)
{
var samlAttribute = ssoResult.Attributes.SingleOrDefault(a => a.Name == ClaimTypes.Email);
if (samlAttribute != null)
{
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, samlAttribute.ToString()));
}
samlAttribute = ssoResult.Attributes.SingleOrDefault(a => a.Name == ClaimTypes.GivenName);
if (samlAttribute != null)
{
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.GivenName, samlAttribute.ToString()));
}
samlAttribute = ssoResult.Attributes.SingleOrDefault(a => a.Name == ClaimTypes.Surname);
if (samlAttribute != null)
{
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Surname, samlAttribute.ToString()));
}
}
}
// Automatically login using the asserted identity.
await _signInManager.SignInAsync(user, isPersistent: false);
// Redirect to the target URL if specified.
if (!string.IsNullOrEmpty(ssoResult.RelayState))
{
return LocalRedirect(ssoResult.RelayState);
}
return RedirectToPage("/Index");
}
public async Task<IActionResult> SingleLogoutService()
{
// Receive the single logout request or response.
// If a request is received then single logout is being initiated by the identity provider.
// If a response is received then this is in response to single logout having been initiated by the service provider.
var sloResult = await _samlServiceProvider.ReceiveSloAsync();
if (sloResult.IsResponse)
{
// SP-initiated SLO has completed.
if (!string.IsNullOrEmpty(sloResult.RelayState))
{
return LocalRedirect(sloResult.RelayState);
}
return RedirectToPage("/Index");
}
else
{
// Logout locally.
await _signInManager.SignOutAsync();
// Respond to the IdP-initiated SLO request indicating successful logout.
await _samlServiceProvider.SendSloAsync();
}
return new EmptyResult();
}
public async Task<IActionResult> ArtifactResolutionService()
{
// Resolve the HTTP artifact.
// This is only required if supporting the HTTP-Artifact binding.
await _samlServiceProvider.ResolveArtifactAsync();
return new EmptyResult();
}
public async Task<IActionResult> ExportMetadata()
{
var entityDescriptor = await _configurationToMetadata.ExportAsync();
var xmlElement = entityDescriptor.ToXml();
Response.ContentType = "text/xml";
Response.Headers.Add("Content-Disposition", "attachment; filename=\"metadata.xml\"");
var xmlWriterSettings = new XmlWriterSettings()
{
Async = true,
Encoding = Encoding.UTF8,
Indent = true,
OmitXmlDeclaration = true
};
using (var xmlWriter = XmlWriter.Create(Response.Body, xmlWriterSettings))
{
xmlElement.WriteTo(xmlWriter);
await xmlWriter.FlushAsync();
}
return new EmptyResult();
}
}
}
// STARTUP:
using ComponentSpace.Saml2.Configuration;
using ComponentSpace.Saml2.Configuration.Resolver;
using ComponentSpace.Saml2.Exceptions;
using ExampleServiceProvider.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shared;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ExampleServiceProvider
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
services.Configure<CookiePolicyOptions>(options =>
{
// SameSiteMode.None is required to support SAML SSO.
options.MinimumSameSitePolicy = SameSiteMode.None;
// Some older browsers don't support SameSiteMode.None.
options.OnAppendCookie = cookieContext => SameSite.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
options.OnDeleteCookie = cookieContext => SameSite.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
});
services.ConfigureApplicationCookie(options =>
{
// Use a unique identity cookie name rather than sharing the cookie across applications in the domain.
options.Cookie.Name = "ExampleServiceProvider.Identity";
// SameSiteMode.None is required to support SAML logout.
options.Cookie.SameSite = SameSiteMode.None;
});
// Add SAML SSO services.
services.AddSaml(Configuration.GetSection("SAML"));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
});
}
// Demonstrates loading SAML configuration programmatically
// rather than through appsettings.json or another JSON configuration file.
// This is useful if configuration is stored in a custom database, for example.
// The SAML configuration is registered in ConfigureServices by calling:
// services.AddSaml(config => ConfigureSaml(config));
private void ConfigureSaml(SamlConfigurations samlConfigurations)
{
samlConfigurations.Configurations = new List<SamlConfiguration>()
{
new SamlConfiguration()
{
LocalServiceProviderConfiguration = new LocalServiceProviderConfiguration()
{
Name = "https://ExampleServiceProvider",
Description = "Example Service Provider",
AssertionConsumerServiceUrl = "https://localhost:44360/SAML/AssertionConsumerService",
SingleLogoutServiceUrl = "https://localhost:44360/SAML/SingleLogoutService",
ArtifactResolutionServiceUrl = "https://localhost:44360/SAML/ArtifactResolutionService",
LocalCertificates = new List<Certificate>()
{
new Certificate()
{
FileName = "certificates/sp.pfx",
Password = "password"
}
}
},
PartnerIdentityProviderConfigurations = new List<PartnerIdentityProviderConfiguration>()
{
new PartnerIdentityProviderConfiguration()
{
Name = "https://ExampleIdentityProvider",
Description = "Example Identity Provider",
SignAuthnRequest = true,
SignLogoutRequest = true,
SignLogoutResponse = true,
WantLogoutRequestSigned = true,
WantLogoutResponseSigned = true,
SingleSignOnServiceUrl = "https://localhost:44313/SAML/SingleSignOnService",
SingleLogoutServiceUrl = "https://localhost:44313/SAML/SingleLogoutService",
ArtifactResolutionServiceUrl = "https://localhost:44313/SAML/ArtifactResolutionService",
PartnerCertificates = new List<Certificate>()
{
new Certificate()
{
FileName = "certificates/idp.cer"
}
}
}
}
}
};
}
}
// Demonstrates loading SAML configuration dynamically using a custom configuration resolver.
// Hard-coded configuration is returned in this example but more typically configuration would be read from a custom database.
// The configurationName parameter specifies the SAML configuration in a multi-tenancy application but is not used in this example.
// The custom configuration resolver is registered in ConfigureServices by calling:
// services.AddSaml();
// services.AddTransient<ISamlConfigurationResolver, CustomConfigurationResolver>();
public class CustomConfigurationResolver : AbstractSamlConfigurationResolver
{
public override Task<bool> IsLocalServiceProviderAsync(string configurationName)
{
return Task.FromResult(true);
}
public override Task<LocalServiceProviderConfiguration> GetLocalServiceProviderConfigurationAsync(string configurationName)
{
var localServiceProviderConfiguration = new LocalServiceProviderConfiguration()
{
Name = "https://ExampleServiceProvider",
Description = "Example Service Provider",
AssertionConsumerServiceUrl = "https://localhost:44360/SAML/AssertionConsumerService",
SingleLogoutServiceUrl = "https://localhost:44360/SAML/SingleLogoutService",
ArtifactResolutionServiceUrl = "https://localhost:44360/SAML/ArtifactResolutionService",
LocalCertificates = new List<Certificate>()
{
new Certificate()
{
FileName = "certificates/sp.pfx",
Password = "password"
}
}
};
return Task.FromResult(localServiceProviderConfiguration);
}
public override Task<PartnerIdentityProviderConfiguration> GetPartnerIdentityProviderConfigurationAsync(string configurationName, string partnerName)
{
if (partnerName != "https://ExampleIdentityProvider")
{
throw new SamlConfigurationException($"The partner identity provider {partnerName} is not configured.");
}
var partnerIdentityProviderConfiguration = new PartnerIdentityProviderConfiguration()
{
Name = "https://ExampleIdentityProvider",
Description = "Example Identity Provider",
SignAuthnRequest = true,
SignLogoutRequest = true,
SignLogoutResponse = true,
WantLogoutRequestSigned = true,
WantLogoutResponseSigned = true,
SingleSignOnServiceUrl = "https://localhost:44313/SAML/SingleSignOnService",
SingleLogoutServiceUrl = "https://localhost:44313/SAML/SingleLogoutService",
ArtifactResolutionServiceUrl = "https://localhost:44313/SAML/ArtifactResolutionService",
PartnerCertificates = new List<Certificate>()
{
new Certificate()
{
FileName = "certificates/idp.cer"
}
}
};
return Task.FromResult(partnerIdentityProviderConfiguration);
}
public override Task<IList<string>> GetPartnerIdentityProviderNamesAsync(string configurationName)
{
IList<string> partnerIdentityProviderNames = new List<string> { "https://ExampleIdentityProvider" };
return Task.FromResult(partnerIdentityProviderNames);
}
}
}