Skip to content

Conversation

@ysmoradi
Copy link
Member

@ysmoradi ysmoradi commented Dec 29, 2025

closes #11939

Summary by CodeRabbit

  • New Features

    • Added authentication method tracking throughout sign-in and token refresh flows to identify which method was used (External, SMS, Email, Push, WebAuthn, or Password).
  • Changes

    • Keycloak identity claims are now conditionally retrieved only when external authentication method is used.
    • Removed the two-factor authentication enabled authorization policy.

✏️ Tip: You can customize this high-level summary in your review settings.

@ysmoradi ysmoradi requested review from Copilot and msynk December 29, 2025 23:21
@coderabbitai
Copy link

coderabbitai bot commented Dec 29, 2025

Walkthrough

The changes introduce a METHOD claim to track authentication methods (External, SMS, Email, Password, etc.) throughout the authentication and token lifecycle. The METHOD claim is stored during sign-in, propagated during token refresh, and used to conditionally gate Keycloak claim retrieval. The previous amr claim-based TFA validation is replaced with this new approach.

Changes

Cohort / File(s) Summary
Claims & Constants
src/Shared/Services/AppClaimTypes.cs, src/Shared/Services/AuthPolicies.cs
Added METHOD claim constant ("method"); removed TFA_ENABLED policy constant to eliminate reliance on amr claim for multi-factor validation.
Authorization Policy Configuration
src/Shared/Extensions/ISharedServiceCollectionExtensions.cs
Removed TFA_ENABLED policy registration that previously required amr claim value of "mfa" during authorization setup.
Authentication Method Storage
src/Server/Boilerplate.Server.Api/Extensions/SignInManagerExtensions.cs, src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs
Store selected authentication method in context items during sign-in and token refresh; replace SignInWithClaimsAsync with SignInAsync to avoid amr claim assignment.
Claims Generation & Keycloak Integration
src/Server/Boilerplate.Server.Api/Services/Identity/AppUserClaimsPrincipalFactory.cs
Add IHttpContextAccessor dependency to constructor; ensure METHOD claim is present in generated claims (default to "Password"); restrict Keycloak claim retrieval to external authentication only (METHOD == "External").

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SignIn as SignIn Flow
    participant Context as HttpContext.Items
    participant ClaimsFactory as AppUserClaimsPrincipalFactory
    participant JWT as JWT Token

    User->>SignIn: Authenticate with method (External/SMS/Password)
    SignIn->>Context: Store METHOD claim
    SignIn->>SignIn: SignInAsync (no amr claim)
    
    rect rgb(200, 220, 240)
    Note over ClaimsFactory: Claims Generation
    ClaimsFactory->>Context: Read METHOD from Items
    ClaimsFactory->>ClaimsFactory: Add METHOD claim to principal
    
    alt METHOD == "External"
        ClaimsFactory->>ClaimsFactory: Retrieve & merge Keycloak claims
    else Other methods
        ClaimsFactory->>ClaimsFactory: Skip Keycloak integration
    end
    end
    
    ClaimsFactory->>JWT: Build token with METHOD claim
    JWT->>User: Return token with auth method preserved
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A METHOD to our madness, stored with care,
From sign-in's hop through tokens in the air,
No amr claims to tangle up the way,
Just METHOD marks—which auth method held sway!
Keycloak gates now wise, external-bound,
Where rabbit's authentication logic is sound! 🔐

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Out of Scope Changes check ❓ Inconclusive The removal of TFA_ENABLED policy and migration from amr to METHOD claims represent intentional refactoring aligned with the new authentication method storage strategy; however, the policy removal warrants clarification regarding deprecation of amr-based MFA enforcement. Clarify whether the TFA_ENABLED policy removal and amr claim deprecation are intentional or if existing MFA enforcement logic needs migration to the new METHOD-based approach.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: storing/preserving the authentication method in JWT tokens, which aligns with the changeset modifications across identity controllers, claims factories, and authentication extensions.
Linked Issues check ✅ Passed The changes implement the core requirement from issue #11939 by adding METHOD claim storage throughout the authentication pipeline: in SignInManagerExtensions, IdentityController, AppUserClaimsPrincipalFactory, and AppClaimTypes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/Identity/AppUserClaimsPrincipalFactory.cs (1)

28-32: Consider defensive null handling for HttpContext.

Line 30 uses the null-forgiving operator on HttpContext. While this code path runs during HTTP request processing where HttpContext should always exist, using ?. with a fallback would be more defensive:

🔎 Defensive null handling
- string? authenticationMethod = httpContextAccessor.HttpContext!.Items[AppClaimTypes.METHOD]?.ToString() ?? "Password";
+ string authenticationMethod = httpContextAccessor.HttpContext?.Items[AppClaimTypes.METHOD]?.ToString() ?? "Password";
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 33aca75 and cad5667.

📒 Files selected for processing (6)
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Extensions/SignInManagerExtensions.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/Identity/AppUserClaimsPrincipalFactory.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppClaimTypes.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AuthPolicies.cs
💤 Files with no reviewable changes (2)
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AuthPolicies.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs
🧰 Additional context used
🧬 Code graph analysis (3)
src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Extensions/SignInManagerExtensions.cs (1)
src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppClaimTypes.cs (1)
  • AppClaimTypes (4-37)
src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs (1)
src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppClaimTypes.cs (1)
  • AppClaimTypes (4-37)
src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/Identity/AppUserClaimsPrincipalFactory.cs (1)
src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppClaimTypes.cs (1)
  • AppClaimTypes (4-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: CodeQL analysis (csharp)
  • GitHub Check: Agent
  • GitHub Check: build Bit.Templates
🔇 Additional comments (5)
src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Extensions/SignInManagerExtensions.cs (2)

68-69: LGTM - Auth method stored for downstream claim generation.

The authentication method is correctly stored in the HTTP context after successful OTP verification. By this point, authenticationMethod is guaranteed non-null since we've passed the tokenIsValid is false guard at line 62.


91-91: Verify the removal of explicit claim assignment.

The change from SignInWithClaimsAsync (with amr claim) to SignInAsync is consistent with the new design where AppUserClaimsPrincipalFactory handles METHOD claim generation. Confirm this aligns with the removal of amr-based TFA validation mentioned in the PR summary.

src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppClaimTypes.cs (1)

31-36: LGTM - Well-documented claim constant.

The METHOD claim constant is properly defined with comprehensive documentation covering all supported authentication methods.

src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/Identity/AppUserClaimsPrincipalFactory.cs (2)

45-47: Keycloak claim retrieval guard is well-designed.

The early return for non-external authentication prevents unnecessary Keycloak token operations for password/OTP-based sign-ins. This correctly limits Keycloak claim merging to users who authenticated via external providers.


9-10: Constructor change looks good.

The addition of IHttpContextAccessor enables access to the authentication method stored in HttpContext.Items during claim generation.

Comment on lines +242 to +243
HttpContext.Items[AppClaimTypes.METHOD] = refreshTicket.Principal.GetClaimValue<string?>(AppClaimTypes.METHOD);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Backward compatibility: Existing external auth tokens will lose Keycloak claims after refresh.

When refreshing tokens issued before this change (lacking the METHOD claim), GetClaimValue<string?> returns null, causing AppUserClaimsPrincipalFactory to default to "Password". This breaks RetrieveKeycloakClaims which early-exits unless METHOD is "External".

Existing Keycloak/external users will lose their Keycloak-derived claims after their first token refresh post-deployment.

Consider deriving the method from stored Keycloak tokens as a fallback:

🔎 Suggested fallback approach
- HttpContext.Items[AppClaimTypes.METHOD] = refreshTicket.Principal.GetClaimValue<string?>(AppClaimTypes.METHOD);
+ var method = refreshTicket.Principal.GetClaimValue<string?>(AppClaimTypes.METHOD);
+ // Fallback for tokens issued before METHOD claim was introduced
+ if (string.IsNullOrEmpty(method))
+ {
+     // Check if user has Keycloak tokens stored, implying external auth
+     var keycloakToken = await userManager.GetAuthenticationTokenAsync(userSession.User!, "Keycloak", "refresh_token");
+     method = string.IsNullOrEmpty(keycloakToken) ? "Password" : "External";
+ }
+ HttpContext.Items[AppClaimTypes.METHOD] = method;

Note: This fix would need to be applied after line 254 where user is available, or refactored accordingly.

Committable suggestion skipped: line range outside the PR's diff.

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

This PR refactors authentication method tracking in JWT tokens by replacing the ASP.NET Core Identity standard "amr" (Authentication Methods Reference) claim with a custom METHOD claim. The change removes the TFA_ENABLED policy that relied on the "amr" claim and ensures all authentication methods (Password, Email, SMS, Push, External, WebAuthn) are consistently tracked throughout the authentication lifecycle.

Key Changes

  • Introduced a new custom claim type AppClaimTypes.METHOD to store the authentication method used
  • Removed the TFA_ENABLED authorization policy that checked for the "amr" claim with "mfa" value
  • Updated token refresh flow to preserve the authentication method across token renewals
  • Modified claims generation to always include the METHOD claim with appropriate fallback

Reviewed changes

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

Show a summary per file
File Description
src/Shared/Services/AuthPolicies.cs Removed the TFA_ENABLED constant and policy definition
src/Shared/Services/AppClaimTypes.cs Added METHOD claim type constant with documentation of supported authentication methods
src/Shared/Extensions/ISharedServiceCollectionExtensions.cs Removed TFA_ENABLED policy registration from authorization configuration
src/Server/Boilerplate.Server.Api/Services/Identity/AppUserClaimsPrincipalFactory.cs Added logic to inject METHOD claim during claims generation, with HttpContext access and early return for non-Keycloak authentications
src/Server/Boilerplate.Server.Api/Extensions/SignInManagerExtensions.cs Added HttpContext.Items storage for authentication method in OTP sign-in flow and removed explicit "amr" claim
src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs Added METHOD claim preservation during token refresh to maintain authentication context

Comment on lines +45 to +46
if (aspnetCoreIdentityClaims.HasClaim(AppClaimTypes.METHOD, "External") is false)
return; // User was not authenticated via Keycloak
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The early return when the METHOD claim is not "External" means RetrieveKeycloakClaims will be skipped for all non-external authentications. However, this logic assumes that Keycloak is only used for external authentication. If there's a possibility of Keycloak being used as an authentication provider in other scenarios, this check might be too restrictive. Consider whether this assumption is always valid, or if additional configuration should be checked.

Copilot uses AI. Check for mistakes.

/// <summary>
/// The method used for user authentication.
/// External (Social), Sms (Web-OTP), Email (Magic Link or 6 digit code), Push notification (6 digit code), WebAuthn (Face-Id, Fingerprint etc), Password.
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The documentation mentions "Web-OTP" for SMS, but the actual authentication method value stored is "Sms" (as seen in SignInManagerExtensions.cs). Consider clarifying the documentation to indicate that "Web-OTP" is the technology used for SMS-based authentication, while the stored value is "Sms", to avoid confusion between the implementation detail and the stored claim value.

Suggested change
/// External (Social), Sms (Web-OTP), Email (Magic Link or 6 digit code), Push notification (6 digit code), WebAuthn (Face-Id, Fingerprint etc), Password.
/// Stored values: External (Social), Sms, Email (Magic Link or 6 digit code), Push notification (6 digit code), WebAuthn (Face-Id, Fingerprint etc), Password.
/// Web-OTP is the technology used for SMS-based authentication; the stored claim value is "Sms".

Copilot uses AI. Check for mistakes.
Comment on lines +242 to +243
HttpContext.Items[AppClaimTypes.METHOD] = refreshTicket.Principal.GetClaimValue<string?>(AppClaimTypes.METHOD);

Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The use of the null-forgiving operator on HttpContext here assumes it's always non-null. While this is likely safe in the Refresh endpoint context, it would be more defensive to handle the null case explicitly or add a null check, especially since this is a critical authentication flow. Consider using httpContextAccessor.HttpContext?.Items or adding an explicit null check with an appropriate exception.

Suggested change
HttpContext.Items[AppClaimTypes.METHOD] = refreshTicket.Principal.GetClaimValue<string?>(AppClaimTypes.METHOD);
var httpContext = HttpContext;
if (httpContext is null)
{
throw new InvalidOperationException("HTTP context is not available for the refresh token request.");
}
httpContext.Items[AppClaimTypes.METHOD] = refreshTicket.Principal.GetClaimValue<string?>(AppClaimTypes.METHOD);

Copilot uses AI. Check for mistakes.

if (result.Claims.Any(c => c.Type == AppClaimTypes.METHOD) is false)
{
string? authenticationMethod = httpContextAccessor.HttpContext!.Items[AppClaimTypes.METHOD]?.ToString() ?? "Password";
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The use of the null-forgiving operator on HttpContext here assumes it's always non-null. While this may be safe during claims generation, it would be more robust to handle the null case explicitly or add a null check. If HttpContext is expected to always be available at this point, consider adding a descriptive exception when it's null. Alternatively, the fallback to "Password" could be adjusted to account for scenarios where HttpContext might be unavailable.

Suggested change
string? authenticationMethod = httpContextAccessor.HttpContext!.Items[AppClaimTypes.METHOD]?.ToString() ?? "Password";
var httpContext = httpContextAccessor.HttpContext;
var authenticationMethod = httpContext?.Items[AppClaimTypes.METHOD]?.ToString() ?? "Password";

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.

bit Boilerplate must preserve auth method in jwt token

1 participant