ASP.Net Core Antiforgery, Angular and IFRAMES - c#

Let's assume you have another website that wants to display your Angular application inside an IFRAME on one of their pages. How do you configure ASP.Net Core Antiforgery to work properly?

I spent a reasonable amount of time trying to piece this together, so this is my attempt to help others trying to figure out how to get anti-forgery tokens working with ASP.Net Core 6 and Angular application being displayed inside an IFRAME on another website.
This assumes you have already configured ASP.Net Core Antiforgery to work with Angular SPA.
ASP.Net Core Antiforgery, Angular and IFRAMES
Let's assume you have another website that wants to display your Angular application inside an IFRAME on one of their pages. You will encounter a couple of issues to get this working. Let's tackle them one at a time.
Refused to display in a frame because it set 'X-Frame-Options' to 'sameorigin'.
By default, when you call service.GetAndStoreTokens(context), the Antiforgery service sets a response header called X-Frame-Options to te value SAMEORIGIN. According to MDN, "The X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a <frame>, <iframe>, <embed> or <object>. Sites can use this to avoid click-jacking attacks, by ensuring that their content is not embedded into other sites."
In order to get past this issue, you need to turn off this behavior when setting up the Antiforgery service:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
// suppress setting X-Frame-Options to SAMEORIGIN
options.SuppressXFrameOptionsHeader = true;
});
Now your web page displays inside the IFRAME, but none of the POST requests work properly. That's because there are no cookies are being passed to it.
Cookies not being sent into IFRAME
According to this excellent article, "this is a relatively new problem, as up until recently cookies would be sent through cross-site requests. It all changed when the default value for the SameSite cookie attribute was changed by Google Chrome -- introducing new default behavior that prevents these cookies from going through cross-site requests."
If you watch the network traffic, you'll notice that the cookies that come down with you application entry point are marked as SameSite:Strict. This means they will only get sent if the request comes from a client directly connected to your site. They don't get sent to the IFRAME, so your Angular application is unable to read them and send it back in the HEADER of your API request.
To resolve this problem, you need to fix both of your cookies. Both of them need to be updated to SameSite:None (cookie can be sent to any site) and Secure:true (can only be delivered over HTTPS), which will allow the cookie to flow into the IFRAME.
First, update the Antiforgery service to fix the properties on the default cookie:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
// suppress setting X-Frame-Options to SAMEORIGIN
options.SuppressXFrameOptionsHeader = true;
// allow cookie to be sent to IFRAME
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});
Then update the cookie being sent to the Angular application:
var app = builder.Build();
app.MapControllers();
var service = app.Services.GetRequiredService<IAntiforgery>();
app.Use(async (context, next) =>
{
var path = context.Request.Path;
if (path.Equals("/default.html", StringComparison.CurrentCultureIgnoreCase))
{
// generate .AspNetCore.Antiforgery authentication cookie
var tokenSet = service.GetAndStoreTokens(context);
var token = tokenSet.RequestToken;
// duplicate the .AspNetCore.Antiforgery authentication and create a cookie called XSRF-TOKEN
if (token != null)
{
context.Response.Cookies.Append("XSRF-TOKEN", token, new CookieOptions
{
Path = "/",
HttpOnly = false,
// allow cookie to be sent to IFRAME
SameSite = SameSiteMode.None,
Secure = true
});
}
}
await next(context);
});
At this point, the application should display properly in the IFRAME and be able to interact with the Web API. I hope this helps!

Related

IDS4 - Redirect Loop When Logging In (Manual Navigation Works and Localhost Works)

Some info first:
Localhost everything works fine.
I uploaded and deployed to azure on a dev enviornment, having issues.
Uses IdentityServer4
Uses EntityFramework
I go to an endpoint on the Application that requires the user to be logged in (Home/Claim).
It correctly redirects to the login screen.
Username and Password are entered, it then correctly redirects back to the Application's OIDC endpoint with the set cookie headers.
It then redirects to the original Home/Claim endpoint. It does not have the cookies in the request.
It then redirects back to the login screen, sees the user is logged in and returns back to the initial Application's OIDC endpoint - putting the app into a loop.
Curious enough, if I stop the browser from loading the page then manually navigate back to the Application's Home/Claim endpoint, it works!? The c1 and c2 cookies are correctly in the header and the user is authenticated correctly. It does not redirect back to login.
This is what the redirect pattern looks like (Claim is the endpoint I am trying to access):
callback?client_id is on the Login's domain
signin0oidc is on the Application's domain
Claim is on the Application's domain
This is the initial Request Header on the first call when redirecting back to the Home/Claim endpoint (It is worth noting that on Localhost the cookies in the second image are set when it does its redirect to Home/Claim. It sets them the first time whereas on dev it is not):
When I manually navigate to the Home/Claim endpoint (after the above steps) this is what the Request Header looks like (notice the cookie is there):
Some final notes:
I did check the logs and verified no errors are being thrown. It is also not throwing any warning or errors saying that the Application failed authentication.
Configuration for both has been checked. Seems less likely to be a major configuration error since it does work after I refresh the page.
The Fix:
The marked answer fixed the issue. Specifically I just used this code from the article linked (putting it in the startup.cs of any clients wishing to use Identity Server).
app.Use(async (ctx, next) => {
if (ctx.Request.Path == "/signout-oidc" &&
!ctx.Request.Query["skip"].Any()) {
var location = ctx.Request.Path +
ctx.Request.QueryString + "&skip=1";
ctx.Response.StatusCode = 200;
var html = $ # " <
html > < head >
<
meta http - equiv = 'refresh'
content = '0;url={location}' / >
<
/head></html > ";
await ctx.Response.WriteAsync(html);
return;
}
await next();
if (ctx.Request.Path == "/signin-oidc" &&
ctx.Response.StatusCode == 302) {
var location = ctx.Response.Headers["location"];
ctx.Response.StatusCode = 200;
var html = $ # " <
html > < head >
<
meta http - equiv = 'refresh'
content = '0;url={location}' / >
<
/head></html > ";
await ctx.Response.WriteAsync(html);
}
});
What value of SameSite do you use for your Cookie?
Remember that a Strict value of SameSite tells the browser that it should not include cookies in case of a redirect from other domain.
However, the cookie is there, so if you access any url in your application (it is no longer a redirect), the cookie is included. Just go to the address bar and hit Intro with the same url. If the cookie is sent it will mean that this is surely your problem.
If this is your problem, set your Cookie whit SameSite = Lax. Lax allows the cookie to be sent on cross redirections.
You can learn more about this issue and try other solution proposed from Brockallen blog: Same-site cookies, ASP.NET Core, and external authentication providers. This solution maintains the Strict attribute in your Cookie.

Microsoft Identity Web: Change Redirect Uri

I am using .net 5, Identity Web Ui to access Microsoft Graph. Where can I configure my Redirect URI?
I need to specify the full Uri, since the generated one from callbackUri is incorrect due to being behind a Load Balancer with SSL offload.
Here is my current ConfigureServices section
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
I was facing a similar problem with a WebApp exposed only behind a front door, the WebApp had to call a custom downstream WebApi.
My service configuration that worked on my localhost dev machine:
// AzureAdB2C
services
.AddMicrosoftIdentityWebAppAuthentication(
Configuration,
"AzureAdB2C", subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true)
.EnableTokenAcquisitionToCallDownstreamApi(p =>
{
p.RedirectUri = redUri; // NOT WORKING, WHY?
p.EnablePiiLogging = true;
},
[... an array with my needed scopes]
)
.AddInMemoryTokenCaches();
I tried the AddDownstreamWebApi but did not manage to make it work so I just fetched the needed token with ITokenAcquisition and added it to an HttpClient to make my request.
Then I needed AzureAd/B2C login redirect to the uri with the front door url:
https://example.org/signin-oidc and things broke. I solved it like this:
First of all you have to add this url to your App registration in the azure portal, very important is case sensitive it cares about trailing slashes and I suspect having many urls that point to the very same controller and the order of these have some impact, I just removed everything and kept the bare minimum.
Then in the configure services method:
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SaveTokens = true; // this saves the token for the downstream api
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctxt =>
{
// Invoked before redirecting to the identity provider to authenticate. This can be used to set ProtocolMessage.State
// that will be persisted through the authentication process. The ProtocolMessage can also be used to add or customize
// parameters sent to the identity provider.
ctxt.ProtocolMessage.RedirectUri = "https://example.org/signin-oidc";
await Task.Yield();
}
};
});
With that the redirect worked, but I entered a loop between the protected page and the AzureB2C login.
After a succesful login and a correct redirect to the signin-oidc controller (created by the Identity.Web package) I was correctly redirected again to the page that started all this authorization thing, but there it did not find the authorization. So I added/modded also this:
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.Secure = CookieSecurePolicy.Always;
});
With this the authorization worked, but I was not able to get the token to call the downstream API, before this redirect thing ITokenAcquisition worked, but now when trying to get the token it throws an exception.
So in my controller/service to get the token I modified and used:
var accessToken = await _contextAccessor.HttpContext
.GetTokenAsync(OpenIdConnectDefaults.AuthenticationScheme, "access_token");
So now with the token I add it to my HttpRequestMessage like this:
request.Headers.Add("Authorization", $"Bearer {accessToken}");
I lived on StackOverflow and microsoft docs for 3 days, I am not sure this is all "recommended" but this worked for me.
I had the same problem running an asp.net application under Google Cloud Run, which terminates the TLS connection. I was getting the error:
AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application.
Using fiddler, I examined the request to login.microsoftonline.com and found that the query parameter redirect_uri exactly matched the url I'd configured in the application in Azure except that it started http rather than https.
I initially tried the other answers involving handling the OpenIdConnectEvents event and updating the redirect uri. This fixed the redirect_url parameter in the call to login.microsoftonline.com and it then worked until I added in the graph api. Then I found my site's signin-oidc page would give its own error about the redirect uri not matching. This would then cause it to go into a loop between my site and login.microsoftonline.com repeatedly trying to authenticate until eventually I'd get a login failure.
On further research ASP.net provides middleware to properly handle this scenario. Your SSL load balancer should add the standard header X-Forwarded-Proto with value HTTPS to the request. It should also send the X-Forwarded-For header with the originating IP address which could be useful for debugging, geoip etc.
In your ASP.net application, to configure the middleware:
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
Then enable the middleware:
app.UseForwardedHeaders();
Importantly, you must include this before the calls to app.UseAuthentication/app.UseAuthorization that depends on it.
Source: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0
If your load balancer doesn't add the X-Forwarded-Proto header and can't be configured to do so then the document above outlines other options.
I was facing with similar issue for 3 days. The below code helped me to get out of the issue.
string[] initialScopes = Configuration.GetValue<string>("CallApi:ScopeForAccessToken")?.Split(' ');
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddInMemoryTokenCaches();
services.AddControllers();
services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser().Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SaveTokens = true; // this saves the token for the downstream api
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctxt =>
{
ctxt.ProtocolMessage.RedirectUri = "https://example.org/signin-oidc";
await Task.Yield();
}
};
});

How to configure the OAuth callback to a different domain in ASP.NET Core authentication

I am Authenticating against an OAuth endpoint where I can only configure 1 callback domain. (and localhost is whitelisted).
I have my web app running in Azure (myapp.azurewebsites.net) and have it available with two custom domains (myapp.cc and myapp.eu). When I use the default setup, the CallbackPath can only be a relative path (to the current domain)
The code documentation of CallbackPath indicates it's relative to the application's base path:
/// <summary>
/// The request path within the application's base path where the user-agent will be returned.
/// The middleware will process this request when it arrives.
/// </summary>
public PathString CallbackPath { get; set; }
I want to make sure the CallBack happens to the (only) domain that I whitelisted on the OAuth backend. I know I can implement everything manually, but I was hoping there would be an easy way to work around this design and still benefit from the baked in Authentication options.
So even if a user is logging on on the myapp.cc or the myapp.eu or the myapp.azurewebsites.net , it should redirect to myapp.azurewebsites.net/ (which is whitelisted on my Auth service)
A part of my Startup.cs file is pasted below:
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "MyService";
})
.AddCookie()
.AddOAuth("MyService", "MyService",
options =>
{
options.ClientId = settings.ClientId;
options.ClientSecret = settings.ClientOauthSecret;
options.CallbackPath = "/relativeonlypath";
options.SaveTokens = true;
options.SignInScheme = IdentityConstants.ExternalScheme;
/// ... removed for brevity
}
);
Any idea on how to implement this?
Thank you
I'm not sure it's possible, because to verify that the user is redirected to your application as part of a "genuine" authentication flow, the ASP.NET OAuth handler performs the following steps:
Before redirecting the user to the OAuth service, ASP.NET Core generates a "correlation" cookie that is tied to the current domain; and
When the user is redirected to the app, the handler looks for this cookie and validates its content.
So if the correlation cookie is generated in step #1 for one domain, let's say myapp.cc, and the user is redirected to another domain, myapp.azurewebsites.net, ASP.NET Core might not be able to read it because the browser will not have included it in the redirection request.
Note
As seen in the first comments, the original thought was to leverage the SameSiteproperty of the correlation cookie to have it sent by the browser to the second domain.
This was all wrong, apologies!
I now think that you have 2 different options:
Redirect every request from myapp.cc and myapp.eu to myapp.azurewebsites.net, so that when the authentication flow happens, we're already on the right domain; or
Redirect the user to the myapp.azurewebsites.net domain before redirecting them to the OAuth server.
I won't go into the first solution, as there's plenty of ways to achieve this.
Here's some code that I haven't tested that could work for the second solution:
services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "MyService";
})
.AddCookie()
.AddOAuth("MyService", options =>
{
options.Events.OnRedirectToAuthorizationEndpoint = context =>
{
var currentRequestUri = new Uri(context.Request.GetDisplayUrl());
// 1. If we're not on the correct domain, redirect the user to the same page, but on the expected domain.
// The assumption is that the authentication flow will also kick in on the other domain (see 2).
if (currentRequestUri.Host != "myapp.azurewebsites.net")
{
var applicationRedirectUri = new UriBuilder(currentRequestUri)
{
Host = "myapp.azurewebsites.net"
}.Uri.ToString();
context.Response.Redirect(applicationRedirectUri);
return Task.CompletedTask;
}
// 2. If we reach here, it means we're on the right domain, so we can redirect to the OAuth server.
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});

Set-cookie not working for cross-site request / response in Dot net Core 3.1 & React setup same-site cookies and/or CORS issue

Alright, I am stumped! I have been trying to solve this for hours now with no luck. I am following this guide to use JWT for auth in a Dot Net Core 3.1 / React.js (typescript) project I am working on to learn the whole setup. I am working using cross site requests. My React server is communicating on https://localhost:3000 (dev using Visual Studio Code), and my API / back end, API server is running on https://localhost:44309 running in Visual Studio.
I am trying to send a refresh token back to the client and the guide states this needs to be in a HTTP Only cookie to mitigate XSS. No matter what I try, I cannot get the browser to execute the ‘set-cookie’ at the client side so I can see it in Google Chrome's Dev Tools > Application > Cookies. This is for any cookie that I set in the response at all. If I use Google Chrome’s Developer Tools panel, in the network response I can see the ‘set-cookie’ headers are there, but they never show in ‘Application > Cookies > LocalHost’. The response I am sending sends the payloads and that can be used / read with no issue. It just will not set cookies!
The setup works fine when I use the same server for client and server application parts (just run it in IIS in the standard Visual Studio setup); any / all cookies set with no problems, so I am guessing I am working with a cross-site issue. I just do not know how to fix it.
My code:
//setting up cors
services.AddCors(options =>
{
options.AddPolicy("CORSAllowLocalHost3000",
builder =>
builder.WithOrigins("https://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
});
//using cors
app.UseCors("CORSAllowLocalHost3000");
//setting up auth
services
.AddDefaultIdentity<ApplicationUser>
(
options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequiredLength = 6;
}
)
.AddEntityFrameworkStores<IdentityApplicationContext>();
services
.AddAuthentication(opts =>
{
opts.DefaultAuthenticateScheme = "JwtBearer";
opts.DefaultScheme = "JwtBearer";
opts.DefaultChallengeScheme = "JwtBearer";
})
.AddJwtBearer("JwtBearer", opts =>
{
opts.SaveToken = true;
opts.TokenValidationParameters = tokenValParams;
});
//tokenValParams to validate the JWT
var tokenValParams = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key:
Encoding.ASCII.GetBytes(configuration.GetSection("Authentication").GetSection("JwtBearer").GetSection("SecurityKey").Value)),
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
//for API only dev env, start as API only service - no browser - client app runs
app.UseStaticFiles();
if (!env.IsEnvironment("APIOnlyDevelopment"))
app.UseSpaStaticFiles();
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
//spa.UseProxyToSpaDevelopmentServer("https://localhost:3000");
});
//Test Cookie generated in client before async Ok() result is returned in controller
HttpContext.Response.Cookies.Append("JwtRefreshTokenn", resultOfSignin.RefreshToken, new CookieOptions() { Secure = true, HttpOnly = true, SameSite = SameSiteMode.None});
Response.Cookies.Append("JwtRefreshTokenn2", resultOfSignin.RefreshToken, new CookieOptions() { HttpOnly = true, Secure = true, SameSite = SameSiteMode.None});
Response.Cookies.Append("JwtRefreshTokenn3", resultOfSignin.RefreshToken, new CookieOptions() { });
Further information:
I have changed the ‘App URL’ in Visual Studio to that of the ‘Enable SSL’ URL as I was getting a CORS issue with a redirect that was occurring.
I am running the server using the inbuilt ‘HTTPS’ setup, and the client app using npm’s https setup
(including sorting the cert error out as with this post).
I have tried all combinations of cookie options, including adding domains / paths (and all variations of same-site attribute)
I have tried different things in the CORS policy (e.g. omitting .AllowCredentials)
I have tried using http rather than https
Firefox is still having CORS issues with the requests that Google Chrome is not
The problem is mirrored in MS Edge
All running in Windows 10
I am relatively new to this, so please let me know if I have missed anything out.
Any helps is greatly appreciated. Many thanks, Paul
After a good few hours invested in this, it was a simple fix that was required on the client. The setup above is / was good and working, with a particular mention to this:
services.AddCors(options =>
{
options.AddPolicy("CORSAllowLocalHost3000",
builder =>
builder.WithOrigins("https://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials() // <<< this is required for cookies to be set on the client - sets the 'Access-Control-Allow-Credentials' to true
);
});
As the client is using axios to make API calls from within React, a global default was needed to be set to match this / work with this header. So at least in one place in the program where axios is imported, the default is set as so:
import axios from 'axios';
axios.defaults.withCredentials = true;
I had a custom axios creator file setup so that I could use e.g. interceptors etc, which is where I put this code. Once these two things were added & aligned, the cookies were being set.
Hope it helps.
I see the problem was with my front end. I was using the fetch api and there I needed to add credentials: "include" then the cookie was saved.

Handling sessions to web api

I have a requirement where our website can generate a tracking id to pass out to users to track how they come to our website.
The website is an Angular frontend and a .Net Web Api2 backend and they are part of the same domain
My initial plan is to have the the initial tracking url go direct to the api (e.g. http://api.mywebsite.com/t/45mg9) and then this returns a redirect to the relevant page (e.g. http://web.mywebsite.com).
The redirect is working fine but I want to keep track of the initial tracking code as the user might not initially be logged in so need to keep hold of it until they are logged in.
I've tried:
Cookies:
On the redirect message I've set a cookie using the following:
var response = new HttpResponseMessage(System.Net.HttpStatusCode.Redirect);
response.Headers.Location = new Uri(tracking.Url);
var cookie = new CookieHeaderValue("tracking", JsonConvert.SerializeObject(tracking))
{
HttpOnly = true,
Path = "/",
Domain = "mywebsite.com",
Expires = DateTimeOffset.UtcNow.AddDays(30)
};
response.Headers.AddCookies(new[] { cookie });
return ResponseMessage(response);
And can see the cookie being set on the redirect but then it isn't there when the Web app does subsequent calls to the api.
Session
I've enabled sessions in my web api and set the the value but it seems to lose it with redirects.
I want to try and keep this within the api as I don't think the web app needs to know about the information being stored, only the api is interested.
How can I go about persisting information (cookie, session etc) being sent to an api? Do I need to tell the web app to pass cookies to the api call?
You can reissue the tracking id from webapp to api in each request by adding a request header with the tracking id.
At web app the tracking id can be persisted in a cookie or localstorage.
For adding the tracking id in header use a httpinterceptor for each call to api.
#Injectable()
export class MyInterceptor implements HttpInterceptor {
constructor() { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = request.clone({
setHeaders: {
TrackingId: localStorage.getItem('trackingId')
}
});
return next.handle(request);
}
}

Categories