I’m developing a Next.JS application that integrates Microsoft Calendar and Teams using Microsoft Graph API.
Users authenticate through the OAuth 2.0 v2 endpoints, and we store their access and refresh tokens for later API calls.
Our .env setup looks like this:
MICROSOFT_OAUTH_URL=https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
MICROSOFT_TOKEN_URL=https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
Our Azure App Registration supports:
✅ Accounts in any organizational directory and personal Microsoft accounts
We request the following scopes:
Calendars.ReadWrite email OnlineMeetings.ReadWrite openid profile User.Read
After the user signs in, we store:
Then, when we need to interact with Microsoft Graph, we create the client like this:
import { Client } from "@microsoft/microsoft-graph-client";
const getGraphClient = async (accessToken: string) => {
return Client.init({
authProvider: (done) => {
done(null, accessToken); // token retrieved from DB
},
});
};
// Example usage:
const client = await getGraphClient(userIntegration.accessToken);
const event = await client.api("/me/events").post(newEvent);
We do not manually prepend Bearer, since the SDK handles that automatically.
This results into a 401 with an empty body.
But when we try the /me endpoint, we get a successful response:
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
"businessPhones": [],
"displayName": "...",
"givenName": "...",
"jobTitle": null,
"mail": null,
"mobilePhone": null,
"officeLocation": null,
"preferredLanguage": "en",
"surname": "...",
"userPrincipalName": "..._gmail.com#EXT#@...gmail.onmicrosoft.com",
"id": "...5cda28fc39d"
}
Token Details
{
"aud": "00000003-0000-0000-c000-000000000000",
"scp": "Calendars.ReadWrite email OnlineMeetings.ReadWrite openid Presence.Read profile User.Read",
"iss": "https://sts.windows.net/958a6836-8d13-4822-a86a-a2ead1649e66/",
"upn": "...._gmail.com#EXT#@....gmail.onmicrosoft.com",
"appid": "f9c4afbd-bd65-455d-877d-6013f29a7e22",
"name": "...."
}
So this is a Gmail-based personal Microsoft account which is my account and actually also has an associated outlook and teams account. But some how was automatically turned into a #EXT# guest user in a shadow tenant.
What’s strange
/me works perfectly using the same access token./me and /me/events).aud) matches Graph (00000003-0000-0000-c000-000000000000)./oauth2/v2.0/...).I am actually new to this. But it seems that Graph Explorer can automatically route to the consumer Outlook.com calendar backend, while custom app registrations use something different. Where this Gmail-based account is only a guest user (#EXT#) and doesn’t have a mailbox.
This problem happened because my app was using a specific tenant ID in OAuth URLs. Personal Microsoft accounts, like Gmail-based ones cannot access calendar data through tenant-specific endpoint.
To fix this, I had to change my your .env to use the /common endpoint:
MICROSOFT_OAUTH_URL=https://login.microsoftonline.com/common/oauth2/v2.0/authorize
MICROSOFT_TOKEN_URL=https://login.microsoftonline.com/common/oauth2/v2.0/token
So in short, when you use a tenant-specific endpoint, Microsoft Graph looks for the user within that particular Azure AD tenant. Since my account was a personal Microsoft account and not part of that tenant, the request couldn’t find it and returned a 401 error.
Link to answer from Microsoft