Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,29 @@

namespace PrincipalExample.Controllers;

public class AccountController(IHttpContextAccessor httpContextAccessor, ILogger<AccountController> logger) : Controller
public class AccountController : Controller
{
public async Task<IActionResult> Login()
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(string? name)
{
// Simulate login (hardcoding a username and role for simplicity)
var claims = new List<Claim>
if (!string.IsNullOrWhiteSpace(name))
{
new(ClaimTypes.Name, "testuser"),
new(ClaimTypes.Role, "Admin")
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
var identity = new ClaimsIdentity([new Claim(ClaimTypes.Name, name.Trim())], CookieAuthenticationDefaults.AuthenticationScheme);

// Thread.CurrentPrincipal is AsyncLocal-backed, so it rides on ExecutionContext
Thread.CurrentPrincipal = principal;

LogAmbientState("Before await");

// Sign in the user
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal).ConfigureAwait(ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.None);

LogAmbientState("After await");

Task suppressedFlowTask;
using (ExecutionContext.SuppressFlow())
{
suppressedFlowTask = Task.Run(() => LogAmbientState("Inside Task.Run with ExecutionContext suppressed"));
// Writes the sign-in cookie. HttpContext.User is assigned from that cookie on the next request.
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
}

await suppressedFlowTask;

return RedirectToAction("Index", "Home");

void LogAmbientState(string stage) => logger.LogInformation(
"{Stage} | Thread {ThreadId} | Thread.CurrentPrincipal: {CurrentPrincipal} | IHttpContextAccessor.HttpContext: {AccessorHttpContext} | Controller.HttpContext: {ControllerHttpContext} | principal local: {PrincipalLocal}",
stage,
Environment.CurrentManagedThreadId,
Thread.CurrentPrincipal?.Identity?.Name ?? "<null>",
httpContextAccessor.HttpContext is null ? "<null>" : "available",
HttpContext is null ? "<null>" : "available",
principal.Identity?.Name);
return RedirectToAction(nameof(HomeController.Index), "Home");
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
var user = User;
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

// Sign out the user
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme).ConfigureAwait(ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.None);
return RedirectToAction("Index", "Home");
return RedirectToAction(nameof(HomeController.Index), "Home");
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PrincipalExample.Models;

namespace PrincipalExample.Controllers;

public class HomeController : Controller
public class HomeController(IHttpContextAccessor httpContextAccessor) : Controller
{
public IActionResult Index()
public IActionResult Index() => View();

[Authorize]
public async Task<IActionResult> RunExperiment()
{
// Access the current user's identity
var username = User.Identity?.Name; // "testuser"
var isAuthenticated = User.Identity?.IsAuthenticated; // true
var userRoles = User.IsInRole("Admin"); // true
var checkpoints = new List<Checkpoint>();
var signedInUser = HttpContext.User;

// Get all claims of the user
var claims = User.Claims.Select(c => new { c.Type, c.Value });
checkpoints.Add(Observe("1. Start of the action"));

// Use the principal (User) for authorization checks
if (User.IsInRole("Admin"))
{
ViewData["Message"] = "Welcome, Admin!";
}
else
Thread.CurrentPrincipal = signedInUser;

// Yields the current thread: the rest of this method runs later as a continuation on the thread pool
await Task.Yield();

checkpoints.Add(Observe("2. After await Task.Yield()"));

checkpoints.Add(await Task.Run(() => Observe("3. Inside Task.Run(...)")));

Task<Checkpoint> suppressedFlowTask;
using (ExecutionContext.SuppressFlow())
{
ViewData["Message"] = "Welcome, User!";
suppressedFlowTask = Task.Run(() => Observe("4. Inside Task.Run(...) started while ExecutionContext flow is suppressed"));
}

return View();
checkpoints.Add(await suppressedFlowTask);

return View(nameof(Index), checkpoints);

// Asks for the signed-in user's name four different ways, from whichever thread is running this code
Checkpoint Observe(string checkpoint) => new Checkpoint(
checkpoint,
Environment.CurrentManagedThreadId,
Thread.CurrentPrincipal?.Identity?.Name,
httpContextAccessor.HttpContext?.User.Identity?.Name,
HttpContext.User.Identity?.Name,
signedInUser.Identity?.Name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace PrincipalExample.Models;

public record Checkpoint(
string Name,
int ThreadId,
string? ThreadCurrentPrincipal,
string? HttpContextAccessorUser,
string? ControllerHttpContextUser,
string? SignedInUserVariable);

This file was deleted.

37 changes: 3 additions & 34 deletions 3. .NET Internals/3. Principal/PrincipalExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,17 @@

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddHttpContextAccessor();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
});
.AddCookie(static options => options.LoginPath = "/");

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
app.UseHttpsRedirection();
}

app.UseRouting();

// Reads the sign-in cookie on every request and assigns the signed-in user to HttpContext.User
app.UseAuthentication();
app.UseAuthorization();

app.MapStaticAssets();

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.WithStaticAssets();

app.MapControllerRoute(
name: "login",
pattern: "{controller=Account}/{action=Login}/{id?}")
.WithStaticAssets();

app.MapControllerRoute(
name: "logout",
pattern: "{controller=Account}/{action=Logout}/{id?}")
.WithStaticAssets();

app.MapDefaultControllerRoute();

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"profiles": {
"PrincipalExample": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,111 @@
@{
ViewData["Title"] = "Home Page";
@model IReadOnlyList<Checkpoint>?
@{
var isSignedIn = User.Identity?.IsAuthenticated is true;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Principal Challenge</title>
<style>
body { margin: 0; padding: 32px 16px; font-family: "Segoe UI", system-ui, sans-serif; background: #f4f6f8; color: #1d2328; }
main { max-width: 1200px; margin: 0 auto; }
h1 { margin: 0 0 4px; }
h2 { margin: 0 0 12px; font-size: 1.1rem; }
p { margin: 0 0 12px; line-height: 1.5; }
code { font-family: Consolas, Menlo, monospace; font-size: 0.9em; }
section { margin-top: 20px; padding: 20px; border: 1px solid #d5dbe0; border-radius: 8px; background: #fff; }
.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 0 20px; }
input { padding: 8px; font: inherit; }
button, .button { display: inline-block; padding: 8px 16px; border: 0; border-radius: 6px; background: #5c2d91; color: #fff; font: inherit; text-decoration: none; cursor: pointer; }
button.secondary { background: #6b7780; }
.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; border-bottom: 1px solid #e3e7ea; text-align: left; vertical-align: top; }
th small { display: block; font-weight: normal; color: #6b7780; }
.name { padding: 2px 8px; border-radius: 12px; background: #dcf2e3; color: #11602f; font-weight: 600; }
.null { padding: 2px 8px; border-radius: 12px; background: #fbe1e1; color: #9b1c1c; font-family: Consolas, Menlo, monospace; }
</style>
</head>
<body>
<main>
<h1>Who is the signed-in user?</h1>
<p>3. .NET Internals &middot; Principal Challenge</p>

<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
<div class="steps">
<section>
<h2>Step 1: Sign in</h2>
@if (isSignedIn)
{
<p>You are signed in as <span class="name">@User.Identity?.Name</span></p>
<form method="post" asp-controller="Account" asp-action="Logout">
<button type="submit" class="secondary">Sign out</button>
</form>
}
else
{
<p>There is no password. The app stores the name you type in a sign-in cookie.</p>
<form method="post" asp-controller="Account" asp-action="Login">
<input name="name" placeholder="Your first name" maxlength="40" required />
<button type="submit">Sign in</button>
</form>
}
</section>

<section>
<h2>Step 2: Run the experiment</h2>
@if (!isSignedIn)
{
<p>Sign in first.</p>
}
else if (Model is null)
{
<p><code>HomeController.RunExperiment()</code> asks for your name four different ways at four checkpoints. Write down your predictions before you click.</p>
<a class="button" asp-controller="Home" asp-action="RunExperiment">Run the experiment</a>
}
else
{
<p>Compare the results below with your predictions, then select <strong>Run it again</strong>.</p>
<a class="button" asp-controller="Home" asp-action="RunExperiment">Run it again</a>
}
</section>
</div>

@if (Model is not null)
{
<section>
<h2>Results</h2>
<p>Each cell shows the user name that expression returned at that checkpoint, or <span class="null">null</span>.</p>
<div class="table-scroll">
<table>
<thead>
<tr>
<th>Checkpoint</th>
<th>Thread</th>
<th><code>Thread.<wbr />CurrentPrincipal</code><small>static property</small></th>
<th><code>httpContextAccessor.<wbr />HttpContext?.<wbr />User</code><small>injected IHttpContextAccessor</small></th>
<th><code>HttpContext.<wbr />User</code><small>controller property</small></th>
<th><code>signedInUser</code><small>local variable</small></th>
</tr>
</thead>
<tbody>
@foreach (var checkpoint in Model)
{
<tr>
<td>@checkpoint.Name</td>
<td>@checkpoint.ThreadId</td>
<td><span class="@(checkpoint.ThreadCurrentPrincipal is null ? "null" : "name")">@(checkpoint.ThreadCurrentPrincipal ?? "null")</span></td>
<td><span class="@(checkpoint.HttpContextAccessorUser is null ? "null" : "name")">@(checkpoint.HttpContextAccessorUser ?? "null")</span></td>
<td><span class="@(checkpoint.ControllerHttpContextUser is null ? "null" : "name")">@(checkpoint.ControllerHttpContextUser ?? "null")</span></td>
<td><span class="@(checkpoint.SignedInUserVariable is null ? "null" : "name")">@(checkpoint.SignedInUserVariable ?? "null")</span></td>
</tr>
}
</tbody>
</table>
</div>
</section>
}
</main>
</body>
</html>

This file was deleted.

This file was deleted.

Loading
Loading