#saml-2.0 #sustainsys-saml2 #kentor-authservices
Вопрос:
Я использую библиотеку sustainsys как для IDP, так и для инициированного потока. В нашей реализации мы регистрируем несколько схем аутентификации. в потоке, инициированном IDP, мы напрямую попадаем в соответствующий URL ACS, и в AcsCommandResultCreated я перенаправляю на пользовательский метод в своем коде, в этом пользовательском методе я пытаюсь получить доступ к User.Claims, что вызывает перенаправление на другой idp. Весь поток очень хорошо работает для запросов, инициированных SP. Запрос, инициированный Idp, хорошо работает в среде разработки и разработки, но терпит неудачу в контроле качества.
Прикрепление файла startup.cs и пользовательского метода контроллера:
{
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] "amp;protocol=SAMLamp;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");
}
}
Комментарии:
1. Ты нашел что-нибудь в журналах?
2. ничего не получаю в журналах, потому что в моем пользовательском методе, как только пользователь. Доступ к утверждениям перенаправляется на IDP по умолчанию. Даже если он перенаправляет на исправление IDP, мы получаем пустые заявления пользователя.