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:
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.Readis 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
| Aspect | Before (LDAP) | After (Azure AD SSO) |
|---|---|---|
| Login UI | Username + Password form | Single "Sign in with Microsoft" button |
| Credential management | Separate PeopleHub password | Existing Microsoft 365 account |
| SSO experience | None — always type credentials | Auto-login if already signed into Microsoft |
| MFA | Not supported | Inherited from Azure AD policies |
| Password resets | IT support tickets | Self-service via Microsoft |
| Infrastructure | On-prem LDAP server required | Cloud-based, no VPN needed |
| Session management | Cookie-based | Cookie-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.