I am using Java Mail to connect to a MS Outlook Mail Server. I can do so successfully using PLAIN authentication with a username and password. I have heard that Microsoft will disable Basic Authentication soon, so I want to upgrade to use OAuth2.0 rather.
I do the following using a username (email address) and password, which connects successfully. However, if I change it to use the the OAuth2 Token instead of the password to connect, it is unsuccessful.
Java Code
Gets the token, which looks like a valid token:
public String getAccessTokenByClientCredentialGrant() {
String accessToken = null;
final String clientId = AZURE_CLIENT_ID; // "<client id from azure app registration>"
final String secret = AZURE_CLIENT_SECRET_VALUE; // "<client secret from azure app registration>" - client secret value
final String authority = "https://login.microsoftonline.com/" AZURE_TENANT_ID "/oauth2/v2.0/token"; // "https://login.microsoftonline.com/<tenant-id from azure>/oauth2/v2.0/token"; or https://login.microsoftonline.com/{tenant}/v2.0/adminconsent?client_id=<CLIENT_ID>&redirect_uri=<REDIRECT_URI>&scope=https://ps.outlook.com/.default
final String scope = "https://outlook.office365.com/.default"; // "https://ps.outlook.com/.default";
try {
ConfidentialClientApplication app = ConfidentialClientApplication.builder(clientId, ClientCredentialFactory.createFromSecret(secret)).authority(authority).build();
// With client credentials flows the scope is ALWAYS of the shape "resource/.default", as the application permissions need to be set statically (in the portal), and then granted by a tenant administrator
ClientCredentialParameters clientCredentialParam = ClientCredentialParameters.builder(Collections.singleton(scope)).build();
CompletableFuture<IAuthenticationResult> future = app.acquireToken(clientCredentialParam);
IAuthenticationResult result = future.get();
accessToken = result.accessToken();
if (StringUtils.isBlank(accessToken)) {
logger.error("Access token: " accessToken);
}
} catch(Exception e) {
logger.error("Exception in acquiring token: " e.getMessage(), e);
}
logger.debug("Access Token : " accessToken);
return accessToken;
}
Get the connection fails:
public Store connect(String userEmailId, String oauth2AccessToken) throws Exception {
Store store = null;
final String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory";
Properties props = new Properties();
props.put("mail.imaps.ssl.enable", "true");
props.put("mail.imaps.sasl.enable", "true");
props.put("mail.imaps.port", port);
props.put("mail.imaps.host", host);
props.put("mail.imaps.protocol", "imap");
props.put("mail.imaps.user", userEmailId);
props.put("mail.imaps.auth.mechanisms", "XOAUTH2");
props.put("mail.imaps.sasl.mechanisms", "XOAUTH2");
props.put("mail.imaps.auth.login.disable", "true");
props.put("mail.imaps.auth.plain.disable", "true");
props.setProperty("mail.imaps.socketFactory.class", SSL_FACTORY);
props.setProperty("mail.imaps.socketFactory.fallback", "true");
props.setProperty("mail.imaps.socketFactory.port", port);
props.setProperty("mail.imaps.starttls.enable", "true");
props.put("mail.debug", "true");
props.put("mail.debug.auth", "true");
Session session = Session.getInstance(props);
session.setDebug(true);
try {
store = session.getStore("imaps");
logger.info("OAUTH2 IMAP [" store.toString() "] connect with system properties to Host:" host ", Port: " port ", userEmailId: " userEmailId ", OAuth2AccessToken: " oauth2AccessToken);
Integer iPort = Integer.parseInt(port);
store.connect(host, iPort, userEmailId, oauth2AccessToken);
logger.info("OAUTH2 IMAP connected with system properties to Host:" host ", Port: " port ", userEmailId: " userEmailId ", OAuth2AccessToken: " oauth2AccessToken);
if(store.isConnected()){
logger.info("Connection Established using imap protocol successfully !");
} else {
logger.info("Connection not Established using imap protocol");
}
} catch (Exception e) {
logger.error("Store.Connect failed with the error: " e.getMessage());
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
String exceptionAsString = sw.toString();
logger.error(exceptionAsString);
}
return store;
}
Output
However, when I try to access the mailbox via OAuth2.0, it still fails authentication.
DEBUG: JavaMail version 1.6.2
DEBUG: successfully loaded resource: /META-INF/javamail.default.address.map
DEBUG: setDebug: JavaMail version 1.6.2
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: enable SASL
DEBUG IMAPS: SASL mechanisms allowed: XOAUTH2
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. [xxx==]
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, [email protected], password=<non-null>
DEBUG IMAPS: SASL Mechanisms:
DEBUG IMAPS: XOAUTH2
DEBUG IMAPS:
DEBUG IMAPS: SASL client XOAUTH2
DEBUG IMAPS: SASL callback length: 2
DEBUG IMAPS: SASL callback 0: javax.security.auth.callback.NameCallback@2ec9d28c
DEBUG IMAPS: SASL callback 1: javax.security.auth.callback.PasswordCallback@7a98e6b5
A1 AUTHENTICATE XOAUTH2 xxx
A1 NO AUTHENTICATE failed.
javax.mail.AuthenticationFailedException: AUTHENTICATE failed.
at com.sun.mail.imap.IMAPStore.protocolConnect(IMAPStore.java:732)
Configuration
I have managed to assign FULLACCESS access rights via Powershell to the outlook mailbox (
Question
I have not developed with Microsoft products in the past, so I am a bit in the dark here. Does anyone have any ideas please?
Update
Thank you to user2250152 for their answer below. I have changed the scope to:
final String scope = "https://graph.microsoft.com/.default";
and get the same error as above. However, when I change it to:
final String scope = "user.read.all";
I get a different error:
MsalServiceException: AADSTS1002012: The provided value for scope openid profile offline_access user.read.all is not valid. Client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI).
How do I know what to set the scope
to?
Further Update
Thanks to Glen Scales below for his answer. If I change the scope
to:
final String scope = "https://outlook.office365.com/.default";
and then use jwt.io to view the token, it shows that the user has access to:
"roles": [
"User.Read.All",
"Mail.ReadWrite",
"POP.AccessAsApp",
"User.ReadBasic.All",
"Mail.Read",
"IMAP.AccessAsApp"
],
Which corresponds to the Azure API permissions:
However, when I try access it via IMAP, it still gets the same error as above:
A1 NO AUTHENTICATE failed.
I am not sure why the IMAP attempted connection fails. So on his suggestion I think I need to figure out how to connect using Graph instead of IMAP.
CodePudding user response:
If you want to get access on behalf of a user then the scope must be a space-separated list of the Microsoft Graph permissions that you want the user to consent to.
final String scope = "user.read imap.accessasuser.all etc.";
If you want to get access without user (daemon app) then the value passed for the scope parameter in auth request should be https://graph.microsoft.com/.default
. This value informs the Microsoft identity platform endpoint to include in the access token all the app-level permissions the admin has consented to.
final String scope = "https://graph.microsoft.com/.default";
Authorization oauth v2 service
CodePudding user response:
= "https://graph.microsoft.com/.default";
this isn't the correct scope to use for IMAP the Graph does not provide IMAP access so using that will always generate a token with an incorrect audience that will fail when you try to authenticate against Exchange.
for the client credentials flow scope = "https://outlook.office365.com/.default" is the correct scope. The first place to start is check your token in jwt.io and make look at what permissions are being included in the returned token (the are listed under roles).
Even with the correct permission and scope IMAP can be disabled at the mailbox level (and it recommended to do so) eg https://help.fookes.com/support/solutions/articles/9000213843-cannot-connect-to-microsoft-365-account-via-imap#:~:text=To allow IMAP access on,and enable IMAP from there.
If your building a new application don't use IMAP it's legacy and has many security issues. You can use the Graph just as easily and it will be guaranteed that your app will work without problem in the future (or at least you can get support if it doesn't). Your IMAP code as with what happened with basic authentication maybe discontinued or disabled (by Microsoft of the client/company) because of the associated security issues at any point in the very near future. (this is just IMO)