I am using sustainsys library for IDP as well sp initiated flow. In our implementation we are having multiple authentication schemes registered. in IDP initiated flow we are directly hitting respective ACS URL and in AcsCommandResultCreated I am redirecting to custom method in my code, in this custom method I am trying to access User.Claims which is causing redirection to another idp. Entire flow works very well for SP initiated requests. Idp initiated request works well in development and PROD environment but fails in QA.
Attaching startup.cs and custom controller method:
{
public class Startup
{
public IConfiguration Configuration { get; }
LoggerManager logger = new LoggerManager();
private static IHttpContextAccessor _httpContextAccessor;
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
List<SamlProvider> samlProviderslist = GetAllIdp();
SetAuthenticationSchemes(services, samlProviderslist);
// we need to associate SHA1/SHA256 with the long web-based names for Sustainsys.Saml2 to work
System.Security.Cryptography.CryptoConfig.AddAlgorithm(typeof(RsaPkCs1Sha256SignatureDescription), System.Security.Cryptography.Xml.SignedXml.XmlDsigRSASHA256Url);
System.Security.Cryptography.CryptoConfig.AddAlgorithm(typeof(RsaPkCs1Sha1SignatureDescription), System.Security.Cryptography.Xml.SignedXml.XmlDsigRSASHA1Url);
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
services.AddRazorPages();
services.AddDapperServices();
services.ConfigureLoggerService();
services.AddSingleton<IStartUpDataRepository, StartUpDataRepository>();
services.AddCors();
services.AddHttpContextAccessor();
services.Configure<FormOptions>(options =>
{
options.ValueLengthLimit = int.MaxValue;
options.MultipartBodyLengthLimit = int.MaxValue;
options.MultipartHeadersLengthLimit = int.MaxValue;
options.ValueCountLimit = int.MaxValue;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
{
app.UseStatusCodePagesWithReExecute("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.UseCors();
}
private List<SamlProvider> GetAllIdp()
{
List<SamlProvider> samlProviderslist = new List<SamlProvider>();
Dictionary<int, Dictionary<string, string>> IdpDictionary = GetConfigurations();
var IdpDictionaryKeysList = IdpDictionary?.Keys.Where(a => a != 0).ToList();
foreach (var key in IdpDictionaryKeysList)
{
try
{
var idp = IdpDictionary[key];
SamlProvider samlProviders = new SamlProvider
{
SchemeName = idp[IDPortalConstants.SchemeName],
EntityId = idp[IDPortalConstants.EntityId],
IdpEntityId = idp[IDPortalConstants.IdpEntityId],
IdpMetadata = idp[IDPortalConstants.idpMetadata],
MinIncomingSigningAlgorithm = idp[IDPortalConstants.MinIncomingSigningAlgorithm],
CertficateContent = idp[IDPortalConstants.CertificateContent],
CertficatePassword = idp[IDPortalConstants.CertficatePassword],
ReturnUrl = idp[IDPortalConstants.ReturnURL]
//Metadata = IdpDictionary[key]["SSO:SAML:Metadata"],
//cert = IdpDictionary[key]["SSO:SAML:CertificateFile"],
};
samlProviderslist.Add(samlProviders);
}
catch (Exception ex)
{
logger.LogError($"{ ErrorCodes.GetErroCode("1001")} { key} and exception : {ex.Message}");
}
}
return samlProviderslist;
}
private Dictionary<int, Dictionary<string, string>> GetConfigurations()
{
StartUpDataRepository startUpData = new StartUpDataRepository(Configuration);
startUpData.GetData();
return startUpData.ConfigData;
}
private void SetAuthenticationSchemes(IServiceCollection services, List<SamlProvider> samlProviderslist)
{
foreach (var Saml in samlProviderslist)
{
try
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = Saml.SchemeName + "Cookies";
sharedOptions.DefaultSignInScheme = Saml.SchemeName + "Cookies";
sharedOptions.DefaultChallengeScheme = Saml.SchemeName;
})
.AddSaml2(Saml.SchemeName, options =>
{
options.SPOptions = new Sustainsys.Saml2.Configuration.SPOptions()
{
AuthenticateRequestSigningBehavior = Sustainsys.Saml2.Configuration.SigningBehavior.Never,
EntityId = new Sustainsys.Saml2.Metadata.EntityId(Saml.EntityId),
ReturnUrl = new Uri(Saml.ReturnUrl),
ModulePath = string.Format("/{0}", Saml.SchemeName)
// MinIncomingSigningAlgorithm = Saml.MinIncomingSigningAlgorithm,
};
if (!string.IsNullOrEmpty(Saml.CertficateContent))
{
//string certFile = string.Format("{0}\\{1}", System.IO.Directory.GetCurrentDirectory(), Saml.cert);
//string content = Convert.ToBase64String(File.ReadAllBytes(certFile));
Byte[] data = Convert.FromBase64String(Saml.CertficateContent);
var cert = new X509Certificate2(data, Saml.CertficatePassword,
X509KeyStorageFlags.MachineKeySet
| X509KeyStorageFlags.PersistKeySet
| X509KeyStorageFlags.Exportable);
if (cert.HasPrivateKey)
{
options.SPOptions.ServiceCertificates.Add(cert);
}
else
{
logger.LogInfo("Certficate doesnt have private key and was not added to Scheme : " + Saml.SchemeName);
}
}
options.Notifications.AcsCommandResultCreated = AcsCommandResultCreated;
// options.Notifications.SelectIdentityProvider = SelectIdentityProvider;
options.IdentityProviders.Add(
new Sustainsys.Saml2.IdentityProvider(
new Sustainsys.Saml2.Metadata.EntityId(Saml.IdpEntityId), options.SPOptions)
{
MetadataLocation = Saml.IdpMetadata,
AllowUnsolicitedAuthnResponse = true,
LoadMetadata = true
});
})
.AddCookie(Saml.SchemeName + "Cookies");
logger.LogInfo($"Auth Scheme Added : {Saml.SchemeName}");
}
catch (Exception ex)
{
logger.LogError($"{ErrorCodes.GetErroCode("1002")} { Saml.SchemeName } .Exception :{ex.Message} ");
}
}
}
private void AcsCommandResultCreated(CommandResult commandResult, Saml2Response saml2Response)
{
LoggerManager logger = new LoggerManager();
var requestID = Guid.NewGuid();
// [REVIEW] Question: Have we seen this code executed? Is the commandResult.Location being altered?Yes
if (!commandResult.Location.ToString().Contains("/JwtAuth/GetTokenResponse"))
{
// [REVIEW] Medium: Is there a safer way to find the scheme? Could the URL look different based on tenant or possibly have query string params?
var schemeName = saml2Response.DestinationUrl.ToString().Split('/').SkipLast(1).Last();
StartUpDataRepository startUpData = new StartUpDataRepository(Configuration);
var tenantID = startUpData.GetTenantIdBySettingValue(schemeName);
Dictionary<int, Dictionary<string, string>> IdpDictionary = GetConfigurations();
var jwtRedirectUri = IdpDictionary[0][IDPortalConstants.JwtRedirectLocation];
// [REVIEW] Medium: Hard-coded "Rows[0][0]" seems risky to use. Here we are selecting only top rows with single column
commandResult.Location = new Uri(jwtRedirectUri + tenantID.Rows[0][0] + "&protocol=SAML&requestId=" + requestID, UriKind.Relative);
}
logger.LogInfo($"requestId: {requestID} saml2Response: {saml2Response} and commandResult.Location: {commandResult.Location}");
}
}
}
//custom Controller method
public async Task<IActionResult> GetTokenResponse(string clientId, string requestId, string protocol = "SAML")
{
_logger.LogInfo("Inside GetTokenResponse.");
var attributeMappingList = await GetTokenAttributes(clientId, protocol);
List<Claim> claimAttributes = new List<Claim>();
var failureRedirectUrl = await _dbConfigSettingRepo.Get(new { SettingName = $"SSO:SAML:FailureRedirectUrl", TenantId = clientId });
try
{
_logger.LogInfo("Inside GetTokenResponse inside try.");
_logger.LogInfo($"Request Id: { requestId}. Claims in SAML response are:\n" + String.Join(",\n", User.Claims.Select(o => o.Type + " : " + o.Value)));
if (!attributeMappingList.Any())
{
_logger.LogError(requestId, clientId, protocol, ErrorCodes.GetErroCode("1004"));
// [REVIEW] Medium: Redirect back to the source system (PerformX) should include an error code so it knows what type of issue ocurred.
return Redirect(failureRedirectUrl.SettingValue + "?errorcode=1004");
}
else
{
foreach (var tuple in attributeMappingList)
{
//Future Scope :TODO handle customization of tuple.Item1 may be with regular expression
var claimEntity = User.Claims.Where(x => (x.Type.Split('/').Last().ToLower() == tuple.Item1.ToLower())).SingleOrDefault();
if (!string.IsNullOrEmpty(claimEntity?.Value))
{
claimAttributes.Add(new Claim(tuple.Item2, claimEntity?.Value));
}
}
var userClaimsList = User.Claims.ToList();
//Get all setting values from DB which are common for all tenant
var signingKey = await _dbConfigSettingRepo.Get(new { SettingName = $"SSO:SAML:SigningKey", TenantId = clientId });
if(signingKey == null)
{
_logger.LogError(requestId, clientId, protocol, ErrorCodes.GetErroCode("1005"));
return Redirect(failureRedirectUrl.SettingValue + "?errorcode=1005");
}
var commonSettings = await _dbConfigSettingRepo.GetAll(new { TenantId = "0" });
byte[] key = Convert.FromBase64String(signingKey?.SettingValue?.ToString());
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
DateTime issueDateTime = DateTime.UtcNow;
DateTime expirationDateTime = DateTime.UtcNow.AddMinutes(double.Parse(commonSettings.Where(c => c.SettingName == "SSO:SAML:ExpiryInMinutes").Select(y => y.SettingValue).FirstOrDefault()));
SecurityTokenDescriptor descriptor = new SecurityTokenDescriptor
{
Subject = claimAttributes != null ? new ClaimsIdentity(claimAttributes) : new ClaimsIdentity(),
Expires = expirationDateTime,
IssuedAt = issueDateTime,
Issuer = commonSettings.Where(c => c.SettingName == "SSO:SAML:Issuer").First().SettingValue,
Audience = requestId,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
NotBefore = DateTime.UtcNow,
};
JwtSecurityToken token = handler.CreateJwtSecurityToken(descriptor);
var accessToken = handler.WriteToken(token);
var performXRedirectUrl = await _dbConfigSettingRepo.Get(new { SettingName = $"SSO:SAML:PerformXRedirectUrl", TenantId = clientId });
if (performXRedirectUrl != null)
{
_logger.LogInfo(string.Format("Request Id: {0} clientId: {1} protocol: {2} \n JWT Token : {3} \n RedirectUrl: {4}", requestId, clientId, protocol, accessToken, performXRedirectUrl.SettingValue));
return Redirect(performXRedirectUrl.SettingValue + accessToken);
}
else
{
_logger.LogError(requestId, clientId, protocol, ErrorCodes.GetErroCode("1006"));
return Redirect(failureRedirectUrl.SettingValue + "?errorcode=1006");
}
}
}
catch (Exception ex)
{
_logger.LogError(requestId, clientId, protocol, ErrorCodes.GetErroCode("1007"), ex);
return Redirect(failureRedirectUrl.SettingValue + "?errorcode=1007");
}
}