Issue on using outlook api for mails and getting 401

Perssonify Dev 0 Reputation points
2025-08-07T05:37:24.38+00:00

We are working on integrating the Outlook API so that client emails can be managed directly from our platform. While users and application endpoints are working as expected (with permissions enabled in the scope during access token generation), we’re running into an issue with the /me/messages and /users/{user-id}/messages endpoint, which is returning a 401 error.

Specifically, the email accounts connected via the users module in Azure are not able to list emails, even though authentication and other endpoints work fine. We suspect this may be related to scopes or a required service not being enabled, but we haven’t been able to identify the root cause.

Could you please provide a detailed list of all prerequisites and settings required to use the Outlook API for managing emails from our platform? Do the email accounts we’re connecting need to have Microsoft 365 enabled for full access?

For context, the app registration we are using is called ‘Outlook RnD v3’. I’ve also attached the Node.js code we’re using for access token generation and for attempting to list emails.

Please let us know what steps we should take, and any additional configurations or permissions we might be missing.

const express = require("express");
const msal = require("@azure/msal-node");
require("dotenv").config();

const app = express();
const PORT = 3000;
const config = {
  auth: {
    clientId: process.env.CLIENT_ID,
    authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`,
    clientSecret: process.env.CLIENT_SECRET,
  },
  redirectUri: "http://localhost:3000/auth/callback",
};

const msalConfig = {
  auth: {
    clientId: config.auth.clientId,
    authority: config.auth.authority,
    clientSecret: config.auth.clientSecret,
  },
};

let accessToken = null;

const pca = new msal.ConfidentialClientApplication(msalConfig);

app.get("/auth", (req, res) => {
  const authUrlParams = {
    scopes: [
      "Mail.Read",
      "Mail.ReadWrite",
      "Mail.Send",
      "Mail.ReadBasic",
      "Mail.Read.Shared",
      "Mail.ReadWrite.Shared",
      "Mail.Send.Shared",
      "Mail.ReadWrite",
      // "MailboxFolder.Read",
      // "MailboxFolder.ReadWrite",
      // "MailboxFolder.Read.All",
      // "MailboxFolder.ReadWrite.All",
      // "MailboxItem.Read",
      // "MailboxItem.Read.All",
      "openid",
      "profile",
      "offline_access",
      "User-Mail.ReadWrite.All"
    ],
    redirectUri: config.redirectUri,
  };
  pca
    .getAuthCodeUrl(authUrlParams)
    .then((response) => {
      res.redirect(response);
    })
    .catch((error) => res.send(error));
});

app.get("/auth/callback", async (req, res) => {
  const tokenRequest = {
    code: req.query.code,
    scopes: [
      "Mail.Read",
      "Mail.ReadWrite",
      "Mail.Send",
      "Mail.ReadBasic",
      "Mail.Read.Shared",
      "Mail.ReadWrite.Shared",
      "Mail.Send.Shared",
      "Mail.ReadWrite",
      // "MailboxFolder.Read",
      // "MailboxFolder.ReadWrite",
      // "MailboxFolder.Read.All",
      // "MailboxFolder.ReadWrite.All",
      // "MailboxItem.Read",
      // "MailboxItem.Read.All",
      "openid",
      "profile",
      "offline_access",
      "User-Mail.ReadWrite.All"
    ],
    redirectUri: config.redirectUri,
  };

  console.log("Received auth code:", req.query.code);

  try {
    const response = await pca.acquireTokenByCode(tokenRequest);
    console.log("Token acquired successfully");
    console.log("Access Token:", response.accessToken);
    console.log("Expires On:", response.expiresOn);
    console.log(
      "Expiry in seconds:",
      new Date(response.expiresOn).getTime() / 1000 -
        Math.floor(Date.now() / 1000)
    );
    console.log("Scopes:", response.scopes);

    accessToken = response.accessToken;
    // req.session.accessToken = response.accessToken;
    res.redirect("/mails");
  } catch (error) {
    res.send(
      `<p>Auth failed: ${error.message}</p><a href="/auth">Try logging in again</a>`
    );
  }
});


const { Client } = require("@microsoft/microsoft-graph-client");
require("isomorphic-fetch");

app.get("/mails", async (req, res) => {
  if (!accessToken) {
    console.log("Access token is not available");
    return res
      .status(401)
      .send("Access token is not available. Please authenticate first.");
  }

  const client = Client.init({
    authProvider: (done) => {
      done(null, accessToken);
    },
  });

  try {
    // const mails = await client
    //   .api("/me/messages")
    //   .top(10)
    //   .get();
    const mails = await client.api('/users/' + process.env.SENDER_EMAIL + '/messages').top(10).get();
    res.json(mails.value);
  } catch (error) {
    console.error("Error fetching mails:", error);
    res.send(error);
  }
});

app.listen(PORT, () =>
  console.log(`Server running on http://localhost:${PORT}`)
);

Outlook | Web | Outlook on the web for business | Email
0 comments No comments
{count} votes

2 answers

Sort by: Most helpful
  1. AlexDN 1,855 Reputation points Microsoft External Staff Moderator
    2025-08-07T06:52:06.68+00:00

    Hi @Perssonify Dev
    Thank you for posting your question in the Microsoft Q&A forum.

    This is required setup for Outlook API Integration:

    1/ Azure AD App Registration:

    • Ensure your app is registered in Azure AD with the correct redirect URI and client secret.
    • For personal Microsoft accounts, use the /common endpoint and enable multi-tenant access.

    2/ Microsoft Graph API Permissions:

    • Use delegated permissions like Mail.Read, Mail.ReadWrite, Mail.Send, and offline_access.
    • For shared mailboxes, include Mail.Read.Shared and Mail.ReadWrite.Shared.
    • Admin consent is required for high-privilege scopes like Mail.ReadWrite.All.

    3/ Mailbox Licensing:

    • Each user must have a Microsoft 365 license with Exchange Online.
    • Without a mailbox, Graph API calls will fail even if permissions are correctly configured.

    4/ Endpoint Usage:

    • Use /me/messages for delegated tokens.
    • Use /users/{id}/messages only with application permissions and client credentials flow.

    5/ MSAL Configuration:

    • Ensure scopes match those in Azure AD.
    • Implement token refresh logic to avoid expired token errors.

    For common causes of 401 Errors:

    • Missing Exchange Online mailbox.
    • Incorrect or missing OAuth scopes.
    • Admin consent not granted for required permissions.
    • Using the wrong endpoint for the token type.
    • Guest or external account without mailbox access.
    • Expired or misconfigured access token.

    Based on your description, I recommend as below:

    • Use Microsoft Graph Explorer or Postman to test API calls.
    • Decode access tokens to verify scopes.
    • Simplify scope requests to essential permissions.
    • Consider application permissions for managing multiple users’ emails via client credentials flow.

    By aligning your app configuration, permissions, and licensing with these guidelines, you should be able to resolve the 401 error and ensure smooth email management via the Outlook API.

    Note: Please understand that our initial response does not always resolve the issue immediately. However, with your help and more detailed information, we can work together to find a solution. Your detailed response will help us diagnose and investigate the issue more efficiently. If I misunderstood what you’re looking for, feel free to let me know or share a screenshot. I’d be happy to help further!

    Thank you for your cooperation. I'm looking forward for your reply.


    If the answer is helpful, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".   

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread. 


  2. AlexDN 1,855 Reputation points Microsoft External Staff Moderator
    2025-08-08T02:59:42.44+00:00

    Hi @Perssonify Dev

    Thank you for your thoughtful message and for sharing the results of your testing with Microsoft Graph Explorer and Postman. Your observations are insightful and highlight several important distinctions in how access tokens are issued and validated across different authentication flows and account types.

    Based on your description, please find below a detailed response to your questions:

    1. Differences in Token Behavior: Graph Explorer vs. App Registration

    Graph Explorer operates as a Microsoft-hosted multi-tenant application using delegated OAuth 2.0 flows. To replicate its behavior in your own application:

    • Ensure your app registration supports “Accounts in any organizational directory and personal Microsoft accounts”.
    • Use the /common or /consumers authority in your OAuth flow to support personal Microsoft accounts.
    • Request delegated permissions such as Mail.Read, Mail.Send, and offline_access, and ensure user consent is properly obtained.
    • Use the v2.0 endpoint to acquire a JWT token with the correct audience (aud: graph.microsoft.com) and scopes.

    For detailed guidance, please refer to:

    2. Personal vs. Work/School Accounts – Endpoint and Permission Differences

    There are indeed key differences between personal Microsoft accounts (MSA) and Azure AD work/school accounts:

    • Personal accounts only support delegated permissions. App-only (client credentials) tokens are not supported.
    • Work/school accounts support both delegated and application permissions.
    • The /me/messages endpoint works for both account types, while /users/{user-id}/messages is only applicable in organizational contexts.

    For further reading: Get access on behalf of a user

    3. Recommended OAuth 2.0 Token Flows

    Delegated Access (for /me/messages), here’s a typical flow using the Authorization Code grant:

    Step 1: App Registration Configuration
    Register an app in Azure AD (App Registrations). Set the Supported account types to include the account types you need (e.g., “Accounts in any organizational directory and personal Microsoft accounts” if you plan to support both). Configure a Redirect URI that your app will use to receive the OAuth code (for example, http://localhost:3000/auth/callback for a dev app). Step 2: Add Delegated Permissions Under API Permissions for the app, add the delegated Microsoft Graph permissions your application needs. For email read/write access, add permissions like Mail.Read (read user mail), Mail.ReadWrite (read and modify mail), and Mail.Send if sending email. Also include offline_access (to get refresh tokens) and basic openid/profile if you need user info. After adding, grant admin consent for these if any require it (in many cases, Mail.Read/.ReadWrite for user mail can be consented by the user themselves). Ensure no Application permissions are added here – we only need delegated ones for this flow. Step 3: User Authorization (OAuth Code Request) Direct the user to the Microsoft identity platform authorize endpoint. For example: GET https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=YourAppID&response_type=code&redirect_uri=YourRedirectURI&response_mode=query&scope=Mail.Read%20Mail.ReadWrite%20Mail.Send%20offline_access%20openid%20profile&state=12345 Use common in the URL to allow both personal and work accounts. The user will be prompted to sign in and consent to the requested scopes. After consent, Azure will redirect back to your provided redirect URI with a ?code=... parameter (and the given state for verification). Step 4: Token Exchange Your app server exchanges the authorization code for tokens by POSTing to the token endpoint. For example, to https://login.microsoftonline.com/common/oauth2/v2.0/token with form data including:

    • client_id and client_secret (or client assertion if using certificate).
    • grant_type=authorization_code, code=the_auth_code_received, redirect_uri=YourRedirectURI.
    • scope=Mail.Read Mail.ReadWrite Mail.Send offline_access openid profile (the same scopes, or you can omit scopes here to default to those requested). Azure AD will respond with an access token (and typically a refresh token and ID token, since openid was requested). The access token is a bearer token to call Graph. Verify that the token’s scope (scp claim) includes the mail permissions. Step 5: Call the Graph API Attach the access token in the HTTP Authorization header and call the Graph endpoint.
    • Use the Authorization Code flow with PKCE or client secret.
    • Include scopes such as Mail.Read, offline_access, openid, and profile.
    • Exchange the authorization code for an access token via the /token endpoint.

    Application Access (for /users/{user-id}/messages):

    • Use the Client Credentials flow.

    Step 1: App Registration for App-Only Use an Azure AD app registration (you can use the same one as above or a separate one) that is in an Azure AD tenant (personal accounts cannot consent to app-only). The app can be single-tenant if you only need it in your organization, or multi-tenant if you plan to grant other orgs access. Ensure you have a client secret or certificate set up for authentication, since this is a server-to-server scenario. Step 2: Add Application Permissions In Azure AD, go to API Permissions for the app and add application permissions for Microsoft Graph. For reading user mail across the organization, add for example Mail.Read.All (or Mail.ReadWrite.All if you need write access) under Application permissions. Also consider if you need to send mail on behalf of any mailbox – that would require Mail.Send as an application permission. After adding, an administrator must grant admin consent for these permissions (the portal has a “Grant admin consent” button). Only global admin or certain privileged admin roles can do this approval. Once granted, your app has tenant-wide permission to read mail in any mailbox. Step 3: Acquire Token via Client Credentials Unlike the delegated flow, no interactive user login occurs here. Your server requests a token from Azure AD using its own credentials. If the request is correct and admin consent was granted, Azure AD will return an access token (no refresh token in client credentials). This token will have a roles claim (for app permissions) instead of scopes. For example, roles might include "Mail.Read.All". Step 4: Call Graph API with App Token Using the token obtained, you can call Graph endpoints that require application permissions. To list a specific user’s messages, call: GET https://graph.microsoft.com/v1.0/users/{user-id}/messages with header Authorization: Bearer YOUR_APP_ACCESS_TOKEN. Here, {user-id} can be the user’s Azure AD object ID or email/UPN. The token grants your app access to that mailbox without that user being signed in. If the token is valid and the mailbox exists, you’ll get a list of messages.

    Reference:

    4. Troubleshooting and Best Practices

    • Use Graph Explorer and Postman to Compare Tokens: Graph Explorer is great for debugging. Compare token contents: check iss and tid. -
      • Personal accounts: tid = 9188040d... (consumer directory)
      • Work accounts: specific tenant GUID
      • scp or roles show permissions, make sure they match your app’s needs.
      • If token is opaque, infer scopes from successful calls (e.g., /me/messages => Mail.Read).
    • Testing with Graph SDKs and Postman.
      • Graph Explorer uses the same API as your app. If it works there but not in your app, it’s likely a permissions or account context issue.
      • Use Postman with Graph Explorer’s client ID and auth code flow.
        Input all OAuth details carefully.
        Test with both personal and work accounts to compare behavior.
    • Mailboxes and Licensing: 401/403 errors often mean the user has no mailbox.
      • Work accounts: check Exchange license in admin portal.
      • Personal accounts: ensure Outlook mailbox exists.
    • Endpoint Tips:
      • Delegated tokens: use/me/messages
      • Application tokens: use/users/{id}/messages
      • Use v1.0 for production.
      • Match token audience to Graph API, not Outlook REST.

    Note: Please understand that our initial response does not always resolve the issue immediately. However, with your help and more detailed information, we can work together to find a solution.

    Thank you for your cooperation. I'm looking forward for your reply.


    If the answer is helpful, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".   

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread. 


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.