Report 25: Security & 2FA Comparison (v8 vs v17)¶
Generated: 2026-02-27 Scope: Progress.Security (v8), Progress.Umbraco2FA (v8), www/Security (v17)
1. Overview: Architectural Shift¶
The security and 2FA implementation underwent a complete architectural rewrite from v8 to v17, driven by the underlying platform change from OWIN middleware (ASP.NET 4.x) to ASP.NET Core Identity.
| Aspect | v8 | v17 |
|---|---|---|
| Authentication framework | OWIN + Microsoft.AspNet.Identity | ASP.NET Core Identity |
| Middleware pipeline | IAppBuilder (Katana/OWIN) |
IServiceCollection / IUmbracoBuilder |
| 2FA provider registration | UserManager.RegisterTwoFactorProvider() |
BackOfficeIdentityBuilder.AddTwoFactorProvider<T>() |
| User store | Custom BackOfficeUserStore subclass |
Custom BackOfficeUserStore subclass (new API) |
| Settings access | Umbraco.ContentSingleAtXPath("//twoFASettings") |
ITwoFactorSettingsService (DI) |
| API controllers | UmbracoApiController (Web API 2) |
ControllerBase (ASP.NET Core MVC) |
| Cookie auth | app.UseCookieAuthentication(...) |
IConfigureNamedOptions<CookieAuthenticationOptions> |
| Token validation | DataProtectorTokenProvider subclass |
ITwoFactorProvider interface |
| Composition | IUserComposer / IComponent |
IComposer |
| Custom DB tables | TwoFactor + TwoFactorTrustedDevice (NPoco) |
Umbraco's built-in umbracoTwoFactorLogin table |
| Projects | 2 separate projects (Progress.Security + Progress.Umbraco2FA) | 1 folder (www/Security/) |
| Total files | 17 source files across 2 projects | 8 files in 1 directory |
File Count Comparison¶
| Category | v8 | v17 |
|---|---|---|
| Composer/Registration | 1 (Composer.cs) | 1 (TwoFactorAuthComposer.cs) |
| Provider/Validation | 1 (TwoFactorValidationProvider.cs) | 1 (GoogleAuthenticatorTwoFactorProvider.cs) |
| User Store | 2 (UserManager + UserStore) | 1 (ForcedTwoFactorBackOfficeUserStore.cs) |
| API Controller | 1 (TwoFactorAuthController.cs) | 1 (TwoFactorSetupController.cs) |
| Service/Business Logic | 1 (TwoFactorService.cs) | 1 (TwoFactorSettingsService.cs) |
| Settings Interface | 0 (inline) | 1 (ITwoFactorSettingsService.cs) |
| Models | 4 (TwoFactor, AuthInfo, AuthSettings, TrustedDevice) | 0 (built-in + inline records) |
| DB Migration | 4 (Tables, Component, Plan, Section) | 0 (Umbraco manages schema) |
| Middleware/Events | 2 (TwoFactorEventHandler, Authorize attr) | 0 (built-in pipeline) |
| Cookie Config | 1 (OwinConfiguration.cs) | 1 (ConfigureTwoFactorRememberMeCookieOptions.cs) |
| View Options | 0 (in UserManager) | 1 (TwoFactorUserGroupOptions.cs) |
| TOTAL | 17 | 8 |
2. Side-by-Side: 2FA Controller¶
v8 (Progress.Umbraco2FA/TwoFactorAuthentication/Controllers/TwoFactorAuthController.cs)¶
[IsBackOffice]
[TwoFactorAuthorize] // Custom authorize attribute
[DisableBrowserCache]
[UmbracoWebApiRequireHttps]
public class TwoFactorAuthController : UmbracoApiController // Web API 2
{
private readonly TwoFactorService _twoFactorService;
public TwoFactorAuthController(TwoFactorService twoFactorService)
{
_twoFactorService = twoFactorService;
}
[HttpGet]
public async Task<IHttpActionResult> Get2FAStatus()
{
int userId = await GetUserId();
bool isActive = await _twoFactorService.IsTwoFactorActivated(userId);
var settings = Get2faSettings(); // XPath query inline
return Ok(new TwoFactorAuthSettings { ... });
}
[HttpGet]
public async Task<IHttpActionResult> GoogleAuthenticatorSetupCode()
{
int userId = await GetUserId();
var user = Services.UserService.GetUserById(userId);
var tfa = new TwoFactorAuthenticator();
var setupInfo = tfa.GenerateSetupCode(...);
// Manual DB operations via TwoFactorService
var twoFactorAuthInfo = await _twoFactorService.GetExistingAccount(...);
return Ok(twoFactorAuthInfo);
}
[HttpPost]
public async Task<IHttpActionResult> ValidateAndSaveGoogleAuth(string code)
{
int userId = await GetUserId();
bool isValid = await _twoFactorService.ValidateAndSaveGoogleAuth(code, userId);
return Ok(isValid);
}
// Trusted device management via custom cookie
[HttpPost]
public async Task<IHttpActionResult> AddTrustedDevice() { ... }
// Admin-only endpoints
[HttpGet] [Authorize(Roles = "admin")]
public IHttpActionResult Get2FAResetUsers() { ... }
[HttpPost] [Authorize(Roles = "admin")]
public bool Reset2FAForUser(string email) { ... }
// Helper: XPath-based settings
private TwoFactorAuthSettings Get2faSettings()
{
var settings = Umbraco.ContentSingleAtXPath("//twoFASettings");
return new TwoFactorAuthSettings { ... };
}
// Helper: OWIN-based user ID
private async Task<int> GetUserId()
{
if (Security.CurrentUser?.Id != null) return Security.CurrentUser.Id;
var signInManager = TryGetOwinContext().Result.GetBackOfficeSignInManager();
int userId = await signInManager.GetVerifiedUserIdAsync();
return userId != int.MinValue ? userId : throw ...;
}
}
v17 (dbl.Progress/src/www/Security/TwoFactorSetupController.cs)¶
[ApiController]
[Route("/umbraco/api/twofactor")]
public class TwoFactorSetupController : ControllerBase // ASP.NET Core
{
private readonly IBackOfficeSignInManager _signInManager;
private readonly IUserTwoFactorLoginService _userTwoFactorLoginService;
private readonly ILogger<TwoFactorSetupController> _logger;
public TwoFactorSetupController(
IBackOfficeSignInManager signInManager,
IUserTwoFactorLoginService userTwoFactorLoginService,
ILogger<TwoFactorSetupController> logger) { ... }
[AllowAnonymous]
[HttpGet("setup")]
public async Task<IActionResult> GetSetupInfo()
{
// Umbraco's built-in 2FA pending user flow
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user is null) return Unauthorized(...);
// Delegate to Umbraco's built-in service
var result = await _userTwoFactorLoginService.GetSetupInfoAsync(
user.Key, GoogleAuthenticatorTwoFactorProvider.Name);
var setupModel = result.Result as GoogleAuthenticatorSetupModel;
return Ok(new { qrCodeSetupImageUrl, manualEntryKey, secret });
}
[AllowAnonymous]
[HttpGet("trusted-device-settings")]
public async Task<IActionResult> GetTrustedDeviceSettings(
[FromServices] ITwoFactorSettingsService settingsService)
{
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user is null) return Unauthorized(...);
return Ok(new { allowTrustedDevices, trustedDeviceDays });
}
[AllowAnonymous]
[HttpPost("validate-and-complete")]
public async Task<IActionResult> ValidateAndComplete(
[FromBody] ValidateAndCompleteModel model)
{
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
// Umbraco's built-in validate-and-save
var saveResult = await _userTwoFactorLoginService.ValidateAndSaveAsync(
providerName, user.Key, model.Secret, model.Code);
// Complete login in one step
var signInResult = await _signInManager.TwoFactorSignInAsync(
providerName, model.Code, model.IsPersistent, model.RememberClient);
return Ok(new { success = true });
}
}
Key Controller Differences¶
| Aspect | v8 | v17 |
|---|---|---|
| Base class | UmbracoApiController (Web API 2) |
ControllerBase (ASP.NET Core) |
| Return type | IHttpActionResult |
IActionResult |
| Route | Convention-based /umbraco/backoffice/api/... |
Attribute: [Route("/umbraco/api/twofactor")] |
| Auth | Custom [TwoFactorAuthorize] attribute |
[AllowAnonymous] (pending 2FA session) |
| User lookup | Security.CurrentUser + OWIN GetVerifiedUserIdAsync() |
_signInManager.GetTwoFactorAuthenticationUserAsync() |
| DB operations | Custom TwoFactorService with raw NPoco SQL |
IUserTwoFactorLoginService (Umbraco built-in) |
| Setup flow | Separate setup + validate endpoints | Combined validate-and-complete endpoint |
| Admin reset | Custom Get2FAResetUsers() + Reset2FAForUser() |
Handled by Umbraco backoffice UI |
| Trusted devices | Custom cookie + TwoFactorTrustedDevice table |
Umbraco's TwoFactorRememberMe cookie |
| Error handling | try/catch + Logger.Error<T>(ex) |
ILogger<T> with structured logging |
| Settings | Inline XPath: Umbraco.ContentSingleAtXPath(...) |
ITwoFactorSettingsService (DI) |
| Endpoints | 6 endpoints | 3 endpoints |
3. Complete File Mapping Table¶
3.1 Progress.Security (v8) -> Consolidated into ASP.NET Core pipeline¶
| v8 File | v17 Equivalent | Notes |
|---|---|---|
OwinConfiguration.cs |
-- | REMOVED: OWIN cookie auth replaced by ASP.NET Core Identity built-in middleware. Cookie configuration for frontoffice auth is now in Program.cs via standard AddAuthentication().AddCookie(). |
Progress.Security.csproj |
-- | REMOVED: Entire project eliminated. |
3.2 Progress.Umbraco2FA (v8) -> www/Security/ (v17)¶
| v8 File | v17 File | Notes |
|---|---|---|
| Composers/Composer.cs | TwoFactorAuthComposer.cs | IUserComposer -> IComposer. Registers provider + user store via IUmbracoBuilder instead of Composition. Also registers settings service and cookie options. |
| Service/TwoFactorService.cs | -- | REMOVED: All raw DB operations (check activation, save secret, manage trusted devices) replaced by Umbraco's built-in IUserTwoFactorLoginService. Custom TwoFactor table replaced by umbracoTwoFactorLogin. |
| Controllers/TwoFactorAuthController.cs | TwoFactorSetupController.cs | Complete rewrite. 6 endpoints -> 3. See side-by-side above. |
| Middleware/TwoFactorBackOfficeUserManager.cs | -- | REMOVED: v8 required subclassing BackOfficeUserManager to register providers and override SupportsUserTwoFactor. In v17, this is handled declaratively via AddTwoFactorProvider<T>(). |
| Middleware/TwoFactorBackOfficeUserStore.cs | ForcedTwoFactorBackOfficeUserStore.cs | Rewritten. Same concept (override GetTwoFactorEnabledAsync) but completely different base class API. XPath settings -> DI service. Trusted device check removed (now cookie-based). |
| Middleware/TwoFactorEventHandler.cs | -- | REMOVED: IComponent that hooked into UmbracoDefaultOwinStartup.MiddlewareConfigured to configure the entire OWIN pipeline. In v17, composition is declarative via IComposer. |
| Middleware/TwoFactorValidationProvider.cs | GoogleAuthenticatorTwoFactorProvider.cs | DataProtectorTokenProvider<BackOfficeIdentityUser, int> -> ITwoFactorProvider. Same Google Authenticator logic, cleaner interface. |
| TwoFactorAuthorizeAttribute.cs | -- | REMOVED: Custom AuthorizeAttribute that checked OWIN 2FA authentication state. In v17, Umbraco's built-in middleware handles the 2FA session flow automatically. |
| Constants.cs | -- | REMOVED: Provider name constant moved to GoogleAuthenticatorTwoFactorProvider.Name static property. Custom table names no longer needed. |
| Models/TwoFactor.cs | -- | REMOVED: NPoco model for custom TwoFactor table. Replaced by Umbraco's built-in umbracoTwoFactorLogin table schema. |
| Models/TwoFactorAuthInfo.cs | GoogleAuthenticatorSetupModel (inline) |
Setup model simplified. Now implements ISetupTwoFactorModel interface. Defined inside GoogleAuthenticatorTwoFactorProvider.cs. |
| Models/TwoFactorAuthSettings.cs | ITwoFactorSettingsService.cs | Settings class -> DI interface. Properties moved to read-only interface backed by CMS content query. |
| Models/TwoFactorTrustedDevice.cs | -- | REMOVED: Custom trusted device table. Replaced by Umbraco's built-in TwoFactorRememberMe cookie mechanism. |
| Migration/CreateTwoFactorTables.cs | -- | REMOVED: Custom DB migration for TwoFactor + TwoFactorTrustedDevice tables no longer needed. |
| Migration/TwoFactorMigrationComponent.cs | -- | REMOVED: Component that ran the custom migration on startup. |
| Migration/TwoFactorPlan.cs | -- | REMOVED: Migration plan class. |
| Migration/TwoFactorAuthenticationSection.cs | -- | REMOVED: Already commented out in v8. Was for a custom backoffice section. |
| -- | ITwoFactorSettingsService.cs | NEW: Abstraction for CMS-driven 2FA settings (enabled, CU name, trusted devices). |
| -- | TwoFactorSettingsService.cs | NEW: Implementation using IPublishedContentQuery to read twoFASettings content node. |
| -- | TwoFactorUserGroupOptions.cs | NEW: IBackOfficeTwoFactorOptions implementation returning custom JS module path for the 2FA login view. |
| -- | ConfigureTwoFactorRememberMeCookieOptions.cs | NEW: IConfigureNamedOptions<CookieAuthenticationOptions> that sets remember-me cookie lifetime from CMS settings. |
4. What Was Consolidated/Removed and Why¶
4.1 Entire Project Removed: Progress.Security¶
1 file: OwinConfiguration.cs
This project existed solely to configure OWIN cookie authentication for the frontoffice. In ASP.NET Core, cookie authentication is configured in Program.cs using the built-in middleware:
// v8 (OWIN)
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/"),
ExpireTimeSpan = TimeSpan.FromMinutes(20),
Provider = new CookieAuthenticationProvider()
});
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
// v17 (ASP.NET Core) -- built into Umbraco's pipeline
// No separate project needed; Umbraco configures its own cookie auth
4.2 Custom Database Tables Removed¶
v8 maintained two custom tables (TwoFactor, TwoFactorTrustedDevice) with NPoco models, a migration plan, and direct SQL operations via TwoFactorService. This required 7 files:
TwoFactor.cs(model)TwoFactorTrustedDevice.cs(model)TwoFactorService.cs(data access)CreateTwoFactorTables.cs(migration)TwoFactorMigrationComponent.cs(migration runner)TwoFactorPlan.cs(migration plan)TwoFactorAuthenticationSection.cs(commented out)
In v17, Umbraco provides the built-in umbracoTwoFactorLogin table and IUserTwoFactorLoginService for all 2FA data operations. The trusted device functionality is handled by Umbraco's TwoFactorRememberMe authentication cookie. All 7 files eliminated.
4.3 OWIN Middleware Pipeline Removed¶
v8 required extensive OWIN middleware configuration across 3 files:
TwoFactorEventHandler.cs-- hooked intoUmbracoDefaultOwinStartup.MiddlewareConfiguredto reconfigure the entire auth pipelineTwoFactorBackOfficeUserManager.cs-- subclassedBackOfficeUserManagerto register providers and return the custom 2FA viewTwoFactorAuthorizeAttribute.cs-- customAuthorizeAttributechecking OWIN 2FA auth state
In v17, this is all handled declaratively in TwoFactorAuthComposer.cs:
// v17: 3 lines replace 3 files
identityBuilder.AddTwoFactorProvider<GoogleAuthenticatorTwoFactorProvider>(Name);
builder.SetBackOfficeUserStore<ForcedTwoFactorBackOfficeUserStore>();
builder.Services.AddSingleton<IConfigureNamedOptions<CookieAuthenticationOptions>,
ConfigureTwoFactorRememberMeCookieOptions>();
4.4 What Was Added in v17¶
Three files are new in v17 with no direct v8 equivalent:
-
ITwoFactorSettingsService.cs+TwoFactorSettingsService.cs: Clean abstraction for CMS-driven settings, replacing inline XPath queries scattered across multiple v8 files (TwoFactorAuthController.Get2faSettings(),TwoFactorBackOfficeUserStore.GetTwoFactorEnabledAsync()). -
TwoFactorUserGroupOptions.cs: ImplementsIBackOfficeTwoFactorOptionsto specify the custom JS module for the 2FA login screen. In v8, this was a method onTwoFactorBackOfficeUserManager.GetTwoFactorView()returning an HTML file path. -
ConfigureTwoFactorRememberMeCookieOptions.cs: Configures the remember-me cookie lifetime from CMS settings. In v8, trusted device cookie management was manual (rawCookieHeaderValuecreation in the controller).
4.5 Security Improvements in v17¶
| Area | v8 Issue | v17 Improvement |
|---|---|---|
| SQL injection | TwoFactorValidationProvider uses string formatting: $"WHERE [userId] = {user.Id}" |
Uses Umbraco's parameterized IUserTwoFactorLoginService |
| Hardcoded API key | TwoFactorBackOfficeUserStore contains Umbraco Headless API key for IP whitelist |
Removed; admin group check only |
| Secret logging | TwoFactorService logs verification codes: $"Invalid 2FA verification code '{code}'" |
v17 logs structured events without sensitive data (though setup logging still includes secret for debugging -- should be removed) |
| Cookie security | Manual HttpOnly = true, Secure = true on trusted device cookie |
Umbraco's built-in cookie authentication handles security attributes |
| Error fallback | On error, returns !IsAdmin(user) which forces 2FA for unknown state |
Same pattern, but with structured logging |
| Deprecated APIs | Multiple [Obsolete] attributes on v8 methods |
Clean API surface in v17 |
4.6 Remaining v17 Concern¶
The GoogleAuthenticatorTwoFactorProvider.cs contains LogWarning calls that log the 2FA secret and validation details. These are debug-level traces that should be downgraded to LogDebug or removed before production:
_logger.LogWarning("2FA SETUP: secret={Secret}, manualKey={ManualKey}, userKey={UserKey}", ...);
_logger.LogWarning("2FA VALIDATE: secret={Secret}, code={Code}, ...", ...);
5. Architecture Diagram¶
v8 Architecture (2 projects, 17 files)
=======================================
Progress.Security/
OwinConfiguration.cs -----> app.UseCookieAuthentication()
Progress.Umbraco2FA/
Composer.cs -----> IUserComposer.Compose()
| |-> Register TwoFactorMigrationComponent
| |-> Register TwoFactorEventHandler
| |-> Register TwoFactorService
|
TwoFactorEventHandler.cs
|-> UmbracoDefaultOwinStartup.MiddlewareConfigured +=
| |-> app.UseTwoFactorSignInCookie()
| |-> app.UseUmbracoBackOfficeCookieAuthentication()
| |-> app.ConfigureUserManagerForUmbracoBackOffice<TwoFactorBackOfficeUserManager>()
|
TwoFactorBackOfficeUserManager.cs
|-> RegisterTwoFactorProvider("GoogleAuthenticator", TwoFactorValidationProvider)
|-> GetTwoFactorView() -> "../App_Plugins/2FactorAuthentication/2fa-login.html"
|
TwoFactorBackOfficeUserStore.cs
|-> GetTwoFactorEnabledAsync() -- XPath: "//twoFASettings"
|-> IsTrustedDevice() -- custom DB table + cookie
|
TwoFactorValidationProvider.cs
|-> ValidateAsync() -- custom DB query + Google Authenticator
|
TwoFactorService.cs -- raw NPoco CRUD operations
|
TwoFactorAuthController.cs -- 6 API endpoints
|
Migration/ -- 4 files for custom DB tables
Models/ -- 4 POCOs
v17 Architecture (1 folder, 8 files)
======================================
www/Security/
TwoFactorAuthComposer.cs -----> IComposer.Compose()
|-> AddScoped<ITwoFactorSettingsService>()
|-> AddTwoFactorProvider<GoogleAuthenticatorTwoFactorProvider>()
|-> SetBackOfficeUserStore<ForcedTwoFactorBackOfficeUserStore>()
|-> AddSingleton<ConfigureTwoFactorRememberMeCookieOptions>()
|
GoogleAuthenticatorTwoFactorProvider.cs
|-> ITwoFactorProvider (ValidateTwoFactorPIN, GetSetupDataAsync)
|
ForcedTwoFactorBackOfficeUserStore.cs
|-> GetTwoFactorEnabledAsync() -- ITwoFactorSettingsService (DI)
|
TwoFactorSettingsService.cs + ITwoFactorSettingsService.cs
|-> IPublishedContentQuery: DescendantsOrSelfOfType("twoFASettings")
|
TwoFactorSetupController.cs -- 3 API endpoints
|-> IBackOfficeSignInManager
|-> IUserTwoFactorLoginService (Umbraco built-in)
|
TwoFactorUserGroupOptions.cs
|-> IBackOfficeTwoFactorOptions.GetTwoFactorView()
|
ConfigureTwoFactorRememberMeCookieOptions.cs
|-> IConfigureNamedOptions<CookieAuthenticationOptions>