Поток, инициированный IDP с помощью sustainsys.saml2, перенаправляется обратно в idp

#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, мы получаем пустые заявления пользователя.