Skip to content

Commit

Permalink
876066: Sample Added
Browse files Browse the repository at this point in the history
  • Loading branch information
SumathiSumi committed Jun 14, 2024
1 parent 600f0c0 commit 09c04c9
Show file tree
Hide file tree
Showing 74 changed files with 4,256 additions and 0 deletions.
25 changes: 25 additions & 0 deletions KB-Samples/AccountsSample/AccountsSample.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccountsSample", "AccountsSample\AccountsSample.csproj", "{C8AB52E0-8D41-4522-A904-E14A380C951B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C8AB52E0-8D41-4522-A904-E14A380C951B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8AB52E0-8D41-4522-A904-E14A380C951B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8AB52E0-8D41-4522-A904-E14A380C951B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8AB52E0-8D41-4522-A904-E14A380C951B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {212809DF-8C27-457D-9D0E-78DA63C175B9}
EndGlobalSection
EndGlobal
18 changes: 18 additions & 0 deletions KB-Samples/AccountsSample/AccountsSample/AccountsSample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-AccountsSample-a5f6aec6-d8e1-43ae-b0ef-e508c331d16c</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
<PackageReference Include="Syncfusion.Blazor" Version="*" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using AccountsSample.Components.Account.Pages;
using AccountsSample.Components.Account.Pages.Manage;
using AccountsSample.Data;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using System.Security.Claims;
using System.Text.Json;

namespace Microsoft.AspNetCore.Routing
{
internal static class IdentityComponentsEndpointRouteBuilderExtensions
{
// These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project.
public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);

var accountGroup = endpoints.MapGroup("/Account");

accountGroup.MapPost("/PerformExternalLogin", (
HttpContext context,
[FromServices] SignInManager<ApplicationUser> signInManager,
[FromForm] string provider,
[FromForm] string returnUrl) =>
{
IEnumerable<KeyValuePair<string, StringValues>> query = [
new("ReturnUrl", returnUrl),
new("Action", ExternalLogin.LoginCallbackAction)];

var redirectUrl = UriHelper.BuildRelative(
context.Request.PathBase,
"/Account/ExternalLogin",
QueryString.Create(query));

var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return TypedResults.Challenge(properties, [provider]);
});

accountGroup.MapPost("/Logout", async (
ClaimsPrincipal user,
SignInManager<ApplicationUser> signInManager,
[FromForm] string returnUrl) =>
{
await signInManager.SignOutAsync();
return TypedResults.LocalRedirect($"~/{returnUrl}");
});

var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization();

manageGroup.MapPost("/LinkExternalLogin", async (
HttpContext context,
[FromServices] SignInManager<ApplicationUser> signInManager,
[FromForm] string provider) =>
{
// Clear the existing external cookie to ensure a clean login process
await context.SignOutAsync(IdentityConstants.ExternalScheme);

var redirectUrl = UriHelper.BuildRelative(
context.Request.PathBase,
"/Account/Manage/ExternalLogins",
QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction));

var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, signInManager.UserManager.GetUserId(context.User));
return TypedResults.Challenge(properties, [provider]);
});

var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
var downloadLogger = loggerFactory.CreateLogger("DownloadPersonalData");

manageGroup.MapPost("/DownloadPersonalData", async (
HttpContext context,
[FromServices] UserManager<ApplicationUser> userManager,
[FromServices] AuthenticationStateProvider authenticationStateProvider) =>
{
var user = await userManager.GetUserAsync(context.User);
if (user is null)
{
return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'.");
}

var userId = await userManager.GetUserIdAsync(user);
downloadLogger.LogInformation("User with ID '{UserId}' asked for their personal data.", userId);

// Only include personal data for download
var personalData = new Dictionary<string, string>();
var personalDataProps = typeof(ApplicationUser).GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
foreach (var p in personalDataProps)
{
personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
}

var logins = await userManager.GetLoginsAsync(user);
foreach (var l in logins)
{
personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey);
}

personalData.Add("Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user))!);
var fileBytes = JsonSerializer.SerializeToUtf8Bytes(personalData);

context.Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json");
return TypedResults.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json");
});

return accountGroup;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using AccountsSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;

namespace AccountsSample.Components.Account
{
// Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation.
internal sealed class IdentityNoOpEmailSender : IEmailSender<ApplicationUser>
{
private readonly IEmailSender emailSender = new NoOpEmailSender();

public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");

public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");

public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Microsoft.AspNetCore.Components;
using System.Diagnostics.CodeAnalysis;

namespace AccountsSample.Components.Account
{
internal sealed class IdentityRedirectManager(NavigationManager navigationManager)
{
public const string StatusCookieName = "Identity.StatusMessage";

private static readonly CookieBuilder StatusCookieBuilder = new()
{
SameSite = SameSiteMode.Strict,
HttpOnly = true,
IsEssential = true,
MaxAge = TimeSpan.FromSeconds(5),
};

[DoesNotReturn]
public void RedirectTo(string? uri)
{
uri ??= "";

// Prevent open redirects.
if (!Uri.IsWellFormedUriString(uri, UriKind.Relative))
{
uri = navigationManager.ToBaseRelativePath(uri);
}

// During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect.
// So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown.
navigationManager.NavigateTo(uri);
throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering.");
}

[DoesNotReturn]
public void RedirectTo(string uri, Dictionary<string, object?> queryParameters)
{
var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path);
var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters);
RedirectTo(newUri);
}

[DoesNotReturn]
public void RedirectToWithStatus(string uri, string message, HttpContext context)
{
context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context));
RedirectTo(uri);
}

private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path);

[DoesNotReturn]
public void RedirectToCurrentPage() => RedirectTo(CurrentPath);

[DoesNotReturn]
public void RedirectToCurrentPageWithStatus(string message, HttpContext context)
=> RedirectToWithStatus(CurrentPath, message, context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using AccountsSample.Data;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Security.Claims;

namespace AccountsSample.Components.Account
{
// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user
// every 30 minutes an interactive circuit is connected.
internal sealed class IdentityRevalidatingAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> options)
: RevalidatingServerAuthenticationStateProvider(loggerFactory)
{
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);

protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
await using var scope = scopeFactory.CreateAsyncScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}

private async Task<bool> ValidateSecurityStampAsync(UserManager<ApplicationUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if (user is null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using AccountsSample.Data;
using Microsoft.AspNetCore.Identity;

namespace AccountsSample.Components.Account
{
internal sealed class IdentityUserAccessor(UserManager<ApplicationUser> userManager, IdentityRedirectManager redirectManager)
{
public async Task<ApplicationUser> GetRequiredUserAsync(HttpContext context)
{
var user = await userManager.GetUserAsync(context.User);

if (user is null)
{
redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context);
}

return user;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@page "/Account/ConfirmEmail"

@using System.Text
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using AccountsSample.Data

@inject UserManager<ApplicationUser> UserManager
@inject IdentityRedirectManager RedirectManager

<PageTitle>Confirm email</PageTitle>

<h1>Confirm email</h1>
<StatusMessage Message="@statusMessage" />

@code {
private string? statusMessage;

[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;

[SupplyParameterFromQuery]
private string? UserId { get; set; }

[SupplyParameterFromQuery]
private string? Code { get; set; }

protected override async Task OnInitializedAsync()
{
if (UserId is null || Code is null)
{
RedirectManager.RedirectTo("");
}

var user = await UserManager.FindByIdAsync(UserId);
if (user is null)
{
HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
statusMessage = $"Error loading user with ID {UserId}";
}
else
{
var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code));
var result = await UserManager.ConfirmEmailAsync(user, code);
statusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
}
}
}
Loading

0 comments on commit 09c04c9

Please sign in to comment.