Integrating Microsoft OAuth and Graph API with NestJS

Ojangole JordanOjangole Jordan
7 min read

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

  1. Go to the Microsoft Entra Id Portal

  2. Navigate to "App Registrations"

  3. Click "New Registration"

  4. 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)

  5. After registration, note down:

    • Application (client) ID

    • Directory (tenant) ID

    • Create a client secret in "Certificates & secrets"

  6. Under "API Permissions", add the following Microsoft Graph permissions:

     Calendars.ReadWrite
     OnlineMeetings.ReadWrite
     User.Read
     offline_access
     openid
     profile
    

Project Setup

  1. Install required dependencies:
npm install @microsoft/microsoft-graph-client @azure/msal-node
  1. 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 flow

  • Returns 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 authenticated

  • Returns 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:

  1. Token Management

    • Every function that makes API calls uses ensureValidToken

    • Tokens are automatically refreshed when expired

    • Secure storage in database

  2. Error Handling

    • Each function includes try-catch blocks

    • Specific error types for different scenarios

    • Detailed error messages for debugging

  3. Type Safety

    • Strong typing for all parameters and return values

    • Interface definitions for API responses

    • Null checking for optional values

  4. 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:

  1. User initiates auth → generateAuthUrl

  2. User authenticates → handleOAuthCallback

  3. Make API calls → createCalendarEvent (and others)

  4. Behind the scenes → ensureValidToken and refreshAccessToken

Usage

  1. Initialize the OAuth flow by redirecting users to the auth URL:
const { url } = await microsoftService.generateAuthUrl(userId);
// Redirect user to url
  1. Handle the OAuth callback:
// In your callback route
const result = await microsoftService.handleOAuthCallback(code, state);
  1. 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

  1. Always store tokens securely in your database

  2. Use environment variables for sensitive credentials

  3. Implement proper token refresh mechanisms

  4. Validate state parameter to prevent CSRF attacks

  5. Use HTTPS in production

  6. 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

0
Subscribe to my newsletter

Read articles from Ojangole Jordan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ojangole Jordan
Ojangole Jordan