Minhaj Hasan
Minhaj Hasan

Reputation: 55

node-ews returning 401 Unauthorized where as using the valid access token

I am using node-ews to fetch emails from the Microsoft Exchange server. It was working fine with basic auth.

But, as Microsoft disabled basic auth. We are currently using the OAuth token (access token) from Graph Explorer to test.

But it's returning 401 Unauthorised error.

This is the sample code we are using to connect to the exchange server.

const ewsConfig = {
            username: item.mail_username,
            password: item.user_pass,
            host: item.ews_host,
            token: 'xxxxxxxxxxx',
            auth: 'bearer'
          };

          // initialize node-ews
          const options = {
            rejectUnauthorized: false,
            strictSSL: false
          };
          // initialize node-ews
          const ews = new EWS(ewsConfig, options);

Upvotes: 0

Views: 1170

Answers (5)

zhukovsv
zhukovsv

Reputation: 11

Part 1-2 - continue:

  1. Find EWS section and select EWS.AccessAsUser.All and click Add permissons: step10_img
  2. Go to Authentication tab and click Add platform: step11_img
  3. Select Mobile and Desctop apps and click Save button: step12_img
  4. Select two options and click Configure: step13-1_img step13-2_img
  5. Also on Authentication tab set "Supported accounts types" and "Allow public client flows" and click Save: step14_img
  6. Go to Overview tab you should see smthg like this: clientID tenantId step15_img
  7. THIS STEP should be made BY EACH USER that WILL USE this API - use USER credentials to open this link (or YOUR ADMIN should make bulk apply). Check made changes by opening next link in browser in incognito mode(FOR each user):
https://login.microsoftonline.com/ADD YOUR TENANTID/oauth2/v2.0/authorize?
client_id=ADD YOUR CLIENTID
&response_type=code
&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient
&response_mode=query
&scope=EWS.AccessAsUser.All
&state=12345
  1. After the opening previously generated link you should login and then receive another link in browser which shoud contains generated code: step16_img
  2. Now we can start add code allows us to get accessToken

Upvotes: 0

zhukovsv
zhukovsv

Reputation: 11

Part 0 - Please find the solution we used to fix the same problem.

The solution consist of 3 parts:

  1. Setup application in AZURE that allows to generate MSAL-access token for EWS.
  2. Add code to get accessToken.
  3. Made changes in old code to use previously received accessToken. I am usind ews-javascript-api. But I think previouse two steps will help you to get accessToken for EWS and you can use it with node-EWS.

Sorry for 3 posts, but as a new user I have a restrictions it impossible for new users to create posts with more than 8 links and etc... )

Upvotes: 0

zhukovsv
zhukovsv

Reputation: 11

Part 1-1 - Setup application in AZURE that allows to generate MSAL-access token for EWS:

  1. Login to MS AZURE portal.
  2. Open "App registration" tool: step2_img
  3. Click "New Registration": step3_img
  4. Setup new App: step4_img
  5. After you click registrate button you will receive smtg like this: step5_img
  6. Open API permissions tab for previously created App + click Add permission and select MS Graph: step6_img
  7. Select Delegated permissions: step7_img
  8. Find User section and select User.Read + Add permission click: step8_img
  9. Add a permission again + APIs my organizaton uses tab(or find it) and find Office 365 Exchange Online: step9_img

Part-1-2 - continue...

Upvotes: 1

zhukovsv
zhukovsv

Reputation: 11

Part 2 - get accessToken by using userName + userPassword to email box:

import * as path from 'path';
import { ExchangeService, EmailMessage, MessageBody, OAuthCredentials, AutodiscoverService, Folder, Item, ExchangeVersion } from 'ews-javascript-api';

public async getEmailAccessToken(
       clientId: string,
        tenantId: string,
        emailUserName: string,
        emailUserPassword: string,
        cacheFilePath: string = `.${path.sep}tokenCache.json`) {

        const msal = require('@azure/msal-node');
        const { promises: fs } = require('fs');

        //Cache Plugin configuration         
        const beforeCacheAccess = async (cacheContext) => {
            try {
                const cacheFile = await fs.readFile(cacheFilePath, 'utf-8');
                cacheContext.tokenCache.deserialize(cacheFile);
            } catch (error) {
                // if cache file doesn't exists, create it
                cacheContext.tokenCache.deserialize(await fs.writeFile(cacheFilePath, ''));
            }
        };

        const afterCacheAccess = async (cacheContext) => {
            if (cacheContext.cacheHasChanged) {
                try {
                    await fs.writeFile(cacheFilePath, cacheContext.tokenCache.serialize());
                } catch (error) {
                    console.log(error);
                }
            }
        };

        const cachePlugin = {
            beforeCacheAccess,
            afterCacheAccess
        };

        const msalConfig = {
            auth: {
                clientId: clientId, // YOUR clientId
                authority: `https://login.microsoftonline.com/${tenantId}` // YOUR tenantId
            },
            cache: {
                cachePlugin
            },
            system: {
                loggerOptions: {
                    loggerCallback(loglevel, message, containsPii) {
                        console.log(message);
                    },
                    piiLoggingEnabled: false,
                    logLevel: msal.LogLevel.Verbose
                }
            }
        };

        const pca = new msal.PublicClientApplication(msalConfig);

        const msalTokenCache = pca.getTokenCache();

        const accounts = await msalTokenCache.getAllAccounts();

        // Acquire Token Silently if an account is present
        let accessToken = null;

        if (accounts.length > 0) {
            const silentRequest = {
                account: accounts[0], // Index must match the account that is trying to acquire token silently
                scopes: ['https://outlook.office365.com/EWS.AccessAsUser.All'],
            };

            const response = await pca.acquireTokenSilent(silentRequest);

            accessToken = response.accessToken;
        } else {
            // fall back to username password if there is no account
            const usernamePasswordRequest = {
                scopes: ['https://outlook.office365.com/EWS.AccessAsUser.All'],
                username: emailUserName, // Add your username here      
                password: emailUserPassword, // Add your password here
            };

            const response = await pca.acquireTokenByUsernamePassword(usernamePasswordRequest);

            accessToken = response.accessToken;
        }

        return accessToken;
    }

This method returns accessToken allows us to use EWS-api and also generates tokenCacheFile.json that will be used for silent usage in case of multiple calls.

Part 3 - connect to emailbox by using previously generated accessToken and ews-javascript-api :

import { ExchangeService, EmailMessage, MessageBody, OAuthCredentials, AutodiscoverService, Folder, Item, ExchangeVersion } from 'ews-javascript-api';

public async connectAndChangeAllEmailsFromBlaBla(
        clientId: string,
        tenantId: string,
        exchangeServiceUrl: string = 'https://outlook.office365.com/Ews/Exchange.asmx',
        emailUserName: string,
        emailUserPassword: string,
        searchMask: string = 'hasattachments:yes and from:[email protected] and received:today') {
        
        // get acces token by method written above in part 2
        const emailAccessToken = await this.getEmailAccessToken(clientId, tenantId, emailUserName, emailUserPassword);

        const ews = require('ews-javascript-api');
        const service = new ExchangeService(ews.ExchangeVersion.Exchange2013);

        // use emailAccesToken
        service.Credentials = new OAuthCredentials(emailAccessToken);

        service.Url = new ews.Uri(exchangeServiceUrl);

        const mailInbox = await ews.Folder.Bind(service, ews.WellKnownFolderName.Inbox);
        const loadPageSize = 1000; // 1 means load last email according to filter

        const view = new ews.ItemView(loadPageSize);
        view.PropertySet = new ews.PropertySet(ews.BasePropertySet.FirstClassProperties);
        let mailItems;
        // hasattachment:yes
        // isread:false
        // received:today or received:[date]
        mailItems = await mailInbox.FindItems(searchMask, view);
        console.log(`Emails were found before processing: ${mailItems.Items.length}`);

        for (const item of mailItems.Items) {
            // mark mail.item as read
            item.IsRead = true;
            await item.Update(1);
            // Do what you want
        }

        return mailItems.Items.length;
    }

Upvotes: 0

Glen Scales
Glen Scales

Reputation: 22032

. We are currently using the OAuth token (access token) from Graph Explorer to test.

The Graph Explorer token won't have permissions for EWS only Graph, the only two permission that are valid in EWS are EWS.AccessAsUser.All or full_access_as_app if using the client credentials flow. https://learn.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-authenticate-an-ews-application-by-using-oauth the Mail.Read etc permission don't work in EWS because it doesn't support the more restrictive authentication scheme that Graph supports (which is a reason to use the Graph over EWS)

If you want to accesstoken to test with use the EWSEditor https://github.com/dseph/EwsEditor/releases and grab its token

Upvotes: 1

Related Questions