ews-javascript-api icon indicating copy to clipboard operation
ews-javascript-api copied to clipboard

Error SoapFaultDetails message: '401 Unauthorized',

Open fgisslen opened this issue 3 years ago • 8 comments
trafficstars

Hi, I have used this package for a while and everything has been working perfectly. Until now.

With exactly the same setup I nowadays get the response: Error SoapFaultDetails { message: '401 Unauthorized', InnerException: null, faultCode: null, faultString: null, faultActor: null, responseCode: 127, errorCode: 0, exceptionType: null, lineNumber: 0, positionWithinLine: 0, errorDetails: DictionaryWithStringKey { keys: [], keysToObjs: {}, objects: {}, keyPicker: [Function (anonymous)] }, HttpStatusCode: 401, Exception: ServiceRequestUnauthorizedException { message: '401 Unauthorized', InnerException: null } }

Here is my setup:

const EXCHANGE_URL = 'https://outlook.office365.com/EWS/Exchange.asmx'; const ewsLogin = config.exchange.login; const ewsPassword = config.exchange.password; const service = new ews.ExchangeService(ews.ExchangeVersion.Exchange2013); service.Credentials = new ews.WebCredentials(ewsLogin, ewsPassword); service.TraceEnabled = true; service.TraceFlags = ews.TraceFlags.All;

const email = new ews.EmailMessage(service); email.ToRecipients.Add('[email protected]'); email.Subject = 'xxx'; email.Body = new ews.MessageBody('xxxx'); email.Body.BodyType = ews.BodyType.Text; email.Send() .then((val) => { console.log("Sent"); }) .catch((e) => { console.log("Error ", e); });

Please help me :P

fgisslen avatar Oct 21 '22 08:10 fgisslen

Microsoft no longer allows to use basic auth.

https://techcommunity.microsoft.com/t5/exchange-team-blog/basic-authentication-deprecation-in-exchange-online-september/ba-p/3609437

some way to extend it for some time. https://practical365.com/exchange-online-basic-authentication/

gautamsi avatar Oct 21 '22 08:10 gautamsi

Thanks @gautamsi , what would you recommend that I should do instead of basic auth?

fgisslen avatar Oct 21 '22 09:10 fgisslen

you have to use microsoft guide to get the oauth refresh and access token then pass access token to this library.

gautamsi avatar Nov 03 '22 17:11 gautamsi

Hi Everyone,

Please find the solution we used to fix the same problem.

The solution consist of 3 parts:

  1. Setup application in AZURE.
  2. Add code to get accessToken.
  3. Made changes in old code to use previously received accessToken.

zhukovsv avatar Nov 16 '22 14:11 zhukovsv

Part 1:

  1. Login to MS AZURE portal.
  2. Open "App registration" tool: image
  3. Click "New Registration": image
  4. Setup new App: image
  5. After you click registrate button you will receive smtg like this: image
  6. Open API permissions tab for previously created App + click Add permission and select MS Graph: image
  7. Select Delegated permissions: image
  8. Find User section and select User.Read + Add permission click: image
  9. Add a permission again + APIs my organizaton uses tab(or find it) and find Office 365 Exchange Online: image
  10. Find EWS section and select EWS.AccessAsUser.All and click Add permissons: image
  11. Go to Authentication tab and click Add platform: image
  12. Select Mobile and Desctop apps and click Save button: image
  13. Select to options and click Configure: image

image 14. Also on Authentication tab set "Supported accounts types" and "Allow public client flows" and click Save: image 15. Go to Overview tab you should see smthg like this: clientID tenantId image 16. 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 17. After the opening previously generated link you should login and then receive another link in browser which shoud contains generated accessToken(secret): image 18. Now we can start add code allows us to get accessToken

zhukovsv avatar Nov 16 '22 15:11 zhukovsv

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;
}

zhukovsv avatar Nov 16 '22 15:11 zhukovsv

Part 3 - connect to emailbox: 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;
}

zhukovsv avatar Nov 16 '22 15:11 zhukovsv

Have a good day.

zhukovsv avatar Nov 16 '22 15:11 zhukovsv