angular.net-coreactive-directorysaml

CORS issue using Angular application calling a .NET Core API using Sustainsys.Saml2 library and Azure Active Directory as Identity Provicer


We are trying to implement SAML authentication in a .NET Core API, when a request comes from an Angular application. We're using the package Sustainsys.Saml2.AspNetCore2 (version 2.9.2) for .NET 6. We have successfully gotten the package to work, authenticating and authorizing a user to access our web API when accessing the API directly via browser.

Now we're trying to get an Angular application to connect to the backend API, and still perform the authentication. However, we receive the CORS error: "Access-Control-Allow-Origin" header was missing in the requested resource, which is the Azure AD.

enter image description here

We noticed that the "Origin" field is set to null in the OPTIONS request headers when calling the Identity Provider. The Identity Provider is hosted on https://login.microsoftonline.com (Azure AD).

enter image description here

We believe that because frontend is running on localhost:4200 and it calls the backend .NET api in localhost:7085, and the backend returns a challenge that redirects the browser to https://login.microsoftonline.com, is causing the browser to set the "Origin" to null and also causing CORS issue, due to this security. It's not feasible to make Angular or any client-side JavaScript application directly allow a cross-origin request without the server (in this case, Microsoft's server) including the Access-Control-Allow-Origin header. This is due to browser-enforced security known as the Same-Origin Policy.

As a reference, this is the code where we're adding SAML in .NET:

public static AuthenticationBuilder AddSaml2Authentication(this IServiceCollection services, IConfiguration configuration)
{
    var saml2Configuration = new Saml2Configurations();
    configuration.Bind(ConfigurationKey, saml2Configuration);
    return services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;

        options.DefaultChallengeScheme = Saml2Defaults.Scheme;
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, (options) =>
    {
        options.LoginPath = GetLoginPath(configuration);
        options.ReturnUrlParameter = "redirectUrl";
    })
    .AddSaml2(options =>
    {
        options.SPOptions.EntityId = new EntityId(saml2Configuration.SPEntityId);
        options.IdentityProviders.Add(new IdentityProvider(
        new EntityId(saml2Configuration.IdPEntityId),
        options.SPOptions)
        {
            MetadataLocation = saml2Configuration.IdPMetadataLocation
        });
    });
}

And this is the controller that takes care of the authentication:

public class AccountController : Controller
{
    [AllowAnonymous]
    [HttpGet("Login")]
    public IActionResult Login(string redirectUrl)
    {
        string? redirectUri = Url.Action(nameof(LoginCallback), new { redirectUrl });
        var properties = new AuthenticationProperties()
        {
            RedirectUri = redirectUri
        };
        ChallengeResult result = Challenge(properties, Saml2Defaults.Scheme);
        return result;
    }

    [AllowAnonymous]
    [HttpGet("Callback")]
    public async Task<IActionResult> LoginCallback(string redirectUrl)
    {
        AuthenticateResult authenticateResult = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        if (!authenticateResult.Succeeded)
        {
            return Unauthorized();
        }
        IEnumerable<Claim>? claimCollection = authenticateResult.Principal?.Claims;
        if (claimCollection == null || !claimCollection.Any())
        {
            return Problem(statusCode: StatusCodes.Status400BadRequest);
        }
        if (!string.IsNullOrEmpty(redirectUrl))
        {
            return Redirect(redirectUrl);
        }
        return Ok();
    }
}

How to fix this CORS issue and be able to authenticate using SAML through the Angular application and .NET Core Api?

UPDATE

We attempted to return the SAML login URL to the frontend and initiated a GET request from there. This allowed us to access and log in with the credentials within the Azure portal successfully. However, a new error has surfaced after clicking on 'SignIn' button:

An unhandled exception occurred while processing the request. UnexpectedInResponseToException: Received message _5d94ac02-98e3-4219-800e-8f389c747b3c contains unexpected InResponseTo "idcd3e9548a6184c7aa0e2c3a061b107f9". No cookie preserving state from the request was found so the message was not expected to have an InResponseTo attribute. This error typically occurs if the cookie set when doing SP-initiated sign on have been lost.

enter image description here


Solution

  • Change the Angular frontend to use window.location.href instead of a GET request when trying to authenticate the user solved the CORS issue.

    Basically, when we were using a GET request, the redirection was throwing CORS error because the backend was trying to redirect the frontend using AJAX and XMLHttpRequest redirection, and the browser blocks this behavior due to security concerns. We can solve this CORS issue by updating the Identity Provider (in this case Microsoft Azure AD) to allow our server as an origin, but since it is a Microsoft server, we are not able to do that. So the solution consisted on completely redirect the browser to our backend endpoint using window.location.href. This avoids CORS issues as the redirection is handled by a standard browser navigation.

    Angular code:

    // Replace the previous GET request with the following code:
    window.location.href = `your-backend-endpoint/Login?redirect=${encodeURIComponent(window.location.href)/dashboard}`;
    

    PS: after solving this issue, we added two new routes in our Angular frontend: