Sitemap

Implementing Microsoft OAuth2 Login in Node.js (TypeScript + Express)

6 min readOct 24, 2025

Hello all, I’m here after months. I thought to write about a challenge that I faced a few years back and yesterday I was able to accomplish it within two hours.

Press enter or click to view image in full size

When I was working at the ADL, I got to develop MSAL auth for one of our projects with Springboot, but since my bread and butter is MERN, it took a few days to finish, and it was a kinda challenge for me. Whatever ins’t harder. I’ll show you how I do that for some of my MERN projects with TypeScript.

Let’s dive into the job.

First of all, you need to have a simple knowledge of Azure and the MERN stack. Then, I’ll simply explain ns how to set up Microsoft login for your web app using MSAL (Microsoft Authentication Library), validate users by email domain, and redirect them to your frontend dashboard with a signed JWT token.

Create an Azure App

Go to Azure Portal → App registrations → New registration

then,

Create an app with a simple name. Then it will ask you to add a Redirect URI. So, choose a web app and keep it as this example.

http://localhost:3000/v1/api/auth/redirect

Then click Register.

Then copy these values.

  • Application (client) ID
  • Directory (tenant) ID
  • Client secret (create one under “Certificates & Secrets”)

NodeJS and Express Project

I believe you know how to work on simple NodeJS and Express backend source code. So, first, you can create a

.env such as

PORT=3000
FRONTEND_URL=http://localhost:5173/
MS_CLIENT_ID=3ea8232d30-eba0-4258e-b8f1-216d22wew5b98
MS_CLIENT_SECRET=your_client_secret_here
MS_TENANT_ID=your_tenant_id_here
MS_REDIRECT_URI=http://localhost:3000/v1/api/auth/redirect
JWT_SECRET=test_secret_key_here
JWT_EXPIRATION=3600

Okay then, inside the src, I’ll create a file structure like below.

project-root/

├── src/
│ ├── config/
│ │ └── msalConfig.ts # MSAL (Microsoft Auth) setup
│ │
│ ├── controllers/
│ │ └── auth.controller.ts # Login + Redirect handlers
│ │
│ ├── middlewares/
│ │ └── authMiddleware.ts # (Optional) JWT verification later
│ │
│ ├── routes/
│ │ └── auth.routes.ts # Defines /login and /redirect routes
│ │
│ ├── utils/
│ │ ├── tokenService.ts # JWT signing / verifying helpers
│ │ └── logger.ts # Optional: central console logging
│ │
│ ├── app.ts # Express app setup
│ └── server.ts # Entry point (starts the server)

├── .env # Environment variables
├── tsconfig.json # TypeScript compiler settings
├── package.json
├── package-lock.json
└── README.md

So, let’s create a config file config/msalConfig.ts:

This file connects your app to Azure AD.
You import ConfidentialClientApplication from @azure/msal-node and give it your app’s credentials: the Client ID, Tenant ID, and Client Secret. These are the three things you get from the Azure portal when you register your app.

import { ConfidentialClientApplication } from '@azure/msal-node';

export const msalClient = new ConfidentialClientApplication({
auth: {
clientId: process.env.MS_CLIENT_ID!,
authority: `https://login.microsoftonline.com/${process.env.MS_TENANT_ID}`,
clientSecret: process.env.MS_CLIENT_SECRET!,
},
});

Then let’s build controllers/auth.controller.ts

1. Login Handler — Opening the Microsoft Login

Think of the login function as the doorman of your application.
When a user clicks “Sign in with Microsoft,” this function builds a special URL that points them to Microsoft’s secure login page. It uses Microsoft’s MSAL library to create this link and includes a few important permissions (called scopes) like user.read and email,These tell Microsoft what basic information your app wants to access once the user signs in.

When this URL is ready, the backend doesn’t try to handle login directly — instead, it simply redirects the user to Microsoft’s authentication page. From there, Microsoft takes care of the heavy lifting: verifying the account, checking passwords, and handling all security protocols.

In short, this function says:

“Hey Microsoft, I’ve got a user who wants to sign in. Here’s where to send them after you’re done.”

That “after you’re done” part is handled by our redirect handler, which is where the real magic happens next.

2. Redirect Handler — Verifying and Welcoming the User

Once Microsoft finishes signing the user in, it sends them back to your backend’s /redirect endpoint along with a special one-time authorization code.
The redirect function is the one waiting at the door to handle this code.

Here’s what it does, step by step:

  1. Token Exchange:
    It takes the authorization code Microsoft provided and exchanges it for an access token. This token proves the user’s identity and gives permission to access their Microsoft profile data.
  2. Get User Details:
    Using that access token, it calls Microsoft’s Graph API (https://graph.microsoft.com/v1.0/me) to get the user’s information — their name, email, and other details.
  3. Validate Email Domain:
    Once it gets the user’s email, it checks if the domain (like @outlook.com or @your_org.com) is allowed. This is how you can control who’s allowed to log in.
    For example, if your app is meant only for your company, you could allow only @your_org.com.
  4. Generate JWT:
    If the email is valid, it creates a JWT (JSON Web Token) — a small, signed data packet that confirms who the user is. This token will be used later on the frontend to keep the user logged in without calling Microsoft again.
  5. Redirect to Frontend:
    Finally, it redirects the user to your frontend dashboard, attaching the JWT as a URL parameter. The frontend can then grab it and store it in localStorage for authenticated access.
import { Request, Response } from 'express';
import { msalClient } from '../config/msalConfig';
import jwt from 'jsonwebtoken';
import axios from 'axios';

const redirectUri = process.env.MS_REDIRECT_URI!;

// 1️⃣ LOGIN HANDLER
export const login = async (req: Request, res: Response) => {
try {
console.log('[LOGIN] Generating Microsoft auth URL...');
const authUrl = await msalClient.getAuthCodeUrl({
scopes: ['user.read', 'openid', 'profile', 'email'],
redirectUri,
});
res.redirect(authUrl);
} catch (error: unknown) {
if (error instanceof Error) {
console.error('[LOGIN ERROR]', error.message);
}
res.status(500).json({ error: 'Failed to initiate Microsoft login' });
}
};

// Allowed email domains
const allowedDomains = [
'outlook.com',
'gmail.com',
'your_org.com',
];

// 2️⃣ REDIRECT HANDLER
export const redirect = async (req: Request, res: Response) => {
console.log('--- MSAL REDIRECT HANDLER START ---');
console.log('[QUERY PARAMS]', req.query);

try {
// Exchange code for tokens
const tokenResponse = await msalClient.acquireTokenByCode({
code: req.query.code as string,
scopes: ['user.read', 'email', 'openid', 'profile'],
redirectUri,
});

console.log('[REDIRECT] Access token acquired.');

// Get user data from Microsoft Graph API
const graphResponse = await axios.get('https://graph.microsoft.com/v1.0/me', {
headers: { Authorization: `Bearer ${tokenResponse.accessToken}` },
});

const user = graphResponse.data;
const userEmail = user.mail || user.userPrincipalName;
console.log('[GRAPH] User Email:', userEmail);

if (!userEmail) return res.status(400).send('No valid email found');

const domain = userEmail.split('@')[1];
console.log('[VALIDATION] Domain:', domain);

if (!allowedDomains.includes(domain)) {
console.log('[VALIDATION] Access denied for domain:', domain);
return res.status(403).send('Unauthorized domain');
}

// Create JWT
const token = jwt.sign(
{ id: user.id, name: user.displayName, email: userEmail },
process.env.JWT_SECRET!,
{ expiresIn: parseInt(process.env.JWT_EXPIRATION!, 10) }
);

console.log('[SUCCESS] Redirecting to dashboard...');
res.redirect(`${process.env.FRONTEND_URL}dashboard?token=${token}`);

} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.error('[REDIRECT ERROR]', error.response?.data || error.message);
} else if (error instanceof Error) {
console.error('[REDIRECT ERROR]', error.message);
}
res.status(500).send('Authentication failed');
}
};

Then all the main functions are one. Let’s create routes.

inside routes/auth.routes.ts

import { Router } from 'express';
import { login, redirect } from '../controllers/auth.controller';

const router = Router();

router.get('/login', login);
router.get('/redirect', redirect);

export default router;

Then all are finished from our end and let’s integrate this with your React app.

  • Create a simple login page, then add a simple login button and set this API
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (token) {
localStorage.setItem('auth_token', token);
window.history.replaceState({}, '', '/dashboard');
}
}, []);

That’s almost done. But

  1. Once you are done with this, you will be redirected to the relevant page with a token with the URL such as
${process.env.FRONTEND_URL}dashboard?token=${token}

2. So, write a program in the frontend to save that in your local storage for reuse.

3. Then implement a middleware in the backend to validate that.

Then that’s all with this, and let’s catch up with another one later.

Let’s connect: lakindu.com

So, if you’re looking to build your next big product, we’re here for you with product engineering, AI, consulting and team augmentation. Please feel free to contact us and stay connected.

Contact Information:

--

--

Lakindu Widuranga Alwis
Lakindu Widuranga Alwis

Written by Lakindu Widuranga Alwis

Self-taught Full-Stack Developer, AI Researcher and Business Strategist turned Entrepreneur. CEO of Akwid Labs.