EWS Modern Authentication in Java

Is there an example on how to do “Modern Authentication” (Oauth) using the EWSClient in Java?

I implemented the solution at OAuth2 support for O365 - #8 by cap.aspose but I receive a timeout.

@mizehrer

You can use AzureROPCTokenProvider class described in the MS Graph.

Code sample to initialize credentials:

ITokenProvider provider = new AzureROPCTokenProvider(oauth2.Tenant, oauth2.ClientId, "", oauth2.userNameEmail, oauth2.userPassword, 
    new String[] { "https://outlook.office.com/EWS.AccessAsUser.All" });
NetworkCredential credentials = new OAuthNetworkCredential(oauth2.userNameEmail, provider);

Setting the Azure “Office 365 Exchange Online”/full_access_as_app permission:

  • Application/Manage/API permissions/ + Add a permission

  • Select “APIs my organization uses” tab and search for “Office 365 Exchange Online” as shown in this image : image1.png (24.1 KB)

  • Select “Application permissions” and enable “full_access_as_app” as shown in this image : image2.png (46.7 KB)

MS Documentation link:

Access Mail Services using OAuth

OAuth 2.0 support has been added to Aspose.Email and can be used to access SMTP, POP3, IMAP and EWS servers.
In general, all servers supporting OAuth 2.0 bearer tokens can be used with Aspose.Email, but our email clients have been tested with Google mail servers and Microsoft Office 365 servers.
Access to the server from the SmtpClient, Pop3Client, ImapClient and EWSClient with OAuth may be implemented in 2 ways.

  1. Provide access token directly into the constructor of email client. In this case, the user has to understand that lifetime of access tokens is limited. When the token is expired, email client can’t be used to access the server.
  2. Provide a custom implementation of token provider based on ITokenProvider interface into the constructor of email client. In this case, the client checks token expiration time and requests ITokenProvider for a new access token when the previous is expired. In this way, the client refreshes tokens periodically and may work with the server for unlimited time. Оften services support a simple way to refresh access tokens. For example, using refresh tokens in google services or ROPC authentication flow in Microsoft identity platform can be used for implementation token provider.

Configure an Account on the Appropriate Server

The following articles help you to configure accounts to access mail services.

Hope this helps you.

Hi, thank you for the hints. This is what I already implemented. If I use the example for the AzureROPCTokenProvider as is, I receive the following error:

java.lang.IllegalAccessError: Operation failed: 401/Unauthorized
Details:
{2}{"error":"invalid_client","error_description":"AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret'.\r\nTrace ID: 0c8b5597-e1f6-49f0-811a-7bc007c40a00\r\nCorrelation ID: 3af60d7d-1837-4f14-b149-7f1f1434dcfb\r\nTimestamp: 2022-08-03 10:40:06Z","error_codes":[7000218],"timestamp":"2022-08-03 10:40:06Z","trace_id":"0c8b5597-e1f6-49f0-811a-7bc007c40a00","correlation_id":"3af60d7d-1837-4f14-b149-7f1f1434dcfb","error_uri":"https://login.microsoftonline.com/error?code=7000218"}

If I add the client secret to the body parameters, I end up in a timeout with no useful information about the cause.

But this method (with adding the client_secret in the AzureROPCTokenProvider) works for IMAP using the Sign in to Outlook scope.

@mizehrer

We have logged this problem in our issue tracking system as EMAILJAVA-35085. You will be notified via this forum thread once this issue is resolved.

We apologize for your inconvenience.

@mizehrer

We suggest you to use MSAL Java lib to acquire Token:

https://mvnrepository.com/artifact/com.microsoft.azure/msal4j/1.13.2

Please check the following sample code from Microsoft site:

private static IAuthenticationResult acquireTokenUsernamePassword(PublicClientApplication pca,
        Set<String> scope,
        IAccount account,
        String username,
        String password) throws Exception {
    IAuthenticationResult result;
    try {
        SilentParameters silentParameters =
                SilentParameters
                        .builder(scope)
                        .account(account)
                        .build();
        // Try to acquire token silently. This will fail on the first acquireTokenUsernamePassword() call
        // because the token cache does not have any data for the user you are trying to acquire a token for
        result = pca.acquireTokenSilently(silentParameters).join();
        System.out.println("==acquireTokenSilently call succeeded");
    } catch (Exception ex) {
        if (ex.getCause() instanceof MsalException) {
            System.out.println("==acquireTokenSilently call failed: " + ex.getCause());
            UserNamePasswordParameters parameters =
                    UserNamePasswordParameters
                            .builder(scope, username, password.toCharArray())
                            .build();
            // Try to acquire a token via username/password. If successful, you should see
            // the token and account information printed out to console
            result = pca.acquireToken(parameters).join();
            System.out.println("==username/password flow succeeded");
        } else {
            // Handle other exceptions accordingly
            throw ex;
        }
    }
    return result;
}

public static String getAccessToken() throws Exception {
    Set<String> scope = new HashSet<String>();
    scope.add("https://outlook.office365.com/.default");
    String username = "test1@onmicrosoft.com";
    String password = "userPass";
    String clientId = "xxxxxb-f4be-4e2e-95dd-7aa4f5dxxxxx";
    String tenantId = "xxxxx65f-f7e3-4bc3-841f-13b29xxxxx";
    String authority =
            "https://login.microsoftonline.com/" + tenantId + "/oauth2/v2.0/token";
    PublicClientApplication pca = PublicClientApplication.builder(clientId)
            .authority(authority)
            .build();

    //Get list of accounts from the application's token cache, and search them for the configured username
    //getAccounts() will be empty on this first call, as accounts are added to the cache when acquiring a token
    Set<IAccount> accountsInCache = pca.getAccounts().join();
    IAccount account = getAccountByUsername(accountsInCache, username);

    //Attempt to acquire token when user's account is not in the application's token cache
    IAuthenticationResult result = acquireTokenUsernamePassword(pca, scope, account, username, password);
    System.out.println("Account username: " + result.account().username());
    System.out.println("Access token:     " + result.accessToken());
    System.out.println("Id token:         " + result.idToken());
    System.out.println();

    accountsInCache = pca.getAccounts().join();
    account = getAccountByUsername(accountsInCache, username);

    //Attempt to acquire token again, now that the user's account and a token are in the application's token cache
    result = acquireTokenUsernamePassword(pca, scope, account, username, password);
    System.out.println("Account username: " + result.account().username());
    System.out.println("Access token:     " + result.accessToken());
    System.out.println("Id token:         " + result.idToken());

    return "";
}

private static IAccount getAccountByUsername(final Set<IAccount> accountsInCache, final String username) {
    if (accountsInCache.isEmpty()) {
        System.out.println("==No accounts in cache");
    } else {
        System.out.println("==Accounts in cache: " + accountsInCache.size());
        for (IAccount account : accountsInCache) {
            if (account.username().equals(username)) {
                return account;
            }
        }
    }
    return null;
}