Skip to content

feat(auth): load Cloudflare JWT user profile and show welcome name in…#97

Merged
PhilipWoulfe merged 1 commit intomainfrom
feat/#65-user-name
Mar 6, 2026
Merged

feat(auth): load Cloudflare JWT user profile and show welcome name in…#97
PhilipWoulfe merged 1 commit intomainfrom
feat/#65-user-name

Conversation

@PhilipWoulfe
Copy link
Owner

… header/home (#65)

Copilot AI review requested due to automatic review settings March 6, 2026 13:32
@PhilipWoulfe PhilipWoulfe merged commit f77a0d0 into main Mar 6, 2026
8 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements Issue #65 by extracting a user display name (from Cloudflare-provided identity/JWT claims) and surfacing it in the Blazor UI (header + home), with API support for a “current user” endpoint.

Changes:

  • Web: load IUserSession at startup and use it to render “Welcome, {Name}” and personalize the Home page.
  • Web: extend the User model and add a ClaimTypes.Name claim when a display name is available.
  • API: enhance Cloudflare middleware and GetMe endpoint to derive email/name from Cf-Access-Jwt-Assertion when needed.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/F1.Web/Services/CustomAuthenticationStateProvider.cs Adds ClaimTypes.Name based on session name/email for UI identity display.
src/F1.Web/Program.cs Initializes IUserSession during app startup.
src/F1.Web/Pages/Home.razor Personalizes greeting using session-derived display name.
src/F1.Web/Models/User.cs Extends user model to include name/auth/id fields.
src/F1.Web/Layout/MainLayout.razor Updates header to show “Welcome, …” and removes per-layout session init.
src/F1.Api/Middleware/CloudflareAccessMiddleware.cs Falls back to parsing Cf-Access-Jwt-Assertion for email/name claims.
src/F1.Api/Controllers/UsersController.cs Adjusts /me endpoint to read identity from JWT/header and returns UserDto.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +16 to 35
var jwtAssertion = Request.Headers["Cf-Access-Jwt-Assertion"].FirstOrDefault();
var nameFromJwt = GetJwtClaim(jwtAssertion, "name");
var emailFromJwt = GetJwtClaim(jwtAssertion, "email");

if (string.IsNullOrEmpty(email))
var email = emailFromJwt ?? Request.Headers["Cf-Access-Authenticated-User-Email"].FirstOrDefault();
var name = nameFromJwt ?? email?.Split('@')[0];

if (string.IsNullOrWhiteSpace(email) && string.IsNullOrWhiteSpace(name))
{
return Unauthorized();
}

return Ok(new UserDto
{
Name = email.Split('@')[0],
Email = email,
Name = name ?? string.Empty,
Email = email ?? string.Empty,
IsAuthenticated = true,
Id = Request.Headers["Cf-Access-Authenticated-User-Id"].FirstOrDefault() ?? string.Empty
});
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetJwtClaim is duplicated here and in CloudflareAccessMiddleware, and this controller re-parses headers/JWT even though the middleware already populates HttpContext.User with ClaimTypes.Email/ClaimTypes.Name. Consider reading User.FindFirstValue(...) in this endpoint and centralizing JWT claim extraction in one place to avoid divergence and double parsing.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +55
var email = context.Request.Headers["Cf-Access-Authenticated-User-Email"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(email))
{
var jwtAssertion = context.Request.Headers["Cf-Access-Jwt-Assertion"].FirstOrDefault();
email = GetJwtClaim(jwtAssertion, "email");
}

if (string.IsNullOrWhiteSpace(email))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("Unauthorized: Cf-Access-Authenticated-User-Email header is missing.");
await context.Response.WriteAsync("Unauthorized: Cloudflare Access identity headers are missing.");
return;
}

var email = emailValues.ToString();
var claims = new List<Claim>
{
new Claim(ClaimTypes.Email, email)
};

var name = GetJwtClaim(context.Request.Headers["Cf-Access-Jwt-Assertion"].FirstOrDefault(), "name");
if (!string.IsNullOrWhiteSpace(name))
{
claims.Add(new Claim(ClaimTypes.Name, name));
}

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior reads email/name from Cf-Access-Jwt-Assertion, but the existing middleware tests only cover the Cf-Access-Authenticated-User-Email header. Please add/extend tests to cover: (1) email resolved from JWT when the email header is missing, and (2) ClaimTypes.Name being added when the JWT contains a name claim, so regressions in the base64url parsing don’t silently break auth.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +59
using (var scope = host.Services.CreateScope())
{
var userSession = scope.ServiceProvider.GetRequiredService<IUserSession>();
await userSession.InitializeAsync();
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Blazor WebAssembly, creating a new scope here will typically create a different scoped IUserSession instance than the one the app later injects, so the initialization may not actually prime the session used by AuthorizeView/pages. Resolve IUserSession from host.Services directly (or register it as a singleton) instead of using CreateScope() for this startup initialization.

Suggested change
using (var scope = host.Services.CreateScope())
{
var userSession = scope.ServiceProvider.GetRequiredService<IUserSession>();
await userSession.InitializeAsync();
}
var userSession = host.Services.GetRequiredService<IUserSession>();
await userSession.InitializeAsync();

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +14
private string DisplayName =>
!string.IsNullOrWhiteSpace(UserSession.User?.Name)
? UserSession.User!.Name!
: UserSession.User?.Email?.Split('@')[0] ?? "Driver";
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DisplayName can become an empty string when UserSession.User?.Email is present but blank (or doesn’t contain a usable local-part), because the Split('@')[0] result is "" and won’t trigger the ?? "Driver" fallback. Consider using a whitespace check on the computed local-part (or using IndexOf('@') + Substring) and falling back to "Driver" when the result is empty.

Suggested change
private string DisplayName =>
!string.IsNullOrWhiteSpace(UserSession.User?.Name)
? UserSession.User!.Name!
: UserSession.User?.Email?.Split('@')[0] ?? "Driver";
private string DisplayName
{
get
{
if (!string.IsNullOrWhiteSpace(UserSession.User?.Name))
{
return UserSession.User!.Name!;
}
var localPart = UserSession.User?.Email?.Split('@')[0];
return string.IsNullOrWhiteSpace(localPart) ? "Driver" : localPart;
}
}

Copilot uses AI. Check for mistakes.
private string DisplayName =>
!string.IsNullOrWhiteSpace(UserSession.User?.Name)
? UserSession.User!.Name!
: UserSession.User?.Email ?? "User";
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DisplayName falls back to UserSession.User?.Email without a whitespace/empty check, so if the API returns an empty string (or a partially-populated user), the header will render Welcome, with a blank name. Consider using string.IsNullOrWhiteSpace(UserSession.User?.Email) in the fallback and/or normalizing UserSession.User.Email to null when empty.

Suggested change
: UserSession.User?.Email ?? "User";
: !string.IsNullOrWhiteSpace(UserSession.User?.Email)
? UserSession.User!.Email!
: "User";

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +21
[Route("user")]
public class UsersController : ControllerBase
{
[HttpGet("me")] // Final: /users/me (when accessed directly or via Nginx stripping /api/)
[HttpGet("me")]
public ActionResult<UserDto> GetMe()
{
var email = Request.Headers["Cf-Access-Authenticated-User-Email"].FirstOrDefault();
var jwtAssertion = Request.Headers["Cf-Access-Jwt-Assertion"].FirstOrDefault();
var nameFromJwt = GetJwtClaim(jwtAssertion, "name");
var emailFromJwt = GetJwtClaim(jwtAssertion, "email");

if (string.IsNullOrEmpty(email))
var email = emailFromJwt ?? Request.Headers["Cf-Access-Authenticated-User-Email"].FirstOrDefault();
var name = nameFromJwt ?? email?.Split('@')[0];
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API route was changed to [Route("user")] (so /api/user/me), but the Web app currently calls users/me (see F1.Web/Services/UserSession.cs), which will 404 and leave the UI unauthenticated. Please align the API route and the Web MeEndpoint so they match (either keep /user/me everywhere or use /users/me everywhere).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants