Skip to content

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 into UmbracoDefaultOwinStartup.MiddlewareConfigured to reconfigure the entire auth pipeline
  • TwoFactorBackOfficeUserManager.cs -- subclassed BackOfficeUserManager to register providers and return the custom 2FA view
  • TwoFactorAuthorizeAttribute.cs -- custom AuthorizeAttribute checking 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:

  1. ITwoFactorSettingsService.cs + TwoFactorSettingsService.cs: Clean abstraction for CMS-driven settings, replacing inline XPath queries scattered across multiple v8 files (TwoFactorAuthController.Get2faSettings(), TwoFactorBackOfficeUserStore.GetTwoFactorEnabledAsync()).

  2. TwoFactorUserGroupOptions.cs: Implements IBackOfficeTwoFactorOptions to specify the custom JS module for the 2FA login screen. In v8, this was a method on TwoFactorBackOfficeUserManager.GetTwoFactorView() returning an HTML file path.

  3. ConfigureTwoFactorRememberMeCookieOptions.cs: Configures the remember-me cookie lifetime from CMS settings. In v8, trusted device cookie management was manual (raw CookieHeaderValue creation 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>
Migration documentation by Double for Progress Credit Union