Integrating Microsoft OAuth and Graph API with NestJS

Introduction
In this guide, I'll walk through the process of integrating Microsoft OAuth authentication and the Microsoft Graph API into a NestJS application. This integration will allow your application to authenticate users with their Microsoft accounts and interact with Microsoft services like Calendar, Teams, and more.
Prerequisites
A NestJS application
Node.js and npm installed
A Microsoft Azure account
Basic understanding of OAuth 2.0 flow
Setting Up Microsoft Azure
Go to the Microsoft Entra Id Portal
Navigate to "App Registrations"
Click "New Registration"
Fill in the application details:
Name: Your app name
Supported account types: "Accounts in any organizational directory and personal Microsoft accounts"
Redirect URI:
http://localhost:3000/api/integrations/microsoft/oauth/callback
(adjust for your domain)
After registration, note down:
Application (client) ID
Directory (tenant) ID
Create a client secret in "Certificates & secrets"
Under "API Permissions", add the following Microsoft Graph permissions:
Calendars.ReadWrite OnlineMeetings.ReadWrite User.Read offline_access openid profile
Project Setup
- Install required dependencies:
npm install @microsoft/microsoft-graph-client @azure/msal-node
- Add environment variables to your
.env
file:
MICROSOFT_CLIENT_ID=your_client_id
MICROSOFT_CLIENT_SECRET=your_client_secret
MICROSOFT_TENANT_ID=your_tenant_id
MICROSOFT_REDIRECT_URI=your_redirect_uri
Implementation
1. Create the Microsoft Service
Create a new service file microsoft.service.ts
:
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Client } from '@microsoft/microsoft-graph-client';
import { ConfidentialClientApplication } from '@azure/msal-node';
@Injectable()
export class MicrosoftService {
private msalClient: ConfidentialClientApplication;
constructor(private prisma: PrismaService) {
this.msalClient = new ConfidentialClientApplication({
auth: {
clientId: process.env.MICROSOFT_CLIENT_ID!,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
authority: 'https://login.microsoftonline.com/common',
},
});
}
private getGraphClient(accessToken: string) {
return Client.init({
authProvider: (done) => {
done(null, accessToken);
},
});
}
async generateAuthUrl(employeeId: string) {
const state = employeeId; // Used to identify the user after OAuth
const authCodeUrlParameters = {
scopes: [
'https://graph.microsoft.com/Calendars.ReadWrite',
'https://graph.microsoft.com/OnlineMeetings.ReadWrite',
'https://graph.microsoft.com/User.Read',
'offline_access',
'openid',
'profile',
],
redirectUri: process.env.MICROSOFT_REDIRECT_URI,
state,
};
const url = await this.msalClient.getAuthCodeUrl(authCodeUrlParameters);
return { url };
}
async handleOAuthCallback(code: string, state: string) {
try {
const tokenResponse = await this.msalClient.acquireTokenByCode({
code,
scopes: [
'https://graph.microsoft.com/Calendars.ReadWrite',
'https://graph.microsoft.com/OnlineMeetings.ReadWrite',
'https://graph.microsoft.com/User.Read',
'offline_access',
'openid',
'profile',
],
redirectUri: process.env.MICROSOFT_REDIRECT_URI,
});
if (!tokenResponse) {
throw new Error('No token response received');
}
const employeeId = state;
// Store tokens in database
const employee = await this.prisma.employee.update({
where: { id: employeeId },
data: {
microsoftAccessToken: tokenResponse.accessToken,
microsoftRefreshToken: tokenResponse.refreshToken,
microsoftTokenExpiry: new Date(tokenResponse.expiresOn!),
},
});
return { success: true, employee };
} catch (error) {
console.error('OAuth callback error:', error);
throw new BadRequestException('Failed to exchange authorization code');
}
}
private async ensureValidToken(employeeId: string) {
const employee = await this.prisma.employee.findUnique({
where: { id: employeeId },
});
if (!employee?.microsoftAccessToken) {
throw new UnauthorizedException('No Microsoft account connected');
}
if (
employee.microsoftTokenExpiry &&
new Date(employee.microsoftTokenExpiry) <= new Date()
) {
return await this.refreshAccessToken(employeeId);
}
return employee.microsoftAccessToken;
}
async refreshAccessToken(employeeId: string) {
const employee = await this.prisma.employee.findUnique({
where: { id: employeeId },
});
if (!employee?.microsoftRefreshToken) {
throw new UnauthorizedException('No refresh token found');
}
try {
const tokenResponse = await this.msalClient.acquireTokenByRefreshToken({
refreshToken: employee.microsoftRefreshToken,
scopes: [
'https://graph.microsoft.com/Calendars.ReadWrite',
'https://graph.microsoft.com/OnlineMeetings.ReadWrite',
'https://graph.microsoft.com/User.Read',
],
});
if (!tokenResponse) {
throw new Error('Failed to refresh token');
}
await this.prisma.employee.update({
where: { id: employeeId },
data: {
microsoftAccessToken: tokenResponse.accessToken,
microsoftTokenExpiry: new Date(tokenResponse.expiresOn!),
},
});
return tokenResponse.accessToken;
} catch (error) {
throw new UnauthorizedException('Failed to refresh access token');
}
}
async createCalendarEvent(employeeId: string, data: {
summary: string;
startTime: Date;
endTime: Date;
description?: string;
}) {
const accessToken = await this.ensureValidToken(employeeId);
const graphClient = this.getGraphClient(accessToken);
const event = {
subject: data.summary,
body: {
contentType: 'HTML',
content: data.description || '',
},
start: {
dateTime: data.startTime.toISOString(),
timeZone: 'UTC',
},
end: {
dateTime: data.endTime.toISOString(),
timeZone: 'UTC',
},
};
try {
const response = await graphClient.api('/me/events').post(event);
return response;
} catch (error) {
console.error('Failed to create calendar event:', error);
throw new BadRequestException('Failed to create calendar event');
}
}
}
2. Create the Controller
Create a new controller file microsoft.controller.ts
:
import { Controller, Get, Query, UseGuards, Request } from '@nestjs/common';
import { MicrosoftService } from './microsoft.service';
import { AuthGuard } from '../auth/auth.guard';
@UseGuards(AuthGuard)
@Controller('integrations/microsoft')
export class MicrosoftController {
constructor(private readonly microsoftService: MicrosoftService) {}
@Get('auth-url')
async getAuthUrl(@Request() req) {
return this.microsoftService.generateAuthUrl(req.user.id);
}
@Get('oauth/callback')
async handleOAuthCallback(
@Query('code') code: string,
@Query('state') state: string,
) {
return this.microsoftService.handleOAuthCallback(code, state);
}
}
3. Update Database Schema
Add Microsoft-related fields to your Employee model in your Prisma schema:
model Employee {
// ... other fields
microsoftAccessToken String?
microsoftRefreshToken String?
microsoftTokenExpiry DateTime?
}
Function Explanations
Let's break down each function in the Microsoft service and understand what they do:
Constructor and Initialization
constructor(private prisma: PrismaService) {
this.msalClient = new ConfidentialClientApplication({
auth: {
clientId: process.env.MICROSOFT_CLIENT_ID!,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
authority: 'https://login.microsoftonline.com/common',
},
});
}
Initializes the MSAL (Microsoft Authentication Library) client
Sets up the authentication configuration with your Azure app credentials
The
authority
URL specifies that we're using the common endpoint that works for both personal and organizational accounts
Graph Client Setup
private getGraphClient(accessToken: string) {
return Client.init({
authProvider: (done) => {
done(null, accessToken);
},
});
}
Creates a new Microsoft Graph client instance
Configures the client with the provided access token
This client is used for making API calls to Microsoft services
Authentication URL Generation
async generateAuthUrl(employeeId: string) {
const state = employeeId;
const authCodeUrlParameters = {
scopes: [...],
redirectUri: process.env.MICROSOFT_REDIRECT_URI,
state,
};
const url = await this.msalClient.getAuthCodeUrl(authCodeUrlParameters);
return { url };
}
Generates the OAuth authorization URL that users will be redirected to
Includes necessary scopes for accessing Microsoft services
Uses the
state
parameter to track which employee initiated the auth flowReturns the URL where users will authenticate with their Microsoft account
OAuth Callback Handler
async handleOAuthCallback(code: string, state: string) {
// ... implementation
}
Handles the response from Microsoft after user authentication
Exchanges the authorization code for access and refresh tokens
Stores the tokens in the database for the employee
The
state
parameter helps identify which employee is being authenticatedReturns a success response with the updated employee data
Token Validation
private async ensureValidToken(employeeId: string) {
// ... implementation
}
Checks if the employee has a valid access token
Verifies if the current token has expired
Automatically refreshes the token if needed
Returns a valid access token for API calls
Throws an error if no Microsoft account is connected
Token Refresh
async refreshAccessToken(employeeId: string) {
// ... implementation
}
Retrieves the refresh token from the database
Uses the refresh token to obtain a new access token
Updates the database with the new access token and expiry
Handles errors if the refresh fails
Returns the new access token
Calendar Event Creation
async createCalendarEvent(employeeId: string, data: {...}) {
// ... implementation
}
Creates a new event in the user's Microsoft calendar
Requires a valid access token (obtained through
ensureValidToken
)Formats the event data according to Microsoft Graph API requirements
Handles timezone conversion to UTC
Returns the created event data or throws an error if creation fails
Common Patterns
Throughout these functions, you'll notice several important patterns:
Token Management
Every function that makes API calls uses
ensureValidToken
Tokens are automatically refreshed when expired
Secure storage in database
Error Handling
Each function includes try-catch blocks
Specific error types for different scenarios
Detailed error messages for debugging
Type Safety
Strong typing for all parameters and return values
Interface definitions for API responses
Null checking for optional values
Security
State parameter validation
Secure token storage
Environment variable usage for sensitive data
These functions work together to provide a complete OAuth flow and API integration:
User initiates auth →
generateAuthUrl
User authenticates →
handleOAuthCallback
Make API calls →
createCalendarEvent
(and others)Behind the scenes →
ensureValidToken
andrefreshAccessToken
Usage
- Initialize the OAuth flow by redirecting users to the auth URL:
const { url } = await microsoftService.generateAuthUrl(userId);
// Redirect user to url
- Handle the OAuth callback:
// In your callback route
const result = await microsoftService.handleOAuthCallback(code, state);
- Make Graph API calls:
const event = await microsoftService.createCalendarEvent(userId, {
summary: 'Team Meeting',
startTime: new Date('2024-02-01T10:00:00Z'),
endTime: new Date('2024-02-01T11:00:00Z'),
description: 'Monthly team sync',
});
Error Handling
The implementation includes error handling for common scenarios:
Invalid or expired tokens
Failed token refresh
Missing permissions
API errors
Security Considerations
Always store tokens securely in your database
Use environment variables for sensitive credentials
Implement proper token refresh mechanisms
Validate state parameter to prevent CSRF attacks
Use HTTPS in production
Implement proper session management
Conclusion
This integration provides a robust foundation for working with Microsoft services in your NestJS application. The implementation handles authentication, token management, and provides a clean interface for making Graph API calls.
Remember to:
Keep your tokens secure
Handle errors appropriately
Implement proper logging
Add rate limiting for production use
Consider implementing token caching for better performance
Resources
Subscribe to my newsletter
Read articles from Ojangole Jordan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
