How to set consent cookie in Blazor Server - c#

I have a Blazor 3.1 App with Identity where I want to implement a cookie consent banner.
In classic ASP .NET Core, there is a nice template for a cookie consent banner.
#using Microsoft.AspNetCore.Http.Features
#{
var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
var showBanner = !consentFeature?.CanTrack ?? false;
var cookieString = consentFeature?.CreateConsentCookie();
}
#if (showBanner)
{
<div class="container">
<div id="cookieConsent" class="alert alert-info alert-dismissible fade show" role="alert">
Use this space to summarize your privacy and cookie use policy. <a class="alert-link" asp-area="" asp-controller="Home" asp-action="Privacy">Learn More</a>.
<button type="button" class="accept-policy close" data-dismiss="alert" aria-label="Close" data-cookie-string="#cookieString">
<span aria-hidden="true">Accept</span>
</button>
</div>
</div>
<script>
(function () {
var button = document.querySelector("#cookieConsent button[data-cookie-string]");
button.addEventListener("click", function (event) {
document.cookie = button.dataset.cookieString;
}, false);
})();
</script>
}
If you are placing this partial in the layout, you have a perfect working cookie consent banner. If configured appropriately, users cant even login to the Identity Framework until they consent.
Because Blazor does not know a HttpContext, this template is not working. Is there a boilerplate way or do I have to create this feature this by myself?

I just solved this in a Blazor 3.1 app yesterday! I opted to use JS Interop and it was super easy.
My web app has multiple front ends, including MVC, Razor Pages and Blazor, so you can open the main solution file and then check out the Blazor project:
https://github.com/shahedc/NetLearnerApp
Things to note in the Blazor project;
I implemented the partial view as a razor component
look for /_CookieConsentPartial.razor in the Shared folder
MainLayout.razor uses this razor component
The _Host.cshtml file includes my js interop file
Look for netLearnerJsInterop.js in wwwroot
the js Interop file contains the document.cookie usage
the razor component uses JSRuntime.InvokeVoidAsync to call the JS method to accept the GDPR consent message and store the cookie

To set cookies on the client, you still need Javascript Interop.
See this answer: How do I create a cookie client side using blazor
Alternatively, you could store the information you need in the Local Storage using this library, without having to write any Javascript:
https://github.com/Blazored/LocalStorage

You can use this GDPR Consent component from Nuget.
However it is only helping you to save work on UI side. User chosen Cookie settings is stored in Local storage which can be read out any time. But it does not restricts any Cookies or Tracking scripts automatically. That must be done by the app developer!
See it in action on demo page.

Microsoft has released this article explaining a relatively simple way to implement a GDPR compliant using the CookiePolicyOptions Class and CookiePolicyAppBuilderExtensions.UseCookiePolicy Method. There appears to be dynamic validation to determine if consent is required.
builder.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; });
Post: EU General Data Protection Regulation (GDPR) support in ASP.NET Core

Related

.NET Core 5 MVC Pagination With Custom Filter

I'm in the middle of development for my third year college project, for this project I decided to use .NET Core 5 MVC which I’m not really familiar with.
I usually use razor pages but for an important project like that I wanted to use the industry standard.
Anyway, I created most of the stuff for the project but when it came to pagination I found a problem.
This is the code for the controller action, where I used a class to get the parameters from the query link (added a lot of filters that's why I used a class).
[Route("/Catalogue")]
public async Task<IActionResult> AllProducts([FromQuery] ProductParameters parameters,int pagenum=1)
{
parameters.PageNumber = pagenum;
ViewData["BrandId"] = new SelectList(await brandsrepo.GetBrandsList(), "Id", "Name");
ViewData["CategoryId"] = new SelectList(await categoryrepo.GetCategoryList(), "Id", "Name");
var viewmodel = new AllProductsViewModel { Products = await productrepo.GetProductsList(parameters), Parameters = new() };
return View(viewmodel);
}
I tried to use what I know from razor pages where I use asp-route-{param_name}="{value}".
But when I search and try to go to the next page all the query parameters (filters and search terms etc ) are gone and it goes only to the second page.
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center">
<li class="page-item #(Model.Products.HasPrevious?"":"disabled")">
<a class="page-link" asp-route-pagenum="#(Model.Products.PageNumber-1)">« Previous</a>
</li>
<li class="page-item #(Model.Products.HasNext?"":"disabled")">
<a class="page-link" asp-route-pagenum="#(Model.Products.PageNumber+1)">Next »</a>
</li>
</ul>
</nav>
I hope I can find a way to do the pagination as well as keep the parameters that were already there.
I have a last resort that is using razor components which I have used a lot in this project for reusable components.
There will be pretty much this similar kind of problem will rise again in the process of your project.
Therefore I think the overall idea that should be enlight here is "how to keep parameters across request in asp.net MVC".
And the answer is the basic of Http Request.
You are likely have 2 obvious options is to store the parameter and anything that should survive multiple request on the server:
By using the cache and session ID.
By sending the parameters along with each Http Request.
And I highly recommend the second approach due to it's flexibility and simplicity.
The idea was when the server receive Http Request, pass it back to the client through ViewData or ViewBag, and then let the client attach them for the next request as you can easily implement them in the view layer.
In case you are not yet familiar with these things, i suggest this as your starting point.
And now, no need to third party dependency, completely control of how you paging the result and counting all result to better describe your paging list, which even more easy if you already using EF Core.

How do you sign out a user, without forcing the user to press a button in Blazor?

I have this very annoying issue in Blazor.
I'm using Azure AD B2C to authenticate users in my web app.
I have a scenario, where I want to check if the user is authenticated, and if yes, the user should be signed out.
It seems like sign out requests need to be peformed within the context inside an . Otherwise the user will be able to sign in again without using credentials.
If I use the following everything works:
<AuthorizeView>
<Authorized>
<Button #onclick="BeginSignout"></Button>
</Authorized>
<NotAuthorized>
....
</NotAuthorized>
Notice the button, that the user is forced to click on. No problem at all.
However if I do like this:
<AuthorizeView>
<Authorized>
<Signout/>
</Authorized>
<NotAuthorized>
....
</NotAuthorized>
Which executes the following code inside the Signout component, it DOESN'T WORK and the user is still signed in:
protected async override Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await BeginSignout();
}
There's simply not enough documentation on this issue, and seems like my use-case is very uncommon apperently and therefore I haven't been able to figure this out.
I've also tried with an invisible DIV that is supposed to execute "BeginSignout" when loaded, but the #onload event doesn't work.
BeginSignout is calling the following code:
public async Task SignOut(string? redirectUri = null)
{
if(redirectUri == null)
{
redirectUri = Uri.EscapeDataString(this.navigationManager.Uri);
}
await this.signOutManager.SetSignOutState();
this.navigationManager.NavigateTo($"authentication/logout?returnUrl={redirectUri}");
}
I'm lost, and this should've been a simple task that is just taking all of my time.
I don't know what's inside your BeginSignout method (please add your method code to the question) but it's better to add this kind of signout to the AfterRender method.
If your authentication mode is based on cookie you need to call a cshtml method because wasm pages cannot set cookies.

Question about AccountController and _LoginPartial in sample project

I am currently familiarizing myself with both the MVC framework and Azure B2C logins / credentials. The goal is to implement some of the logic provided in the sample project to an existing project. Before getting into details about my troubles with my own project, I was not able to figure out the following:
The sample app provided (cf. https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi) contains a class AuthController.cs, which provides logic for Signing In / Out, etc. The methods in the AuthController appear to be called when using a partial view _LoginPartial, e.g.
#if (Request.IsAuthenticated)
else
{
<ul class="nav navbar-nav navbar-right">
<li>#Html.ActionLink("Sign up / Sign in", "SignUpSignIn", "Account", routeValues: null, htmlAttributes: new { id = "signUpSignInLink" })</li>
</ul>
}
The sign-in-method in the AccountController takes an argument string redirectUrl, e.g
public void SignUpSignIn(string redirectUrl)
{
redirectUrl = redirectUrl ?? "/";
// Use the default policy to process the sign up / sign in flow
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = redirectUrl });
return;
}
Now, the controller does not seem to be accessible when Debugging, thus I cannot figure out from where the parameter redirectUrl is passed. The reason behind this is that I would like to implement the SignUpSignIn method in an already existing controller class.
What is the relationship between the partial class and the controller and from where do I get this redirectUrl. I am sorry if my question seems trivial, but I am currently making a transition from classic ASP.NET to MVC.
You need to configure the main domain name where you're hosting your app as a reply URL in the AAD registration for your app and pass it as the redirect_uri when redirecting to AAD to allow the user to sign in.
AAD will only redirect the access token to URLs that are specified as Reply URLs in the app registration in AAD.
So in this case, redirectUrl has been set to "https://localhost:44316/". The controller gets the redirectUrl value from the web.config file.
It must match with the Reply URL configured in Azure AD.

How to add LinkedIn to SignInManager.GetExternalAuthenticationSchemes()

How do I make a custom authentication provider like LinkedIn appear in SignInManager.GetExternalAuthenticationSchemes() from where Login.cshtml picks up by default
Background Details:
I am trying to understand asp.net core identity framework. In that quest, I created a standard .net core project
I tried out the supported Google authentication alongside reading the documentation and it all worked fine for me.
I was able to make LinkedIn authentication work for me, but couldn't understand how to make certain pieces work. To add support for LinkedIn authentication, I made the following changes
Added the below lines In Startup.Configure method
app.UseOAuthAuthentication(new OAuthOptions() {
AuthenticationScheme = "LinkedIn",
ClientId = Configuration["Authentication:LinkedIn:ClientID"],
ClientSecret = Configuration["Authentication:LinkedIn:ClientSecret"],
CallbackPath = new PathString("/signin-linkedin"),
AuthorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization",
TokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken",
UserInformationEndpoint = "https://api.linkedin.com/v1/people/~:(id,formatted-name,email-address,picture-url)",
Scope = { "r_basicprofile", "r_emailaddress" },
});
Added the required ClientId and ClientSecret to the configuration
Added the following line to the Login.cshtml
<button type="submit" class="btn btn-default" name="provider" value="LinkedIn" title="Log in using your LinkedIn account">LinkedIn</button>
All this works fine. Now my question is:
For supported authentication providers, as soon as I call, say, app.UseGoogleAuthentication in Startup.Configure, my call to SignInManager.GetExternalAuthenticationSchemes() in Login.cshtml lists Google as a provider. What do I need to do, so the call to SignInManager.GetExternalAuthenticationSchemes() will also list LinkedIn as a provider
What do I need to do, so the call to SignInManager.GetExternalAuthenticationSchemes() will also list LinkedIn as a provider
This method only lists the authentication middleware that have been assigned a "display name".
To include Linked in the providers list, set OAuthOptions.DisplayName:
app.UseOAuthAuthentication(new OAuthOptions
{
DisplayName = "LinkedIn"
// ...
});

ASP.net MVC cross application POST request

I have several ASP.net MVC applications deployed on a single site in IIS. All of the applications are using forms authentication, and all of the applications are configured to use the same machine key.
One of the applications is a 'base site' which provides navigation to the other applications and is where login/logout functionality is being handled. As it stands, a user can log in on the base site and visit the other applications and they will still be authenticated, which is working as intended.
I have a logout form in the header of my shared layout views which submits a post request to the logout action in a controller belonging to the base site. When I submit this form from the base site, the logout works as expected. But if I try to submit the form from any of the other sites, I receive the error message:
"The anti-forgery cookie token and form field token do not match."
This is what my log off action looks like in my Security controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
FormsAuthentication.SignOut();
return Redirect("~/");
}
This is what my form looks like in the base site view:
using (Html.BeginForm("LogOff", "Security", FormMethod.Post, null))
{
#Html.AntiForgeryToken()
<input type="submit" value="Log Off"/>
}
Because of the way the directories are set up the other sites use a slightly different version of the same form to call the logout action from the base site:
using (Html.BeginForm("LogOff", "../Security", FormMethod.Post, null))
{
#Html.AntiForgeryToken()
<input type="submit" value="Log Off"/>
}
The base site is at the root of the directory, and the other applications are contained in their own folder within that root.
None of the views I have tried this on have any conflicting forms or antiforgery tokens, and the machine key among all the apps seems to be configured properly or else I don't think the authentication would be working at all. I am considering just redirecting to the base site and performing the logout action from there, but if there is another more simple solution that I have yet to come across that would be nice.
The AntiForgeryToken works by creating a hidden field and a Cookie with the same token (see this blod post). Since your form is being posted to another url, I think that the cookie is either not being transmitted along with the POST or your browser still has a cookie from a former request to your base site and therefore transmits a wrong one. Does this behaviour also occur after deleting the cookies (to ensure that no old ones are being used)?
What I have done to solve this issue is move the LogOff action into a custom controller class which all of my controllers were already inheriting from.
public abstract class BaseController : Controller
{
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
FormsAuthentication.SignOut();
//Redirect to home page when logging off.
return Redirect("~/");
}
}
And then simply changed my logout form to call the LogOff action from the current controller, rather than specifiying the Security controller in the base site:
using (Html.BeginForm("LogOff", "", FormMethod.Post, null))
{
#Html.AntiForgeryToken()
<input type="submit" value="Log Off"/>
}
I am not sure if there is a way to get my original version to work or not, so for now I think I am going to go with this.

Categories

Resources