ASP.NET Core 6.0 - Terms Of Service
This article describes the Terms Of Service AppUserClaim, Terms Of Service page, and TermsOfServiceMiddleware. I will assume you have downloaded the ASP.NET Core 6.0 - Users With Device 2FA Project.
Users With Device 2FA Project and Article Series
This article series is about the implementation of ASP.NET Core 6.0 with Visual Studio 2022. The ASP.NET Core 6.0 - Users With Device 2FA Project (UWD2FAP) implements WebAuthn, also known as FIDO2, instead of authenticator apps for two-factor authentication (2FA). The project implements Bootstrap v5 and Bootstrap Native. After a user registers, they can enable 2FA with Windows Hello, Android Lock Screen, or a FIDO2 security key. If I had this project when I created KenHaggerty. Com three years ago, I would have started with this user authentication template. The latest version is published at Preview. KenHaggerty. Com. I encourage you to register and evaluate multiple 2FA devices in Manage Account > Two-Factor Authentication. Details, screenshots, and related articles can be found at ASP.NET Core 6.0 - Users With Device 2FA Project. The details page includes the version change log.
- ASP.NET Core 6.0 - Users With Device 2FA
- ASP.NET Core 6.0 - Migrate From .NET 5.0
- ASP.NET Core 6.0 - Top-Level Statements
- ASP.NET Core 6.0 - Global Usings
- ASP.NET Core 6.0 - Data Protection Keys
- ASP.NET Core 6.0 - Administrator Claim
- ASP.NET Core 6.0 - Terms Of Service
- ASP.NET Core 6.0 - Claims-Based Authorization
- ASP.NET Core 6.0 - Impersonate User
- ASP.NET Core 6.0 - Remember Me Or Not
- ASP.NET Core 6.0 - Zero Trust
Updated with links as the articles become published.
The UWD2FAP implements an optional Terms Of Service feature with a Terms Of Service AppUserClaim. When the AppSettings. RequireTOSAgreement is set true, the TermsOfServiceMiddleware redirects authenticated users to TermsOfService page when the TermsOfService claim is not found or less than the AppSettings. TermsOfServiceDate.
AppSettings.cs
/// <summary> /// Redirects to TermsOfService page when TermsOfService claim is not found or less than the TermsOfServiceDate. /// </summary> public static bool RequireTOSAgreement { get; } = true; /// <summary> /// Used with TermsOfServiceMiddleware for Terms Of Service updates. /// </summary> /// <remarks> /// If set to null, UtcNow is used. Set a Date to override UtcNow. Like DateTimeOffset.Parse("12/4/2021"). /// Must be less than the DateOnlyFormat for DateTimeOffset.UtcNow. /// </remarks> public static DateTime TermsOfServiceDate { get; } = DateTime.Parse("11/14/2021");
Program.cs
if (AppSettings.RequireTOSAgreement) app.UseTermsOfService();
TermsOfServiceMiddleware.cs
public class TermsOfServiceMiddleware { private readonly RequestDelegate _next; public TermsOfServiceMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { if (context.User.Identity!.IsAuthenticated && context.Request.Path != new PathString("/account/termsofservice") && context.Request.Path != new PathString("/account/logout") && context.Request.Path != new PathString("/privacy") && (!((ClaimsIdentity)context.User.Identity).HasClaim(c => c.Type == ApplicationClaimType.TermsOfService) || ((ClaimsIdentity)context.User.Identity).HasClaim(c => c.Type == ApplicationClaimType.TermsOfService && DateTime.Parse(c.Value) < AppSettings.TermsOfServiceDate))) { var returnUrl = context.Request.Path.Value == "/" ? "" : "?returnUrl=" + HttpUtility.UrlEncode(context.Request.Path.Value); context.Response.Redirect("/account/termsofservice" + returnUrl); } await _next(context).ConfigureAwait(true); } }
The Terms Of Service page displays an Agree button when the user is authenticated and does not have a valid Terms Of Service AppUserClaim.
TermsOfService.cshtml.cs
public IActionResult OnGet(string? returnUrl = null) { ReturnUrl = returnUrl; if (!Url.IsLocalUrl(ReturnUrl)) ReturnUrl = Url.Content(AppSettings.DefaultLoginReturnUrl); if (_signInService.IsSignedIn(User) && (!User.HasClaim(c => c.Type == ApplicationClaimType.TermsOfService) || (User.HasClaim(c => c.Type == ApplicationClaimType.TermsOfService && DateTime.Parse(c.Value) < AppSettings.TermsOfServiceDate)))) ShowAgreeButton = true; return Page(); }
The authenticated user must click the Agree button or log out before they can browse any page other than the Privacy page. The Agree button posts to the Terms Of Service page which sets the Terms Of Service AppUserClaim.
TermsOfService.cshtml.cs
public async Task<IActionResult> OnPostAsync(string? returnUrl = null) { ReturnUrl = returnUrl; if (!Url.IsLocalUrl(ReturnUrl)) ReturnUrl = Url.Content(AppSettings.DefaultLoginReturnUrl); if (!User.Identity!.IsAuthenticated) return NotFound(); var appUserId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)); var appUser = await _userService.GetAppUserByIdAsync(appUserId).ConfigureAwait(false); if (appUser == null) return NotFound(); if (!await _userService.SetTermsOfServiceClaimByAppUserIdAsync(appUserId).ConfigureAwait(false)) throw new InvalidOperationException("An error occurred updating a TermsOfService appUserClaim."); await _signInService.RefreshSignInAsync(appUser).ConfigureAwait(false); return LocalRedirect(ReturnUrl); }
UserService.cs
public async Task<bool> SetTermsOfServiceClaimByAppUserIdAsync(int appUserId) { string tosDateString = string.Format(AppSettings.DateOnlyFormat, DateTimeOffset.UtcNow); var tosClaim = await _dbAppUserClaimSet .Where(c => c.AppUserId == appUserId && c.Type == ApplicationClaimType.TermsOfService) .FirstOrDefaultAsync() .ConfigureAwait(false); if (tosClaim == null) return await AddAppUserClaimAsync( new AppUserClaim(appUserId, ApplicationClaimType.TermsOfService, tosDateString)) .ConfigureAwait(false); if (tosClaim != null && DateTime.Parse(tosClaim.Value) < AppSettings.TermsOfServiceDate) { tosClaim.Value = tosDateString; return await UpdateAppUserClaimAsync(tosClaim).ConfigureAwait(false); } return true; }
Comments(0)