Search code examples
node.jsamazon-web-servicesazureazure-active-directorymicrosoft-graph-api

Cant access user calendar though permission granted, Issue with a Microsoft Graph API


I am trying to book an event in Outlook calendar through Graph API. I have created the application and granted the permission as shown in the screenshot. The application has both Delegated permission and Application permission Application Permission

Here is the code that I am using.

import axios from 'axios';
import querystring from 'querystring';

const tenantId = '123456'; // Your Directory (Tenant) ID
const clientId = '-558d64288141'; // Your Application (Client) ID
const clientSecret = 'ZXc8Q~t'; // Your Application Secret Value

async function getAccessToken() {
    const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
    const params = {
        grant_type: 'client_credentials',
        client_id: clientId,
        client_secret: clientSecret,
        scope: 'https://graph.microsoft.com/.default'
    };

    const response = await axios.post(tokenEndpoint, querystring.stringify(params), {
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    });

    return response.data.access_token;
}

async function bookCalendarEvent(accessToken, eventDetails) {
    const endpoint = 'https://graph.microsoft.com/v1.0/users/schedule@test.com/events'; // Updated endpoint
    
    const response = await axios.post(endpoint, eventDetails, {
        headers: {
            Authorization: `Bearer ${accessToken}`,
            'Content-Type': 'application/json'
        }
    });

    return response.data;
}

export const handler = async (event) => {
    try {
        // Get access token using client credentials
        const accessToken = await getAccessToken();

        // Sample event details
        const eventDetails = {
            subject: 'Meeting with Lambda',
            body: {
                contentType: 'HTML',
                content: 'This is a meeting scheduled by AWS Lambda'
            },
            start: {
                dateTime: '2024-08-27T09:00:00', // Sample start date and time
                timeZone: 'UTC'
            },
            end: {
                dateTime: '2024-08-27T10:00:00', // Sample end date and time
                timeZone: 'UTC'
            },
            attendees: [
                {
                    emailAddress: {
                        address: 'example@domain.com', // Sample attendee email
                        name: 'John Doe'
                    },
                    type: 'required'
                }
            ]
        };

        // Book the calendar event
        const result = await bookCalendarEvent(accessToken, eventDetails);

        return {
            statusCode: 200,
            body: JSON.stringify({
                message: 'Event booked successfully',
                eventId: result.id
            })
        };
    } catch (error) {
        console.error('Error booking calendar event:', error);
        return {
            statusCode: 500,
            body: JSON.stringify({
                message: 'Error booking event',
                error: error.message
            })
        };
    }
};

But when I run the AWS Lambda I get this error

{
  "statusCode": 500,
  "body": "{\"message\":\"Error booking event\",\"error\":\"Request failed with status code 403\"}"
}

So I am not sure what I am doing wrong Any help thanks


Solution

  • I agree with @juunas, you need to switch to delegated flows like authorization code flow to have access to only signed-in user's calendar.

    I registered one Azure AD application and granted Calendars.ReadWrite permission of Delegated type as below:

    enter image description here

    Now, I added redirect URI as http://localhost:3000/callback in Web platform of application like this:

    enter image description here

    In my case, I used below modified code that works with authorization code flow to acquire token and books event in signed-in user calendar:

    index.js:

    const express = require('express');
    const axios = require('axios');
    const querystring = require('querystring');
    
    const app = express();
    const port = 3000;
    
    let storedAccessToken = '';
    
    const tenantId = 'tenantID';
    const clientId = 'appID';
    const clientSecret = 'secret';
    const redirectUri = 'http://localhost:3000/callback';
    
    app.get('/login', async (req, res) => {
        const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?` + 
        querystring.stringify({
            client_id: clientId,
            response_type: 'code',
            redirect_uri: redirectUri,
            response_mode: 'query',
            scope: 'openid profile offline_access Calendars.ReadWrite',
            state: '12345'
        });
    
        const { default: open } = await import('open');
        await open(authorizationUrl);
    
        res.send('Opening Microsoft login page...');
    });
    
    app.get('/callback', async (req, res) => {
        const code = req.query.code;
    
        async function getAccessToken(code) {
            const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
    
            const response = await axios.post(tokenEndpoint, querystring.stringify({
                grant_type: 'authorization_code',
                client_id: clientId,
                client_secret: clientSecret,
                code: code,
                redirect_uri: redirectUri,
                scope: 'openid profile offline_access Calendars.ReadWrite'
            }), {
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
            });
    
            return response.data.access_token;
        }
    
        try {
            const accessToken = await getAccessToken(code);
            console.log('Access Token:', accessToken);
            storedAccessToken = accessToken;
            res.send('Login successful! You can now book an event.');
        } catch (error) {
            console.error('Error obtaining access token:', error.response ? error.response.data : error.message, error.stack);
            res.status(500).send('Error obtaining access token: ' + error.message);
        }
    });
    
    app.get('/book-event', async (req, res) => {
        try {
            if (!storedAccessToken) {
                return res.status(401).json({ message: 'User is not authenticated. Please login first.' });
            }
    
            console.log('Using Stored Access Token:', storedAccessToken);
    
            const eventDetails = {
                subject: 'Meeting with Lambda',
                body: {
                    contentType: 'HTML',
                    content: 'This is a meeting scheduled by AWS Lambda'
                },
                start: {
                    dateTime: '2024-08-27T09:00:00',
                    timeZone: 'UTC'
                },
                end: {
                    dateTime: '2024-08-27T10:00:00',
                    timeZone: 'UTC'
                },
                attendees: [
                    {
                        emailAddress: {
                            address: 'sridevi.maxxxxxxxxx@gmail.com',
                            name: 'Sridevi'
                        },
                        type: 'required'
                    }
                ]
            };
    
            const calendarEndpoint = `https://graph.microsoft.com/v1.0/me/events`; 
    
            const response = await axios.post(calendarEndpoint, eventDetails, {
                headers: {
                    Authorization: `Bearer ${storedAccessToken}`,
                    'Content-Type': 'application/json'
                }
            });
    
            res.json({ message: 'Event booked successfully', eventId: response.data.id });
        } catch (error) {
            console.error('Error booking event:', error.response ? error.response.data : error.message, error.stack);
            res.status(500).json({ message: 'Error booking event', error: error.message });
        }
    });
    
    app.listen(port, () => {
        console.log(`Server running on http://localhost:${port}`);
    });
    

    Now, run the code and visit http://localhost:3000/login in browser that asks user to login and gives code in address bar after completing authentication like this:

    enter image description here

    When I ran http://localhost:3000/book-event in browser, it booked the event in signed-in user's calendar successfully like this:

    enter image description here

    If you try to book event in different user's calendar, it will give 403 error as access will be denied for users other than signed-in user like this:

    enter image description here