Modernizing Enterprise Auth: Integrating Azure AD SSO into Legacy Systems

In the evolving landscape of enterprise software, managing identity is often the most significant friction point for both developers and end-users. At a mid-sized tech firm—let's call it TechFlow Solutions—we faced a classic architectural debt: our internal employee management system, PeopleHub, was stuck in the era of LDAP (Lightweight Directory Access Protocol) authentication.

PeopleHub is the backbone of our daily operations, handling everything from timesheets and leave management to HR records and performance reviews. Every employee interacts with it daily. And every day, they had to type in a username and password that was entirely separate from their Microsoft 365 credentials.

This is the story of how we modernized PeopleHub's authentication by integrating Azure Active Directory (Azure AD) Single Sign-On (SSO), eliminating password fatigue, and significantly improving our security posture.

The Problem: Living with LDAP

For years, PeopleHub authenticated users against an on-premises LDAP directory. The flow was straightforward: the user types their credentials into a login form, the backend opens an LdapConnection, binds with the provided credentials, and if the bind succeeds, the user is authenticated.

// The old way — LDAP bind authentication
using (var connection = new LdapConnection("corp.techflow.local"))
{
    connection.Bind(new NetworkCredential(username, password, "corp.techflow.local"));
    // If no exception, credentials are valid
    var employee = await dbContext.Employees
        .FirstOrDefaultAsync(e => e.UserName == username);
    // Create session...
}

This worked, but it came with a growing list of pain points:

  • Password fatigue: Employees already had Azure AD credentials for email, Teams, and SharePoint. A separate PeopleHub password meant yet another credential to remember.
  • IT support overhead: Password resets, account lockouts, and expired password tickets consumed a disproportionate amount of IT support time.
  • Security risks: Separate passwords encouraged password reuse. LDAP also transmitted credentials that needed careful handling.
  • Infrastructure dependency: The on-premises LDAP server was a single point of failure. VPN was required for remote workers to authenticate.

Why Azure AD SSO?

The decision to integrate Azure AD was driven by a simple observation: everyone at TechFlow already has an Azure AD account. Microsoft 365 is our productivity suite. Every employee signs in to their machine, opens Outlook, joins Teams meetings—all through Azure AD.

By leveraging Azure AD SSO for PeopleHub, we could:

  • Eliminate separate credentials: One identity across all systems
  • Enable true SSO: If you're already signed into your browser with your Microsoft account, PeopleHub login is a single click with zero password typing
  • Leverage MFA: Azure AD's Multi-Factor Authentication policies apply automatically
  • Remove LDAP dependency: No more on-prem directory server for authentication
  • Improve security posture: Token-based flows, no password transmission

Technical Architecture: The OAuth2 Authorization Code Flow

Azure AD SSO uses the OAuth2 Authorization Code flow, which is the industry standard for server-side web applications. Here's the complete flow we implemented:

BROWSER (Vue.js) PEOPLEHUB Backend API AZURE AD OAuth2 / OIDC MS GRAPH API DATABASE Employee DB 1. Click SSO 2. Return auth URL (MSAL) 3. Redirect to Azure AD login 4. User signs in (or auto-SSO) 5. Redirect back with ?code=xxx 6. Forward code 7. Exchange code for token 8. Access token 9. GET /me (Bearer token) 10. User profile (email, name) 11. Find employee by email 12. Employee record 13. Set cookie + return user 14. Redirect to dashboard ✓

The beauty of this flow is that the client secret never leaves the server. The frontend only handles redirects and the authorization code—it never sees tokens or secrets.

Key Implementation Details

1. Azure AD App Registration

Before writing any code, you need to register your application in Azure AD:

  • Navigate to Azure Portal → Azure Active Directory → App Registrations
  • Create a new registration with your redirect URIs
  • Note down the Client ID, Tenant ID, and create a Client Secret
  • Configure API permissions: User.Read is sufficient for basic profile info

2. Backend: MSAL Integration

We used MSAL (Microsoft Authentication Library) on the backend. MSAL is available for .NET, Node.js, Python, and Java. Here's the .NET implementation:

// Configuration
public class AzureAdConfig
{
    public string TenantId { get; set; }
    public string ClientId { get; set; }
    public string ClientSecret { get; set; }
    public string Instance { get; set; } = "https://login.microsoftonline.com/";
    public string[] Scopes { get; set; } = new[] { "User.Read" };
}

// Building the MSAL confidential client
var app = ConfidentialClientApplicationBuilder
    .Create(config.ClientId)
    .WithAuthority($"{config.Instance}{config.TenantId}")
    .WithClientSecret(config.ClientSecret)
    .Build();

Generating the Login URL

// GET /api/auth/login-url
public async Task<string> GetLoginUrl(string redirectUri)
{
    var authUrl = await _msalClient
        .GetAuthorizationRequestUrl(config.Scopes)
        .WithRedirectUri(redirectUri)
        .ExecuteAsync();
    
    return authUrl.ToString();
}

Exchanging the Code for a Token

// GET /api/auth/callback?code=xxx
public async Task<UserSession> HandleCallback(string code, string redirectUri)
{
    // 1. Exchange authorization code for token
    var result = await _msalClient
        .AcquireTokenByAuthorizationCode(config.Scopes, code)
        .ExecuteAsync();

    // 2. Call MS Graph to get user profile
    var httpClient = new HttpClient();
    httpClient.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Bearer", result.AccessToken);
    
    var graphResponse = await httpClient.GetAsync("https://graph.microsoft.com/v1.0/me");
    var userData = await graphResponse.Content.ReadFromJsonAsync<GraphUser>();

    // 3. Match to internal employee database
    var email = userData.Mail ?? userData.UserPrincipalName;
    var username = email.Contains("@") ? email.Split('@')[0] : email;
    
    var employee = await _dbContext.Employees
        .FirstOrDefaultAsync(e => e.UserName == username);

    if (employee == null)
        throw new UnauthorizedException("User not found in PeopleHub");

    if (!employee.IsActive)
        throw new UnauthorizedException("Account is inactive");

    // 4. Create session (same cookie-based approach as before)
    return new UserSession(employee);
}

3. Frontend: From Form to Button

The frontend change was surprisingly minimal. We replaced the entire username/password form with a single button:

<!-- BEFORE: Traditional login form -->
<form @submit="handleLogin">
  <input v-model="username" placeholder="Username" />
  <input v-model="password" type="password" placeholder="Password" />
  <button type="submit">Sign In</button>
</form>

<!-- AFTER: SSO button -->
<button @click="handleSsoLogin" :disabled="isLoading">
  <microsoft-icon />
  Sign in with Microsoft
</button>

The JavaScript logic handles two scenarios: initiating the login and processing the callback:

// auth.service.js
export default {
  getLoginUrl: () => api.get('/api/auth/login-url'),
  handleCallback: (code) => api.get(`/api/auth/callback?code=${code}`),
}

// login.vue
mounted() {
  const code = new URLSearchParams(window.location.search).get('code');
  if (code) {
    this.handleCallback(code);
  }
},
methods: {
  async handleSsoLogin() {
    const { url } = await AuthService.getLoginUrl();
    window.location.href = url;
  },
  async handleCallback(code) {
    const { user } = await AuthService.handleCallback(code);
    this.$store.commit('SET_USER', user);
    this.$router.push('/dashboard');
  }
}

4. Session Management: Unchanged

One of our best decisions was to keep the existing cookie-based session management. After Azure AD authentication, the backend creates the exact same cookie session as before:

var claims = new List<Claim>
{
    new Claim("username", employee.UserName),
    new Claim("resourceId", employee.Id.ToString()),
    new Claim("isAdmin", employee.IsAdmin ? "1" : "0"),
};

var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(identity));

This meant zero changes to the authorization middleware, permission checks, or any downstream code that relied on the cookie. The entire existing codebase continued to work exactly as before—only the how of authentication changed, not the what.

Before & After

AspectBefore (LDAP)After (Azure AD SSO)
Login UIUsername + Password formSingle "Sign in with Microsoft" button
Credential managementSeparate PeopleHub passwordExisting Microsoft 365 account
SSO experienceNone — always type credentialsAuto-login if already signed into Microsoft
MFANot supportedInherited from Azure AD policies
Password resetsIT support ticketsSelf-service via Microsoft
InfrastructureOn-prem LDAP server requiredCloud-based, no VPN needed
Session managementCookie-basedCookie-based (unchanged)

Lessons Learned

1. Get the App Registration Ready Early

The Azure AD app registration is a prerequisite for all development. You need the Client ID, Tenant ID, and Client Secret before you can write a single line of auth code. Request this from your Azure AD administrator as the very first step.

2. Redirect URI Mismatches Are the #1 Debugging Headache

Azure AD is extremely strict about redirect URIs. The URI in your code must exactly match what's registered in the app registration—including trailing slashes, HTTP vs HTTPS, and port numbers. We lost hours to a missing trailing slash. Pro tip: log the exact redirect URI your code is generating and compare it character-by-character with the registered one.

3. Consider a Transition Period

We initially planned a hard cutover from LDAP to SSO. Instead, we kept both login methods available during a two-week transition period. This was invaluable for catching edge cases and giving users time to adjust. The LDAP form was hidden behind a "Use legacy login" link at the bottom of the page.

4. Test with Both Valid and Invalid Users

Not everyone with an Azure AD account is necessarily a PeopleHub user. Contractors, external collaborators, or recently departed employees might have Azure AD access but no corresponding record in your internal database. Make sure your error messages are clear and actionable.

5. Mind the Scopes

Only request the Azure AD scopes you actually need. User.Read is sufficient for getting the user's email and display name. Adding unnecessary scopes like Directory.Read.All requires admin consent and raises security review flags.

6. MSAL Handles the Hard Parts

Don't try to implement the OAuth2 flow manually with raw HTTP calls. MSAL handles token caching, refresh tokens, authority validation, and protocol nuances. It's available for every major platform and is actively maintained by Microsoft.

Conclusion

Integrating Azure AD SSO into PeopleHub was one of those rare infrastructure changes where the impact was immediately felt by every user. The day we deployed, the IT support queue for password resets dropped by over 40%. Users stopped complaining about "yet another password." And the security team finally had confidence that MFA was enforced across all internal systems.

If your organization uses Microsoft 365 and still has internal systems with separate login credentials, Azure AD SSO integration should be near the top of your technical debt backlog. The OAuth2 Authorization Code flow is well-documented, MSAL makes the implementation straightforward, and the ROI in terms of user experience and security is substantial.

The hardest part isn't the code—it's getting the app registration right and managing the redirect URIs. Once those are in place, the actual integration is surprisingly elegant.

← Back to Blog