Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

configuring mulitple Azure Entra Id Saml2 based IdP's in in middleware, and attempting to invoke chosesn IdP in the Acs Challange, I get the following exception #1480

Open
joshuafranklinengineeringsystems opened this issue Dec 3, 2024 · 0 comments

Comments

@joshuafranklinengineeringsystems

My code works perfeclty fine if I enable one or the other IdP's but when I enabled them both at the same time I get the following internal execption:

System.Collections.Generic.KeyNotFoundException: No Idp with entity id "https://sts.windows.net/8c47ef63-1296-4e7a-97b7-649f4eb09330/" found.
---> System.Collections.Generic.KeyNotFoundException: The given key 'Sustainsys.Saml2.Metadata.EntityId' was not present in the dictionary.
at System.Collections.Generic.Dictionary2.get_Item(TKey key) at Sustainsys.Saml2.Configuration.IdentityProviderDictionary.get_Item(EntityId entityId) --- End of inner exception stack trace --- at Sustainsys.Saml2.Configuration.IdentityProviderDictionary.get_Item(EntityId entityId) at Sustainsys.Saml2.Configuration.Saml2Notifications.<>c.<.ctor>b__84_18(EntityId ei, IDictionary2 rd, IOptions opt)
at Sustainsys.Saml2.WebSso.AcsCommand.GetIdpContext(XmlElement xml, HttpRequestData request, IOptions options)
at Sustainsys.Saml2.WebSso.AcsCommand.Run(HttpRequestData request, IOptions options)
at Sustainsys.Saml2.AspNetCore2.Saml2Handler.HandleRequestAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

Part of the middleware, that iterates over a look of 1 to n IdP's from appsetting.json.

       public void ConfigureServices(IServiceCollection services)
    {
        _logger.LogInformation("Configuring services for SSO: {UseSSO}", _azureEntraOptions.UseSSO);

        try
        {
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                if (!_azureEntraOptions.UseSSO)
                {
                    options.DefaultChallengeScheme = _azureEntraOptions.DefaultIdProvider;
                    _logger.LogInformation("Configured default challenge scheme as: {DefaultIdProvider}", _azureEntraOptions.DefaultIdProvider);
                }
            })
            .AddCookie();

            foreach (var idProvider in _azureEntraOptions.IdProviders.Where(ShouldIncludeIdProvider))
            {
                var idProviderKey = idProvider.Key;
                var idProviderOptions = idProvider.Value;

                _logger.LogInformation("Adding SAML2 authentication for provider: {IdProviderKey}", idProviderKey);
                _logger.LogInformation("Processing identity provider {IdProviderKey} with status Enabled: {Enabled}", idProvider.Key, idProvider.Value.Enabled);

                services.AddAuthentication()
                    .AddSaml2(idProviderKey, options =>
                    {
                        ConfigureSaml2Options(options, idProviderOptions.Saml2!);
                    });
            }

            //// Log all configured Identity Providers after addition
            //services.PostConfigure<Saml2Options>(options =>
            //{
            //    _logger.LogInformation("Configured Identity Providers:");
            //    var idPs = options.IdentityProviders.KnownIdentityProviders;

            //    foreach (var idProvider in idPs)
            //    {
            //        _logger.LogInformation("Configured IdP EntityId: {EntityId}", idProvider.EntityId.Id);
            //    }
            //});
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred while configuring authentication services.");
        }

        services.AddScoped<IClaimsTransformationService, ClaimsTransformationService>();
    }

    private bool ShouldIncludeIdProvider(KeyValuePair<string, IdProviderOptions> idProvider)
    {
        return (_azureEntraOptions.UseSSO && idProvider.Value.Enabled) ||
               (!_azureEntraOptions.UseSSO &&
                idProvider.Key.Equals(_azureEntraOptions.DefaultIdProvider, StringComparison.OrdinalIgnoreCase));
    }
    
    private void ConfigureSaml2Options(Saml2Options options, EntraSaml2Options entraSaml2Config)
    {
        try
        {
            _logger.LogInformation("Attempting to add IdentityProvider with EntityId: {EntityId}", entraSaml2Config.MicrosoftEntraIdentifier);

            options.SPOptions.EntityId = new EntityId(entraSaml2Config.EntityId);

            options.SPOptions.ReturnUrl = new Uri(_azureEntraOptions.AcsReturnUrl!);
            options.SPOptions.PublicOrigin = new Uri(_azureEntraOptions.PublicOrigin);

            var identityProvider = new IdentityProvider(
                new EntityId(entraSaml2Config.MicrosoftEntraIdentifier),
                options.SPOptions)
            {
                LoadMetadata = true,
                MetadataLocation = entraSaml2Config.IdPMetadata,
                SingleSignOnServiceUrl = new Uri(entraSaml2Config.LoginURL!),
                SingleLogoutServiceUrl = new Uri(entraSaml2Config.LogoutURL!),

                AllowUnsolicitedAuthnResponse = entraSaml2Config.AllowUnsolicitedAuthnResponse
            };

            if (_azureEntraOptions.ForceLoginPrompt)
            {
                options.Notifications.AuthenticationRequestCreated = (request, provider, dictionary) =>
                {
                    request.ForceAuthentication = true;
                };
            }

            _logger.LogInformation("Registering IdP with EntityId.Id: {Id}", identityProvider.EntityId.Id);
            options.IdentityProviders.Add(identityProvider);

            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;

            _logger.LogInformation("Configuring SAML2 options for EntityId: {EntityId}", entraSaml2Config.EntityId);
            _logger.LogInformation("AcsReturnUrl: {AcsReturnUrl}", _azureEntraOptions.AcsReturnUrl);
            _logger.LogInformation("PublicOrigin: {PublicOrigin}", _azureEntraOptions.PublicOrigin);
            _logger.LogInformation("MicrosoftEntraIdentifier: {MicrosoftEntraIdentifier}", entraSaml2Config.MicrosoftEntraIdentifier);
            _logger.LogInformation("IdPMetadata: {IdPMetadata}", entraSaml2Config.IdPMetadata);
            _logger.LogInformation("LoginURL: {LoginURL}", entraSaml2Config.LoginURL);
            _logger.LogInformation("LogoutURL: {LogoutURL}", entraSaml2Config.LogoutURL);
            _logger.LogInformation("AllowUnsolicitedAuthnResponse: {AllowUnsolicitedAuthnResponse}", entraSaml2Config.AllowUnsolicitedAuthnResponse);
            _logger.LogInformation("ForceAuthentication: {ForceAuthentication}", _azureEntraOptions.ForceLoginPrompt);
        }
        catch (UriFormatException uriEx)
        {
            _logger.LogError(uriEx, "Invalid URI format in SAML2 configuration.");
            throw new ConfigurationErrorsException("Invalid URI in SAML2 configuration. Check the URLs in your settings.", uriEx);
        }
        catch (ArgumentNullException argEx)
        {
            _logger.LogError(argEx, "A required argument is null in SAML2 configuration.");
            throw new ConfigurationErrorsException("A required value in the SAML2 configuration is missing. Check your configuration.", argEx);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unexpected error occurred while configuring SAML2 options.");
            throw new ConfigurationErrorsException("An unexpected error occurred during SAML2 configuration. See the logs for details.", ex);
        }
    }
    
    [HttpGet("login")]
    [RequiresAddClaimMiddleware]
    public IActionResult Login([FromQuery] string selectionKey, [FromQuery] string selectedIdProvider)
    {
        try
        {
            _logger.LogInformation("Login endpoint called with SelectionKey: {SelectionKey}, SelectedIdProvider: {SelectedIdProvider}", selectionKey, selectedIdProvider);

            // Update claims based on selection key
            UpdateSelectionKeyClaim(selectionKey);
            _logger.LogDebug("Updated selection key claim for SelectionKey: {SelectionKey}", selectionKey);

            if (User?.Identity?.IsAuthenticated != true)
            {
                _logger.LogInformation("User is not authenticated. Redirecting to SAML Challenge.");

                // Construct the redirect URI to be used after successful login
                // we use this to transfer state state into the auth'd realm
                var redirectToCallbackUri = Url.Action("Callback", "Saml", new { SelectionKey = selectionKey, SelectedIdProvider = selectedIdProvider });
                _logger.LogDebug("Constructed redirect to callback URI for SAML Challenge: {RedirectUri}", redirectToCallbackUri);

                // If redirectUri is null, log the issue
                if (redirectToCallbackUri == null)
                {
                    string redirectUriMessage = "Redirect URI is null. Cannot proceed with SAML Challenge.";
                    _logger.LogError(redirectUriMessage);
                    return StatusCode(StatusCodes.Status500InternalServerError, "Failed to construct redirect URI for SAML Challenge.");
                }

                // Redirect to SAML Challenge
                var properties = new AuthenticationProperties
                {
                    RedirectUri = redirectToCallbackUri
                };
                return Challenge(properties, selectedIdProvider);
            }

            string message = $"User is already authenticated with SelectionKey: {selectionKey}, returning authenticated status.";
            _logger.LogInformation(message);

            return Ok("User is already authenticated.");
        }
        catch (Exception ex)
        {
            string errorMessage = $"Error in SAML Login with SelectionKey: {selectionKey}, SelectedIdProvider: {selectedIdProvider}. Exception Message: {ex.Message}";
            _logger.LogError(ex, errorMessage);

            return StatusCode(StatusCodes.Status500InternalServerError, $"Internal server error: {ex.Message}");
        }
    }

    [Authorize]
    [HttpGet("Callback")]
    public async Task<IActionResult> Callback([FromQuery] string selectionKey, [FromQuery] string selectedIdProvider)
    {
        _logger.LogInformation("ACS endpoint called with SelectionKey: {SelectionKey}, SelectedIdProvider: {SelectedIdProvider}", selectionKey, selectedIdProvider);
        try
        {
            // Start by authenticating the user
            var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            _logger.LogDebug("Authentication result obtained: Succeeded - {AuthSucceeded}", result?.Succeeded);

            if (!IsAuthenticationValid(result))
            {
                _logger.LogWarning("Authentication failed at ACS endpoint. SelectionKey: {SelectionKey}, SelectedIdProvider: {SelectedIdProvider}", selectionKey, selectedIdProvider);
                return Unauthorized(new { Message = "Authentication failed." });
            }

            _logger.LogInformation("Authentication succeeded for ACS endpoint with SelectionKey: {SelectionKey}, SelectedIdProvider: {SelectedIdProvider}", selectionKey, selectedIdProvider);

            // Update selection key claim in the authenticated user's principal
            UpdateSelectionKeyClaim(selectionKey, result.Principal);
            _logger.LogDebug("Selection key claim updated in principal: {SelectionKey}", selectionKey);

            // Attempt to transform the user's claims
            var transformedPrincipal = await _claimsTransformation.TransformAsync(result.Principal!);
            if (transformedPrincipal != null)
            {
                var claims = new List<Claim>(transformedPrincipal.Claims)
                {
                    new("Database", selectionKey),
                    new("IdP", selectedIdProvider)
                };

                var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
                _logger.LogInformation("Claims transformation succeeded. Prepared claims for sign-in.");

                // Log the claims being added (ensure sensitive information is handled appropriately)
                foreach (var claim in claims)
                {
                    _logger.LogDebug("Claim added: {ClaimType} - {ClaimValue}", claim.Type, claim.Value);
                }

                // Signing in the user
                await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), result.Properties);
                _logger.LogInformation("User signed in successfully after SAML authentication. SelectionKey: {SelectionKey}", selectionKey);

                return Ok();
            }
            else
            {
                _logger.LogWarning("Claims transformation returned null. User will be signed out. SelectionKey: {SelectionKey}", selectionKey);

                result.Principal?.RemoveSelectionKeyClaim();
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

                _logger.LogInformation("User signed out due to unsuccessful claims transformation.");

                var properties = new AuthenticationProperties
                {
                    RedirectUri = "/"
                };

                var claimsIdentity = result.Principal.Identity as ClaimsIdentity;
                var idPClaim = claimsIdentity?.Claims.FirstOrDefault(c => c.Type == "IdP");

                // Remove the "IdP" claim if it exists
                if (claimsIdentity != null && idPClaim != null)
                {
                    claimsIdentity.RemoveClaim(idPClaim);
                }

                _logger.LogInformation("Redirecting to home after failed claims transformation. IdP: {IdP}", idPClaim.Value);
                return SignOut(properties, CookieAuthenticationDefaults.AuthenticationScheme, idPClaim.Value);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Exception occurred in ACS endpoint: {Message} | SelectionKey: {SelectionKey}, SelectedIdProvider: {SelectedIdProvider}", ex.Message, selectionKey, selectedIdProvider);
            return await HandleAcsException(ex, $"Error in ACS endpoint: {ex.Message}");
        }
    }

appsetting.json:

"IdProviders": {
  "HO": {
    "Enabled": true,
    "DisplayName": "Proviso Test",
    "Saml2": {
      "EntityId": "entityid.4.proviso.fcdos.gov.uk",
      "IdPMetadata": "https://login.microsoftonline.com/8c47ef63-1296-4e7a-97b7-649f4eb09330/federationmetadata/2007-06/federationmetadata.xml?appid=1c1b4e3c-4301-4a18-a7ed-ed9b7a33b333",
      "LoginURL": "https://login.microsoftonline.com/8c47ef63-1296-4e7a-97b7-649f4eb09330/saml2",
      "LogoutURL": "https://login.microsoftonline.com/8c47ef63-1296-4e7a-97b7-649f4eb09330/saml2",
      "MicrosoftEntraIdentifier": "https://sts.windows.net/8c47ef63-1296-4e7a-97b7-649f4eb09330/",
      "AllowUnsolicitedAuthnResponse": true
    },
  "FCDOS": {
    "Enabled": true,
    "DisplayName": "NOT IN USE - FCDO Services (Sandbox) - NOT IN USE",
    "Saml2": {
      "EntityId": "https://proviso.fcdo.gov.uk",
      "IdPMetadata": "https://login.microsoftonline.com/08aff812-f74a-45a1-bc66-261ee3c74597/federationmetadata/2007-06/federationmetadata.xml?appid=af8b8c8b-f077-4ab5-a176-e2105aab844d",
      "LoginURL": "https://login.microsoftonline.com/08aff812-f74a-45a1-bc66-261ee3c74597/saml2",
      "LogoutURL": "https://login.microsoftonline.com/08aff812-f74a-45a1-bc66-261ee3c74597/saml2",
      "MicrosoftEntraIdentifier": "https://sts.windows.net/08aff812-f74a-45a1-bc66-261ee3c74597/",
      "AllowUnsolicitedAuthnResponse": true
    },
@joshuafranklinengineeringsystems joshuafranklinengineeringsystems changed the title configuring mulitple IdP's in in middleware, and attempting to invoke chosesn IdP in the Acs Challange, I get the following exception configuring mulitple Entra Id Saml2 based IdP's in in middleware, and attempting to invoke chosesn IdP in the Acs Challange, I get the following exception Dec 3, 2024
@joshuafranklinengineeringsystems joshuafranklinengineeringsystems changed the title configuring mulitple Entra Id Saml2 based IdP's in in middleware, and attempting to invoke chosesn IdP in the Acs Challange, I get the following exception configuring mulitple Azure Entra Id Saml2 based IdP's in in middleware, and attempting to invoke chosesn IdP in the Acs Challange, I get the following exception Dec 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant