Xamarin WKWebView and Cookies - cookies

I have a Xamarin Forms app that uses cookies to track login status and uses both HTTPRequests and Webviews, so both need to share cookies. With UIWebView this cookies were shared without any extra management on my part; with WKWebView this appears not to be the case. I have been searching for an explanation on how cookies are handled with WKWebView or an example of how to manually retrieve and set the cookies between these two objects, but have been unable to find any. How do I get the cookie behavior that I have relied on when using UIWebView with WKWebView?

When I tried to implement a WKNamvigationDelegate the WebView OnLoadFinished was not called, so my loading indicator remained after loading was complete. What ended up working for me is in my iOS CustomWebViewRenderer's constructor I call this function to clear out any existing cookies and copy any cookies from the HTTP Shared Storage into the webview:
protected async void SetCookies()
{
var dataStore = WKWebsiteDataStore.DefaultDataStore;
var cookies = NSHttpCookieStorage.SharedStorage.Cookies;
var oldcookies = await dataStore.HttpCookieStore.GetAllCookiesAsync();
foreach (var cookie in oldcookies)
{
await dataStore.HttpCookieStore.DeleteCookieAsync(cookie);
}
foreach (var cookie in cookies)
{
await dataStore.HttpCookieStore.SetCookieAsync(cookie);
}
}
To get the cookies from the webview I have existing in shared code a CustomWebView that uses OnShouldLoad to detect the indication of a successful login, then call platform specific code. This was created to handle Android cookies, but will now work for iOS as well. The iOS implementation clears out any existing HTTP shared storage cookies and copies the cookies from the webview into the Shared Storage.
public async Task GetCookiesFromWebview()
{
var dataStore = WKWebsiteDataStore.DefaultDataStore;
var cookies = await dataStore.HttpCookieStore.GetAllCookiesAsync();
var oldcookies = NSHttpCookieStorage.SharedStorage.Cookies;
foreach (var cookie in oldcookies)
{
NSHttpCookieStorage.SharedStorage.DeleteCookie(cookie);
}
foreach (var cookie in cookies)
{
NSHttpCookieStorage.SharedStorage.SetCookie(cookie);
}
return;
}

You can create a custom WKNavigationDelegate.
public class MyCustomWebViewDelegate : WKNavigationDelegate
{
public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action<WKNavigationActionPolicy> decisionHandler)
{
var jCookies = Appl.FlurlClient.Cookies.Values;
NSHttpCookie[] nsHttpCookies = jCookies.Where(c => c != null).Select(c => new NSHttpCookie(c)).ToArray();
foreach (var c in nsHttpCookies)
{
webView.Configuration.WebsiteDataStore.HttpCookieStore.SetCookie(c);
}
decisionHandler(WKNavigationActionPolicy.Allow);
}
}
and assign in to webView as :
webView.NavigationDelegate = new MyCustomWebViewDelegate();

There is difference above iOS 12 to get cookie from WKWebview .You can have a try with getting cookie by invoking DecidePolicy method from WKNavigationDelegate .
public class NavigationDelegate : WKNavigationDelegate
{
NSMutableArray multiCookieArr = new NSMutableArray();
public override void DecidePolicy(WKWebView webView, WKNavigationResponse navigationResponse, [BlockProxy(typeof(Action))]Action<WKNavigationResponsePolicy> decisionHandler)
{
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
WKHttpCookieStore wKHttpCookieStore = webView.Configuration.WebsiteDataStore.HttpCookieStore;
Console.WriteLine("wKHttpCookieStore is :" + wKHttpCookieStore.GetDebugDescription());
wKHttpCookieStore.GetAllCookies(cookies => {
if(cookies.Length > 0)
{
foreach (NSHttpCookie cookie in cookies)
{
//NSHttpCookieStorage.SharedStorage.SetCookie(cookie);
Console.WriteLine("cookie is :" + cookie);
}
}
});
}
else
{
NSHttpUrlResponse response = navigationResponse.Response as NSHttpUrlResponse;
NSHttpCookie[] cookiesAll = NSHttpCookie.CookiesWithResponseHeaderFields(response.AllHeaderFields, response.Url);
foreach (NSHttpCookie cookie in cookiesAll)
{
Console.WriteLine("Here is the cookie inside wkwebview is :" + cookie);
NSArray cookieArr = NSArray.FromObjects(cookie.Name, cookie.Value, cookie.Domain, cookie.Path);
multiCookieArr.Add(cookieArr);
}
Console.WriteLine("cookie is :" + cookiesAll);
}
decisionHandler(WKNavigationResponsePolicy.Allow);
//base.DecidePolicy(webView, navigationResponse, decisionHandler);
}
The renderer code as follow :
public class HybridWebViewRenderer : WkWebViewRenderer
{
public HybridWebViewRenderer() : this(new WKWebViewConfiguration())
{
}
public HybridWebViewRenderer(WKWebViewConfiguration config) : base(config)
{
}
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
//...
}
if (e.NewElement != null)
{
this.NavigationDelegate = new NavigationDelegat();
}
}
}
Also can refer to this dicussion :How to Copy Cookie to WKWebview in iOS?

Related

How to Copy Cookie to WKWebview in iOS?

I don't know how/which cookies are coming from which website. Because of that, I can't set manually the cookie names.
How can I get third party cookies to paste to a WKWebview? Here is my code but no chance.
My webview;
public class CustomWebView : WebView
{
public static readonly BindableProperty UriProperty =
BindableProperty.Create(
propertyName: "Uri",
returnType: typeof(Uri),
declaringType: typeof(CustomWebView),
defaultValue: default(string));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
}
My custom renderer (Shouldn't necessary an event per request? This method fires once in the first request);
[assembly: ExportRenderer(typeof(CustomWebView), typeof(HTMobile.iOS.WebViewRenderer))]
namespace HTMobile.iOS
{
public class WebViewRenderer : ViewRenderer<CustomWebView, WKWebView>
{
protected override void OnElementChanged(ElementChangedEventArgs<CustomWebView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
// Cookie
var cookieUrl = new Uri("abc.com");
NSHttpCookieStorage.SharedStorage.AcceptPolicy = NSHttpCookieAcceptPolicy.Always;
var cookieJar = NSHttpCookieStorage.SharedStorage;
cookieJar.AcceptPolicy = NSHttpCookieAcceptPolicy.Always;
foreach (var aCookie in cookieJar.Cookies)
{
cookieJar.DeleteCookie(aCookie);
}
var jCookies = UserInfo.CookieContainer.GetCookies(cookieUrl);
IList<NSHttpCookie> eCookies =
(from object jCookie in jCookies
where jCookie != null
select (Cookie)jCookie
into netCookie
select new NSHttpCookie(netCookie)).ToList();
cookieJar.SetCookies(eCookies.ToArray(), cookieUrl, cookieUrl);
// WebView Instance
webView = new WKWebView(Frame, new WKWebViewConfiguration());
SetNativeControl(webView);
if (e.NewElement != null)
{
Control.LoadRequest(new NSUrlRequest(new NSUrl("abc.com")));
}
}
}
}
}
I think an event should be fired per request and I should be able to get a cookie list for the visited page and then set it to my WebView.
Advise, please.
You can have a try with getting cookie by invoking DecidePolicy method from WKNavigationDelegate .
public class NavigationDelegate : WKNavigationDelegate
{
NSMutableArray multiCookieArr = new NSMutableArray();
public override void DecidePolicy(WKWebView webView, WKNavigationResponse navigationResponse, [BlockProxy(typeof(Action))]Action<WKNavigationResponsePolicy> decisionHandler)
{
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
WKHttpCookieStore wKHttpCookieStore = webView.Configuration.WebsiteDataStore.HttpCookieStore;
Console.WriteLine("wKHttpCookieStore is :" + wKHttpCookieStore.GetDebugDescription());
wKHttpCookieStore.GetAllCookies(cookies => {
if(cookies.Length > 0)
{
foreach (NSHttpCookie cookie in cookies)
{
//NSHttpCookieStorage.SharedStorage.SetCookie(cookie);
Console.WriteLine("cookie is :" + cookie);
}
}
});
}
else
{
NSHttpUrlResponse response = navigationResponse.Response as NSHttpUrlResponse;
NSHttpCookie[] cookiesAll = NSHttpCookie.CookiesWithResponseHeaderFields(response.AllHeaderFields, response.Url);
foreach (NSHttpCookie cookie in cookiesAll)
{
Console.WriteLine("Here is the cookie inside wkwebview is :" + cookie);
NSArray cookieArr = NSArray.FromObjects(cookie.Name, cookie.Value, cookie.Domain, cookie.Path);
multiCookieArr.Add(cookieArr);
}
Console.WriteLine("cookie is :" + cookiesAll);
}
decisionHandler(WKNavigationResponsePolicy.Allow);
//base.DecidePolicy(webView, navigationResponse, decisionHandler);
}
In addition , using Renderer to Customizing a WebView you can refer to this doc .
public class HybridWebViewRenderer : WkWebViewRenderer
{
public HybridWebViewRenderer() : this(new WKWebViewConfiguration())
{
}
public HybridWebViewRenderer(WKWebViewConfiguration config) : base(config)
{
}
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
//...
}
if (e.NewElement != null)
{
this.NavigationDelegate = new NavigationDelegat();
}
}
}

IdentityServer4 ignores RememberMe

I am working on a project with IdentityServer4, Asp.Net.Identity and all is done in MVC and Razor. I have most of it working, the only thing I am struggling with is the RememberMe capability. Currently Idsrv sets the cookie even though RememberMe is false when logging in.
Here is my code:
The LoginModel challenges Idsrv with the oidc Authentication Scheme.
public class LoginModel : PageModel {
public IActionResult OnGet() {
return Challenge(new AuthenticationProperties {
RedirectUri = "/" }, "oidc");
}
}
}
Here is my startup extension method for the client authentication
public static void AddClientAuthentication(this IServiceCollection services) {
services.AddAuthentication(options => {
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options => {
options.SignInScheme = "Cookies";
options.Authority = AuthorityUrl;
options.RequireHttpsMetadata = true;
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.ClientId = "*ClientId*";
options.ClientSecret = "*ClientSecret*";
options.Scope.Add("profile");
options.Scope.Add("openid");
});
}
And here is my login-logic:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model) {
if (!ModelState.IsValid) {
return View(model);
}
SignInResult result = await signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberLogin, true);
if (result.Succeeded) {
ApplicationUser user = await userManager.FindByEmailAsync(model.Email);
await events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName));
return Redirect("https://localhost:5002/home");
}
The issue is, that cookies are set even though model.RememberLogin is false. Does anyone have a solution for that? Thank you in advance!
So, it was not a bug in my code. I didn't know that there is a cookie set for the duration of the browser session.

How to manage Facebook login to avoid authentication frequently in Xamarin.Forms?

I use following code for Facebook login and access user information like albums and pictures. I have set code to get access token using following code. Now, the problem is I need to get access token everytime when user open application. However, once user authenticate, application will not ask for authenticate until user close the application. But it will ask for authenticate again after user reopen application. This way user will frustrate if they will ask to authentication everytime they will try to access albums or any other things of facebook.
Is there anyway to skip this? I mean once user provided access of Facebook, application must not ask for login(authenticate). I will have access token but I don't know how to use to play with authentication. So, we can avoid authentication frequently.
My Code:
public class FacebookService : IFacebookService
{
private readonly string[] permissions = { "public_profile", "email", "user_birthday", "user_photos" };
public event EventHandler<FacebookUser> LoginCompleted;
public string Token => AccessToken.CurrentAccessToken.TokenString;
public void Logout()
{
LoginManager manager = new LoginManager();
manager.LogOut();
}
public void LogInToFacebook()
{
if (AccessToken.CurrentAccessToken == null)
{
ObtainNewToken(LogInToFacebook);
return;
}
var fields = new[] { "name", "email", "birthday", "gender", "picture" };
var query = $"/me?fields={string.Join(",", fields)}";
var token = AccessToken.CurrentAccessToken.TokenString;
var request = new GraphRequest(query, null, token, null, "GET");
request.Start((connection, result, error) =>
{
if (error != null)
{
HandleError(error.LocalizedDescription);
}
else
{
var userInfo = result as NSDictionary;
var id = userInfo["id"].ToString();
var email = userInfo["email"].ToString();
var name = userInfo["name"].ToString();
var birthday = userInfo["birthday"].ToString();
var gender = userInfo["gender"].ToString();
var picture = ((userInfo["picture"] as NSDictionary)["data"] as NSDictionary)["url"].ToString();
var args = new FacebookUser(id, email, name, birthday, gender, picture);
LoginCompleted?.Invoke(this, args);
}
});
}
public async System.Threading.Tasks.Task RequestAlbums(Action<FacebookAlbum[]> callback)
{
if (AccessToken.CurrentAccessToken == null)
{
ObtainNewTokenForAlbum(callback);
return;
}
using (HttpClient client = new HttpClient())
{
try
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
var host = "https://graph.facebook.com/";
var json = await client.GetStringAsync($"{host}me/albums");
var data = JObject.Parse(json).First.First.ToString();
var albums = JsonConvert.DeserializeObject<FacebookAlbum[]>(data);
var getPhotosTasks = new List<System.Threading.Tasks.Task>();
foreach (var album in albums)
getPhotosTasks.Add(System.Threading.Tasks.Task.Run(() => RequestPhotos(album)));
await System.Threading.Tasks.Task.WhenAll(getPhotosTasks.ToArray());
callback(albums);
}
catch (Exception ex1)
{
HandleError(ex1.Message);
}
}
}
private void ObtainNewTokenForAlbum(Action<FacebookAlbum[]> callback)
{
var login = new LoginManager();
login.LogInWithReadPermissions(permissions, null, (r, e) =>
{
if (e == null && !r.IsCancelled)
{
RequestAlbums(callback);
}
else
HandleError(e?.LocalizedDescription);
});
}
private async System.Threading.Tasks.Task RequestPhotos(FacebookAlbum album)
{
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
try
{
var host = "https://graph.facebook.com/";
var json = await client.GetStringAsync($"{host}{album.Id}/photos?fields=source,picture");
var data = JObject.Parse(json)["data"].ToString();
album.Photos = JsonConvert.DeserializeObject<FacebookPicture[]>(data);
}
catch (Exception exc)
{
HandleError(exc.Message);
}
}
}
private void ObtainNewToken(Action callback)
{
var login = new LoginManager();
login.LogInWithReadPermissions(permissions, null, (r, e) =>
{
if (e == null && !r.IsCancelled)
callback?.Invoke();
else
HandleError(e?.LocalizedDescription);
});
}
private void HandleError(string messageDescription)
{
messageDescription = messageDescription ?? "Request was cancelled";
_notificationService.DisplayNotification(messageDescription, Colors.d8Red);
}
}
AppDelegate
public override bool FinishedLaunching(UIApplication uiApplication, NSDictionary launchOptions)
{
UAirship.TakeOff();
RegisterServices();
SetupFacebookSDK();
FFImageLoading.Forms.Touch.CachedImageRenderer.Init();
var dummy = new FFImageLoading.Forms.Touch.CachedImageRenderer();
Xamarin.Forms.Forms.Init();
LoadApplication(new App());
UIApplication.SharedApplication.StatusBarHidden = false;
UIApplication.SharedApplication.SetStatusBarStyle(UIStatusBarStyle.LightContent, false);
_networkManager = new NetworkManager();
OverrideDefaultListViewCustomActionsColors();
UAirship.Push.UserPushNotificationsEnabled = true;
new PhotoAccessChecker();
return ApplicationDelegate.SharedInstance.FinishedLaunching(uiApplication, launchOptions);
}
void SetupFacebookSDK()
{
FacebookProfile.EnableUpdatesOnAccessTokenChange(true);
FacebookSettings.AppID = "000000000049000";
FacebookSettings.DisplayName = "MyProduct";
}
public override bool OpenUrl(UIApplication application, NSUrl url, string sourceApplication, NSObject annotation)
{
return ApplicationDelegate.SharedInstance.OpenUrl(application, url, sourceApplication, annotation);
}
I guess you forgot initialize FBSDK in AppDelegate.
Check your code if return ApplicationDelegate.SharedInstance.FinishedLaunching (application, launchOptions); has been executed in FinishedLaunching.
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
Settings.AppID = appId;
Settings.DisplayName = appName;
// ...
// This method verifies if you have been logged into the app before, and keep you logged in after you reopen or kill your app.
return ApplicationDelegate.SharedInstance.FinishedLaunching (application, launchOptions);
}
public override bool OpenUrl (UIApplication application, NSUrl url, string sourceApplication, NSObject annotation)
{
// We need to handle URLs by passing them to their own OpenUrl in order to make the SSO authentication works.
return ApplicationDelegate.SharedInstance.OpenUrl (application, url, sourceApplication, annotation);
}

Auth0 OWIN API not validating JWT token when published via Docker Container on AWS EC2

My OWIN Web.API 2 Hosted on EC2 will not authorize a JWT token. I have tested the functionality locally with out a problem but once I publish it out to my docker container hosted on EC2 it responds with a 401. I am using the default RS256 Algorithm and these settings:
var domain = Environment.GetEnvironmentVariable("AUTH0_DOMAIN");
var audience = Environment.GetEnvironmentVariable("AUTH0_CLIENT_IDS");
var keyResolver = new OpenIdConnectSigningKeyResolver(domain);
appBuilder.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audience },
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = audience,
ValidIssuer = domain,
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) => keyResolver.GetSigningKey(identifier)
}
});
My Endpoint simply states whether your are authenticated or not.
[Authorize]
[Route("secure")]
public HttpResponseMessage GetSecured()
{
var userId = ClaimsPrincipal.Current.Identity.GetUserId();
return Request.CreateResponse($"Hello, {userId}! You are currently authenticated.");
}
Here is my Startup config:
public void Configuration(IAppBuilder appBuilder)
{
appBuilder.UseCors(CorsOptions.AllowAll); //must be first
Auth0Config.Register(appBuilder);
var httpConfiguration = new HttpConfiguration();
httpConfiguration.MapHttpAttributeRoutes();
UnityConfig.Register(httpConfiguration);
appBuilder.UseWebApi(httpConfiguration);
}
I no longer use the OWIN pipeline but from a previous project here is how I had it configured. It looks like you are using OpenID, I did not. Not sure if this helps.
var issuer = AppSettings.Auth0Domain;
var audience = AppSettings.Auth0ClientID;
var secret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["Auth0ClientSecret"]);
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] {audience},
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, secret)
},
Provider = new Auth0AuthenticationProvider()
});
EDIT Added Auth0AuthenticationProvider
public class Auth0AuthenticationProvider : IOAuthBearerAuthenticationProvider
{
private string token;
public Task ApplyChallenge(OAuthChallengeContext context)
{
return Task.FromResult<object>(null);
}
public Task RequestToken(OAuthRequestTokenContext context)
{
token = context.Token;
return Task.FromResult<object>(null);
}
public Task ValidateIdentity(OAuthValidateIdentityContext context)
{
if (string.IsNullOrEmpty(token))
return Task.FromResult<object>(null);
var notPadded = token.Split('.')[1];
var padded = notPadded.PadRight(notPadded.Length + (4 - notPadded.Length % 4) % 4, '=');
var urlUnescaped = padded.Replace('-', '+').Replace('_', '/');
var claimsPart = Convert.FromBase64String(urlUnescaped);
var obj = JObject.Parse(Encoding.UTF8.GetString(claimsPart, 0, claimsPart.Length));
// simple, not handling specific types, arrays, etc.
foreach (var prop in obj.Properties().AsJEnumerable())
{
switch (prop.Name)
{
case "app_metadata":
SetAppMetadataClaims(context, prop.Value.ToString());
break;
}
}
return Task.FromResult<object>(null);
}
private static void SetAppMetadataClaims(OAuthValidateIdentityContext context, string jsonString)
{
var appMetadata = JsonConvert.DeserializeObject<Auth0AppMetaDataModel>(jsonString);
if(!context.Ticket.Identity.HasClaim("AccountId", appMetadata.accountId.ToString()))
context.Ticket.Identity.AddClaim(new Claim("AccountId", appMetadata.accountId.ToString()));
if (!context.Ticket.Identity.HasClaim("ClientId", appMetadata.clientId.ToString()))
context.Ticket.Identity.AddClaim(new Claim("ClientId", appMetadata.clientId.ToString()));
if (!context.Ticket.Identity.HasClaim("IsActive", appMetadata.isActive.ToString()))
context.Ticket.Identity.AddClaim(new Claim("IsActive", appMetadata.isActive.ToString()));
if (appMetadata.roles == null)
return;
foreach (var role in appMetadata.roles)
{
if (context.Ticket.Identity.HasClaim(ClaimTypes.Role, role))
continue;
context.Ticket.Identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
}
}
The problem was not with Auth0 using HS256 or RS256 but instead with the base image I was using. Mono was failing silently most likely blocking the validation of my token. I have switched over to dotnet core in order to use the docker image: microsoft/dotnet:latest
My Startup file now appears like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCors("CorsPolicy");
var options = new JwtBearerOptions
{
Audience = Configuration["Auth0:ApiIdentifier"],
Authority = $"https://{Configuration["Auth0:Domain"]}/"
};
app.UseJwtBearerAuthentication(options);
...
}

Angular2 ASP.NET Core AntiForgeryToken

I have an Angular2 app. It is running within ASP.NET 5 (Core).
It makes Http calls to the controller which is working fine.
But now I need to establish Cross Site Scripting projection.
How do I generate a new token on each Http request and then subsequently perform the AntiForgeryToken check in Angular2 apps?
Note: My data forms in Angular are not produced from an MVC view but entirely written in Angular2 and call web services only.
All the examples I have seen are out dated and do not work / do not work fully.
How do I integrate AntiForgeryToken checks in Angular2 against ASP.NET 5 where forms are pure Angular?
Thanks.
A custom action filter is not necessary. It can all be wired up in Startup.cs.
using Microsoft.AspNetCore.Antiforgery;
(...)
public void ConfigureServices(IServiceCollection services)
{
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
(...)
}
public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
app.Use(next => context =>
{
if (context.Request.Path == "/")
{
//send the request token as a JavaScript-readable cookie, and Angular will use it by default
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions { HttpOnly = false });
}
return next(context);
});
(...)
}
Then all you need in your controllers is the [ValidateAntiForgeryToken] decorator wherever you want to enforce that a token is provided.
For reference, I found this solution here - AspNet AntiForgery Github Issue 29.
I am using a action filter to send the request tokens.
Simply apply it to the actions you want a new antiforgery token, e.g. Angular2 SPA, WebAPI action, etc.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class AngularAntiForgeryTokenAttribute : ActionFilterAttribute
{
private const string CookieName = "XSRF-TOKEN";
private readonly IAntiforgery antiforgery;
public AngularAntiForgeryTokenAttribute(IAntiforgery antiforgery)
{
this.antiforgery = antiforgery;
}
public override void OnResultExecuting(ResultExecutingContext context)
{
base.OnResultExecuting(context);
if (!context.Cancel)
{
var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append(
CookieName,
tokens.RequestToken,
new CookieOptions { HttpOnly = false });
}
}
}
/* HomeController */
[ServiceFilter(typeof(AngularAntiForgeryTokenAttribute), IsReusable = true)]
public IActionResult Index()
{
return View();
}
/* AccountController */
[HttpPost()]
[AllowAnonymous]
[ValidateAntiForgeryToken]
// Send new antiforgery token
[ServiceFilter(typeof(AngularAntiForgeryTokenAttribute), IsReusable = true)]
public async Task<IActionResult> Register([FromBody] RegisterViewModel model)
{
//...
return Json(new { });
}
Register the attribute in Startup, and configure Antiforgery service to read the request token form "X-XSRF-TOKEN" header.
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddScoped<AngularAntiForgeryTokenAttribute>();
services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
});
}
}
I think you need to make custom AntiForgeryValidationToken attribute that supports sending token via header instead of form values. Then add token to header of every request from your Angular2 app to your api. Example here How do you set global custom headers in Angular2?
To validate the token from a header you can use something like this:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class ValidateHeaderAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException(nameof(filterContext));
}
var httpContext = filterContext.HttpContext;
if (httpContext.Request.Headers["__RequestVerificationToken"] == null)
{
httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
httpContext.Response.StatusDescription = "RequestVerificationToken missing.";
filterContext.Result = new JsonResult
{
Data = new { ErrorMessage = httpContext.Response.StatusDescription },
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
return;
}
var cookie = httpContext.Request.Cookies[System.Web.Helpers.AntiForgeryConfig.CookieName];
System.Web.Helpers.AntiForgery.Validate(cookie != null ? cookie.Value : null, httpContext.Request.Headers["__RequestVerificationToken"]);
}
}
Then you just add [ValidateHeaderAntiForgeryToken] on the methods in your controller. Note though, this is from a MVC 5, ASP.NET 4.5.2 project, so you may have to alter it slightly to adjust to .NET Core. Also I modified this to return a JSON result if the token is missing, you can remove that part if you don't handle the error response and output it to the user.
Credits for the core part of this attribute goes to: https://nozzlegear.com/blog/send-and-validate-an-asp-net-antiforgerytoken-as-a-request-header
The hard part is how to generate the AntiForgeryToken without using #Html.AntiForgeryToken() in pure Angular 2 application (without access to .cshtml files). I'm looking for an answer to that as well.