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.
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).
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.
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:
/login
: to handle the authentication using window.location.href
redirecting the browser to our backend /Login
./dashboard
: that first does a GET request to the /Callback
backend endpoint to verify if the user is already authenticated, and then loads our dashboard with the authenticated user. If /Callback
request returns 401 Unauthorized, we simply redirect the user to the '/login' route to handle the authentication as showed above.
PS.2: both backend endpoints /Login
and /Callback
were already implemented as the samples in Sustainsys library on GitHub. We didn't need to change anything in the backend to fix this issue, just on frontend.