IMAP OAuth 2.0 Authorization in Exchange Online
This article shows how a Java-based client application can connect to an e-mail server via IMAP protocol after obtaining an OAuth 2.0 access token.
Join the DZone community and get the full member experience.
Join For FreeMicrosoft announced that starting October 2022, Basic authentication for specific protocols in Exchange Online would be considered deprecated and turned off gradually and randomly for certain tenants. As insightful details concerning this topic may be found in Resources items one and two, among these protocols, there are Exchange ActiveSync (EAS), POP, IMAP, Remote PowerShell, Exchange Web Services (EWS), Offline Address Book (OAB).
Consequently, customer applications leveraging Basic authentication towards Exchange Online as part of their business use cases need to replace it with Modern authentication — OAuth 2.0 token-based authorization — which no doubt has many benefits and improvements that help mitigate the former's risks.
The purpose of this article is to document and showcase how a Java-based client application can connect to an e-mail server via IMAP (or IMAPS) protocol using the JavaMail library after previously obtaining an OAuth 2.0 access token. The token retrieval is implemented in two different manners - by leveraging the OAuth 2.0 Resource Owner Password Credentials (ROPC) grant and using Microsoft Authentication Library (MSAL) for Java — thus letting developers choose the preferred way.
Assumptions
Prior to successfully running the sample code, the Exchange server set-up shall be fulfilled to require an OAuth 2.0 authentication mechanism. Details on how a system administrator may configure it can be found at least in Resources item seven. Moreover, significant gotchas described in Resources item eight proved to be very helpful. Briefly, when creating the service principal using PowerShell in Exchange Online, one shall use the ObjectId of the enterprise application as the ServiceId, as the application ObjectId is different from the enterprise application ObjectId.
Set-up
The proof of concept uses the following:
- Java 17
- Maven 3.6.3
- Spring Boot version 2.7.5
- JavaMail version 1.6.2
- MSAL4J version 1.13.2
Connecting via JavaMail
According to Oracle (Resources item 3), starting with version 1.5.2, JavaMail supports OAuth 2 authentication via the SASL XOAUTH2 mechanism. While IMAP and SMTP protocols are covered, POP3 is not. Nevertheless, the proof of concept in this article uses IMAP and demonstrates a simple use case — a session is created, then used to connect to a store to get a folder with a specified name.
Since the important aspect here is connecting via IMAP and authenticating using OAuth 2, a great deal of other use cases may be easily implemented as needed.
In order to accomplish the aimed scenario, a simple MailReader
component is created.
public class MailReader {
private final MailProperties mailProperties;
private final TokenProvider tokenProvider;
public MailReader(MailProperties mailProperties, TokenProvider tokenProvider) {
this.mailProperties = mailProperties;
this.tokenProvider = tokenProvider;
}
public Folder getFolder(String name) {
final Properties props = new Properties();
props.put("mail.debug", "true");
props.put("mail.store.protocol", "imaps");
props.put("mail.imaps.port", 993);
props.put("mail.imaps.ssl.enable", "true");
props.put("mail.imaps.starttls.enable", "true");
props.put("mail.imaps.auth.mechanisms", "XOAUTH2");
Session session = Session.getInstance(props);
try {
Store store = session.getStore();
store.connect(mailProperties.getHost(), mailProperties.getUser(), tokenProvider.getAccessToken());
return store.getFolder(name);
} catch (MessagingException e) {
throw new RuntimeException("Unable to connect to the default folder.", e);
}
}
}
getFolder()
function sets up the connection properties, connects, and returns the folder.
A brief comparison between Basic and OAuth 2.0 authentication methods is worth doing at this point. In order to connect, a client application uses an account set-up accordingly. Concerning the implementation, the differences between the two methods are minimal. For OAuth 2.0:
mail.imaps.auth.mechanisms
property shall be set to XOAUTH2.- The
password
parameter of theStore#connect()
method contains an access token instead of the configured password.
Retrieving an Access Token
For connecting to the store via IMAP(S) using OAuth 2.0 authentication, one shall first obtain an access token. In either of the two ways — ROPC grant or MSAL — the client application needs the following pieces of information whatsoever:
- The authentication URL.
- The tenant identifier — the tenant directory the user is logged into.
- The client identifier — the Application (client) ID, the portal page assigned to the application.
- The client's secret, required if the application is a confidential client.
The two ways of retrieving the token are described in the next sections. In this direction, this proof of concept defines the following interface.
public interface TokenProvider {
String getAccessToken();
}
And then, for each of them, an implementation is coded.
Using Resource Owner Password Credentials (ROPC) Grant
The ROPC flow is pretty straightforward; it requires a single HTTP POST call toward the authorization endpoint that corresponds to the particular tenant. The request shall contain the client identifier, the client secret, the scope (usually https://outlook.office365.com/.default), and the 'client_credentials' or 'password' grant type. The response contains the access token necessary to connect afterward.
The implementation uses a WebClient
instance to perform the POST call.
public class RopcTokenProvider implements TokenProvider {
private final MailProperties mailProperties;
private final WebClient client;
public RopcTokenProvider(MailProperties mailProperties) {
this.mailProperties = mailProperties;
client = WebClient.builder()
.baseUrl(mailProperties.getAuthUrl())
.build();
}
@Override
public String getAccessToken() {
MultiValueMap<String, String> bodyValues = new LinkedMultiValueMap<>();
bodyValues.add("client_id", mailProperties.getClientId());
bodyValues.add("client_secret", mailProperties.getClientSecret());
bodyValues.add("scope", "https://outlook.office365.com/.default");
bodyValues.add("grant_type", "client_credentials");
TokenDTO token = client.post()
.uri("/{tenantId}/oauth2/v2.0/token", mailProperties.getTenantId())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.accept(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromFormData(bodyValues))
.retrieve()
.bodyToMono(TokenDTO.class)
.block();
if (token == null) {
throw new RuntimeException("Unable to retrieve OAuth2 access token.");
}
return token.getAccessToken();
}
}
Two useful aspects are worth observing here:
- the endpoint URL is https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
- request content type is
x-www-form-urlencoded
A successful HTTP POST result has the following form:
{
"token_type": "Bearer",
"expires_in": 3599,
"ext_expires_in": 3599,
"access_token": "the access token"
}
And maybe unmarshalled into an object as the TokenDTO
below.
@Data
public class TokenDTO {
@JsonProperty("token_type")
private String tokenType;
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("expires_in")
private int expiresIn;
@JsonProperty("ext_expires_in")
private int extExpiresIn;
}
The important piece here is obviously the accessToken
.
Using Microsoft Authentication Library (MSAL)
In order to retrieve an access token using MSAL for Java, the client application classpath shall be enhanced with the following library:
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
In this case, getAccessToken()
method of the TokenProvider
implementation returns directly the token used to connect to the store.
public class MsalTokenProvider implements TokenProvider {
private final ConfidentialClientApplication confidentialClientApp;
private final Set<String> scopes;
public MsalTokenProvider(MailProperties mailProperties) throws MalformedURLException {
IClientCredential credential = ClientCredentialFactory.createFromSecret(mailProperties.getClientSecret());
final String authority = String.format("%s/%s",
mailProperties.getAuthUrl(), mailProperties.getTenantId());
confidentialClientApp = ConfidentialClientApplication.builder(mailProperties.getClientId(), credential)
.authority(authority)
.build();
scopes = Set.of("https://outlook.office365.com/.default");
}
@Override
public String getAccessToken() {
try {
ClientCredentialParameters parameters = ClientCredentialParameters
.builder(scopes)
.build();
return confidentialClientApp.acquireToken(parameters)
.join()
.accessToken();
} catch (Exception e) {
throw new RuntimeException("Unable to retrieve OAuth2 access token.", e);
}
}
}
Testing the Solutions
The pieces of information needed to authenticate that have been previously mentioned are set as application properties. Except for mail.host
and mail.authUrl
ones, the values shall be filled in with the actual values configured on the Exchange server.
mail.host = outlook.office365.com
mail.user = technically@correct.com
mail.authUrl = https://login.microsoftonline.com
mail.tenantId = tenantid
mail.clientId = clientid
mail.clientSecret = secret
If either of the following integration tests is run, details may be observed while connecting to the store. As mail.debug
session property was set to true
; the logs will depict these.
@SpringBootTest
class MailReaderTest {
@Autowired
@Qualifier("msalMailReader")
private MailReader msalMailReader;
@Autowired
@Qualifier("ropcMailReader")
private MailReader ropcMailReader;
private void getFolder(MailReader mailReader) {
final String name = "INBOX";
Folder folder = mailReader.getFolder("INBOX");
Assertions.assertNotNull(folder);
Assertions.assertEquals(name, folder.getFullName());
}
@Test
void getFolderMsal() {
getFolder(msalMailReader);
}
@Test
void getFolderRopc() {
getFolder(ropcMailReader);
}
}
What happens when connecting?
DEBUG: JavaMail version 1.6.2
DEBUG: successfully loaded resource: /META-INF/javamail.default.address.map
DEBUG: getProvider() returning javax.mail.Provider[STORE,imaps,com.sun.mail.imap.IMAPSSLStore,Oracle]
DEBUG IMAPS: mail.imap.fetchsize: 16384
DEBUG IMAPS: mail.imap.ignorebodystructuresize: false
DEBUG IMAPS: mail.imap.statuscachetimeout: 1000
DEBUG IMAPS: mail.imap.appendbuffersize: -1
DEBUG IMAPS: mail.imap.minidletime: 10
DEBUG IMAPS: enable STARTTLS
DEBUG IMAPS: closeFoldersOnStoreFailure
DEBUG IMAPS: trying to connect to host "outlook.office365.com", port 993, isSSL true
* OK The Microsoft Exchange IMAP4 service is ready. [TQBOADIAUABSADEANgBDAEEAMAAwADUANgAuAG4AYQBtAHAAcgBkADEANgAuAHAAcgBvAGQALgBvAHUAdABsAG8AbwBrAC4AYwBvAG0A]
A0 CAPABILITY
* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=XOAUTH2 SASL-IR UIDPLUS ID UNSELECT CHILDREN IDLE NAMESPACE LITERAL+
A0 OK CAPABILITY completed.
DEBUG IMAPS: AUTH: PLAIN
DEBUG IMAPS: AUTH: XOAUTH2
DEBUG IMAPS: protocolConnect login, host=outlook.office365.com, user=technically@correct.com, password=<non-null>
DEBUG IMAPS: AUTHENTICATE XOAUTH2 command trace suppressed
DEBUG IMAPS: AUTHENTICATE XOAUTH2 command result: A1 OK AUTHENTICATE completed.
A2 CAPABILITY
* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=XOAUTH2 SASL-IR UIDPLUS MOVE ID UNSELECT CLIENTACCESSRULES CLIENTNETWORKPRESENCELOCATION BACKENDAUTHENTICATE CHILDREN IDLE NAMESPACE LITERAL+
A2 OK CAPABILITY completed.
DEBUG IMAPS: AUTH: PLAIN
DEBUG IMAPS: AUTH: XOAUTH2
The OAuth 2.0 authentication is successful.
Conclusions
This article documents possible implementations that client applications shall accomplish in order to embrace the Modern authentication (OAuth 2.0) promoted by Microsoft and thus remain functional after Basic Authentication will have been turned off. While the connection via the JavaMail library does not imply major changes, the access token retrieval needs to be coded. Two different mechanisms were implemented, either by leveraging the Resource Owner Password Credentials grant or using Microsoft Authentication Library for Java.
The former is apparently more lightweight, as it is done via an HTTP POST call to the Authorization server. According to Microsoft, more secure alternatives are available and recommended (Resources item five).
The latter needs to include the msal4j library as part of the client application classpath; nevertheless, taking into account the recommendations, it seems the preferred manner of retrieving an access token.
Published at DZone with permission of Horatiu Dan. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments