Contents
data/authors/ .json

Integrating SSO with EntraID Authentication into Legacy Applications

The application being retro-fitted with Single Sign On (SSO) was an internally hosted web app (IIS), with a public URL.

Built using ASP.NET Framework MVC 5 and used a custom authentication system based on username and password stored in the database.

The number of users was relatively small, and there wasn’t a high churn rate.

The manual account creation process was manageable but not ideal.

This made it a great candidate for trialing a move to SSO with EntraID, as the user base was stable and we could easily communicate changes to them.

πŸ” Current Authentication System

To gain access to the system, users entered their login name and password (or more likely, the browser pre-filled these values) in a typical login screen.

The application verified that:

  • such a user existed in the database Users table
  • that the password matched the stored value

This was not a self-service system, so users had to request an account from IT.

New user accounts were created using SQL scripts and the login information was manually emailed to the user.

It also required Ops to add the new user’s email address to the SMTP server relay rules, as the application sent emails directly via SMTP, spoofing the sender address to be the authenticated user.

There was usually a delay between the initial request for a new account and its actual creation.

πŸ” Security Features Implemented

Authentication via EntraID

  • βœ… OpenID Connect protocol
  • βœ… Claims-based authentication
  • βœ… Anti-forgery token support configured

Authorization

  • βœ… Token validated and UPN/email extracted
  • βœ… Database user check for authorization
  • βœ… Role-based authorization (claims-based)
  • βœ… User-specific permissions
  • βœ… Session management

Audit & Logging

  • βœ… Successful & failed login events logged
  • βœ… Authentication method tracked via audit table (EntraID vs Legacy)

Backward Compatibility

  • βœ… Session variables still populated via CurrentUser.SyncToSession()
  • βœ… Legacy authentication available via web config switch
  • βœ… Existing [Authorize] attributes work unchanged
  • βœ… Role-based redirects maintained

πŸ“Š Success Metrics

Query the AuditLog table to track authentication method usage and login success rates.

As users transition to EntraID, we should see a decrease in legacy logins and an increase in EntraID logins, with a high success rate for both methods during the transition period.

Target: 90%+ EntraID adoption within 60 days

Authentication Flow

User β†’ Login Page
  ↓
Redirected to Microsoft Login
  ↓
EntraID Authentication
  ↓
Token Validated
  ↓
User Lookup in Application Database
  ↓
User Found & Enabled?
  ↓ YES                    ↓ NO
Claims Added         Access Denied Page
  ↓
Roles Added
  ↓
Audit Log Entry
  ↓
Redirect to Application

πŸ’‘ Lessons Learned

What Went Well

  • βœ… Hybrid authentication mode allowed seamless transition
  • βœ… Claims-based architecture integrated cleanly with existing code
  • βœ… Audit logging provided visibility into authentication patterns
  • βœ… CurrentUser helper maintained backward compatibility

Challenges Encountered

  • ⚠️ Anti-forgery token configuration was initially overlookedβ€”a subtle but critical detail
  • ⚠️ Understanding how OWIN middleware changes authentication expectations took time
  • ⚠️ Testing required fresh browser sessions to clear cached cookies

Recommendations for Similar Projects

  1. Plan for anti-forgery tokens early - Add AntiForgeryConfig.UniqueClaimTypeIdentifier during initial setup
  2. Test incrementally - Verify each form type (GET, POST with tokens) works with both auth methods
  3. Document claim architecture - Be explicit about which claims are used where
  4. Allow time for cookie troubleshooting - Authentication cookies can cache issues; plan for this in testing
  5. Use hybrid mode - Don’t force users to switch immediately; let them choose during transition

Future Enhancements

Microsoft Graph

Currently the app sends emails directly via SMTP, spoofing the sender address to be the authenticated user. This requires amending the SMTP server relay rules to allow this, and also means that sent emails don’t appear in the user’s Sent Items folder. Integrating with Microsoft Graph API for sending emails would allow us to send on behalf of the authenticated user.

🎯 Key Features

Hybrid Authentication Mode

The implementation supports a transition period where both authentication methods can coexist:

Hybrid Mode (Transition):

  • βœ… No disruption to existing users
  • βœ… Gradual adoption of EntraID
  • βœ… Easy rollback if issues occur
  • βœ… Users can choose based on comfort level
<!-- UseLegacyAuthentication options:
	 - "false" = EntraID only (most secure)
	 - "true" = Legacy username/password only (for rollback)
	 - "hybrid" = Show both options, let users choose (for transition)
-->
<add key="UseLegacyAuthentication" value="hybrid" />

EntraID Only (Recommended):

<add key="UseLegacyAuthentication" value="true" />

Quick Rollback:

<!-- Set in Web.config -->
<add key="UseLegacyAuthentication" value="true" />

Users can immediately login with username/password.

βœ… Implementation

Azure Portal - EntraID

  • App registration with correct redirect URIs (localhost for dev, public facing URL for production)
  • ID tokens enabled - OWIN middleware requires implicit grant flow
  • ClientId and Tenant values added to Web.config
  • User.Read permission granted
  • Users in EntraID have UPN/email matching UserName in application database

App Config

  • Web.config has correct ClientId and Tenant
  • redirectUri is correct for your environment. Web config transformations used to set different values for dev vs production.

Backend

  • EntraID authentication now initialized before other services.
  • User validation against application’s database - is the user authenticated via EntraID actually a valid user in the system?
  • Custom claims mapping (UserID, UserName, DisplayName, Roles) added to identity
  • Audit logging for authentication events with method tracking (EntraID vs Legacy)
  • Helper class for accessing authenticated user from claims
  • Backward compatibility with session-based code via CurrentUser.SyncToSession()

UI

  • New button for EntraID authentication on login page
  • Existing username/password form still displayed when legacy/hybrid authentication enabled

Database

  • Users table altered to allow nullable passwords for future users created via EntraID.
  • UserRepository updated to support lookup by UserID for claims-based authentication.
  • Existing users have UserName set to their EntraID email/UPN for seamless lookup after authentication.
  • AuditLog table used to track authentication events, including method (EntraID vs Legacy) for monitoring transition progress.

πŸ§ͺ Testing in Hybrid Mode

Test Scenario 1: User Chooses EntraID

  1. Go to login page
  2. Click “Sign in with Microsoft EntraID”
  3. Authenticate with Microsoft
  4. Verify logged in successfully
  5. Check audit log shows “EntraID” method

Test Scenario 2: User Chooses Legacy

  1. Go to login page
  2. Enter username/password in form
  3. Click “Login” button
  4. Verify logged in successfully
  5. Check audit log shows “Legacy” method

Test Scenario 3: Mixed Usage

  1. User logs in with EntraID
  2. Logs out
  3. Logs in again with Legacy
  4. Both should work
  5. Audit log should show both methods

Test Scenario 4: Concurrent Sessions

  1. User A logs in with EntraID on Browser 1
  2. User B logs in with Legacy on Browser 2
  3. Both should work independently
  4. No session conflicts

Other Test Cases to Consider

  1. Test with multiple user roles
  2. Test denied access scenarios
  3. Verify audit logs are correct
  4. Test logout and session management

⚠️ Critical Implementation Detail: Anti-Forgery Token Configuration

The Problem

After implementing EntraID authentication, a subtle but critical issue emerged: forms using @Html.AntiForgeryToken() threw an InvalidOperationException:

The forms worked perfectly with legacy authentication but failed with EntraID login.

Root Cause

ASP.NET MVC’s anti-forgery token system requires a unique identifier claim to cryptographically bind tokens to specific users. By default, it looks for:

  • ClaimTypes.NameIdentifier, OR
  • ClaimTypes.IdentityProvider

When OWIN authentication middleware is registered (which EntraID requires), the framework automatically switches to expecting claims-based identities for all authenticated users β€” even if they logged in via the legacy method.

Initially, only custom application claims were added (ApplicationUserID, ApplicationUserName, etc.) but didn’t explicitly configure which claim the anti-forgery system should use for user identification.

The Solution

Configure the anti-forgery system to use one of the custom claims that’s present for both authentication methods:

In Global.asax.cs β†’ Application_Start():

// Configure anti-forgery to use our custom claim that exists for both auth methods System.Web.Helpers.AntiForgeryConfig.UniqueClaimTypeIdentifier = “ApplicationUserID”;

Why This Works

  1. Consistency: The ApplicationUserID claim is added by both authentication paths (EntraID in SecurityTokenValidated notification, Legacy in session-based login)
  2. Stability: Uses our application’s internal user ID rather than EntraID’s cryptographic subject identifier
  3. Compatibility: Maintains backward compatibility with existing forms throughout the application
  4. Simplicity: Single configuration line solves the problem for all forms system-wide

Alternative Approach (Not Used)

We initially attempted to ensure ClaimTypes.NameIdentifier was present by removing EntraID’s auto-generated NameIdentifier and replacing it with our user ID:

// In SecurityTokenValidated notification var existingNameIdClaim = identity.FindFirst(ClaimTypes.NameIdentifier); if (existingNameIdClaim != null) { identity.RemoveClaim(existingNameIdClaim); } identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserID.ToString()));

While this approach worked, configuring AntiForgeryConfig.UniqueClaimTypeIdentifier is cleaner because:

  • It’s explicit about which claim to use
  • It doesn’t modify EntraID’s standard claims
  • It’s easier to understand and maintain

Key Takeaway

When implementing SSO in legacy ASP.NET MVC applications:

Always configure AntiForgeryConfig.UniqueClaimTypeIdentifier to use a custom claim that exists in all authentication scenarios. This prevents anti-forgery token errors when mixing authentication methods during migration.

This small configuration detail is easy to miss but critical for production stability.

πŸ”§ Files Modified

Configuration

  1. Startup.cs

    • Added ConfigureAuth(app) call at beginning of Configuration
    • EntraID authentication now initialized before other services
  2. Web.config

    • Added UseLegacyAuthentication setting (currently hybrid)
    • EntraID settings present (ClientId, Tenant, Authority, redirectUri)

Controllers

  1. Controllers\AccountController.cs
    • Login() GET - Triggers EntraID authentication challenge
    • Login(LoginViewModel) POST - Legacy login (only if enabled)
    • Logout() - Signs out from EntraID and local cookies
    • Added AccessDenied() and Error() action methods
    • Added using statements for OWIN Security and CurrentUser

Services

  1. LoginService.cs
    • Authenticate() - Added isLegacyAuth parameter
    • Supports both EntraID and legacy authentication
    • SuccessfulLogin() - Added authMethod parameter for audit logging
    • FailedLogin() - Added authMethod parameter

Repositories

  1. UserRepository.cs
    • Added FindByID(int userID) method
    • Used by CurrentUser helper to retrieve full user object

Views

  1. Views\Account\Login.cshtml
    • Shows EntraID redirect message when legacy auth disabled
    • Shows legacy login form only when UseLegacyAuthentication=true
    • Displays warning during transition period

πŸ†˜ Support & Troubleshooting

If Issues Occur During Testing

Quick Rollback:

<!-- Set in Web.config -->
<add key="UseLegacyAuthentication" value="true" />

Users can immediately login with username/password.

Common Issues:

  1. Anti-Forgery Token Errors

    • Verify AntiForgeryConfig.UniqueClaimTypeIdentifier = "ApplicationUserID" is set
    • Check that ApplicationUserID claim is being added in both auth paths
    • Clear browser cookies and re-authenticate
  2. Claims Not Found

    • Ensure SecurityTokenValidated notification is adding all custom claims
    • Verify user exists in database and is enabled
    • Check audit logs for authentication failures
  3. Redirect Loop

    • Verify redirectUri matches EntraID app registration
    • Check that cookies are being set correctly
    • Ensure no middleware is interfering with authentication