Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth via ADFS with MFA support #134

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 89 additions & 58 deletions src/java/davmail/exchange/auth/O365Authenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,23 @@

package davmail.exchange.auth;

import davmail.BundleMessage;
import davmail.Settings;
import davmail.exception.DavMailAuthenticationException;
import davmail.http.HttpClientAdapter;
import davmail.http.request.GetRequest;
import davmail.http.request.PostRequest;
import davmail.http.request.ResponseWrapper;
import davmail.http.request.RestRequest;
import davmail.ui.MessageDialog;

import org.apache.http.HttpStatus;
import org.apache.http.client.utils.URIBuilder;
import org.apache.log4j.Logger;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

import java.awt.HeadlessException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
Expand Down Expand Up @@ -236,15 +240,15 @@ public void authenticate() throws IOException {

}

private String authenticateRedirectADFS(HttpClientAdapter httpClientAdapter, String federationRedirectUrl, String authorizeUrl) throws IOException {
private String authenticateRedirectADFS(HttpClientAdapter httpClientAdapter, String federationRedirectUrl, String authorizeUrl) throws JSONException, IOException {
// get ADFS login form
GetRequest logonFormMethod = new GetRequest(federationRedirectUrl);
logonFormMethod = httpClientAdapter.executeFollowRedirect(logonFormMethod);
String responseBodyAsString = logonFormMethod.getResponseBodyAsString();
return authenticateADFS(httpClientAdapter, responseBodyAsString, authorizeUrl);
}

private String authenticateADFS(HttpClientAdapter httpClientAdapter, String responseBodyAsString, String authorizeUrl) throws IOException {
private String authenticateADFS(HttpClientAdapter httpClientAdapter, String responseBodyAsString, String authorizeUrl) throws JSONException, IOException {
URI location;

if (responseBodyAsString.contains("login.microsoftonline.com")) {
Expand Down Expand Up @@ -322,7 +326,7 @@ private String authenticateADFS(HttpClientAdapter httpClientAdapter, String resp
throw new IOException("Unknown ADFS authentication failure");
}

private URI processDeviceLogin(HttpClientAdapter httpClient, URI location) throws IOException {
private URI processDeviceLogin(HttpClientAdapter httpClient, URI location) throws JSONException, IOException {
URI result = location;
LOGGER.debug("Proceed to device authentication");
GetRequest deviceLoginMethod = new GetRequest(location);
Expand All @@ -339,9 +343,13 @@ private URI processDeviceLogin(HttpClientAdapter httpClient, URI location) throw
processMethod.setParameter("ctx", ctx);
processMethod.setParameter("flowtoken", flowtoken);

httpClient.executePostRequest(processMethod);
responseBodyAsString = httpClient.executePostRequest(processMethod);
result = processMethod.getRedirectLocation();

if (result == null && responseBodyAsString != null && responseBodyAsString.indexOf("arrUserProofs") > 0) {
result = handleMfa(httpClient, processMethod, username, null);
}

if (result == null) {
throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
}
Expand All @@ -356,6 +364,7 @@ private URI handleMfa(HttpClientAdapter httpClientAdapter, PostRequest logonMeth

String urlBeginAuth = config.getString("urlBeginAuth");
String urlEndAuth = config.getString("urlEndAuth");
String urlProcessAuth = config.optString("urlPost", "https://login.microsoftonline.com/" + tenantId + "/SAS/ProcessAuth");

boolean isMFAMethodSupported = false;

Expand All @@ -382,74 +391,96 @@ private URI handleMfa(HttpClientAdapter httpClientAdapter, PostRequest logonMeth
String hpgact = config.getString("hpgact");
String hpgid = config.getString("hpgid");

String clientRqId = clientRequestId;
if (clientRqId == null) {
clientRqId = config.getString("correlationId");
}

RestRequest beginAuthMethod = new RestRequest(urlBeginAuth);
beginAuthMethod.setRequestHeader("Accept", "application/json");
beginAuthMethod.setRequestHeader("canary", apiCanary);
beginAuthMethod.setRequestHeader("client-request-id", clientRequestId);
beginAuthMethod.setRequestHeader("client-request-id", clientRqId);
beginAuthMethod.setRequestHeader("hpgact", hpgact);
beginAuthMethod.setRequestHeader("hpgid", hpgid);
beginAuthMethod.setRequestHeader("hpgrequestid", hpgrequestid);

// only support PhoneAppNotification
JSONObject beginAuthJson = new JSONObject();
beginAuthJson.put("AuthMethodId", "PhoneAppNotification");
beginAuthJson.put("Ctx", context);
beginAuthJson.put("FlowToken", flowToken);
beginAuthJson.put("Method", "BeginAuth");
beginAuthMethod.setJsonBody(beginAuthJson);

config = httpClientAdapter.executeRestRequest(beginAuthMethod);
LOGGER.debug(config);

if (!config.getBoolean("Success")) {
throw new IOException("Authentication failed: " + config);
MessageDialog messageDialog;
try {
messageDialog = new MessageDialog(BundleMessage.format("UI_O365Modern_MFA_PHONE_NOTIFICATION"));
} catch (HeadlessException e) {
messageDialog = null;
LOGGER.debug(e);
}
try {
JSONObject beginAuthJson = new JSONObject();
beginAuthJson.put("AuthMethodId", "PhoneAppNotification");
beginAuthJson.put("Ctx", context);
beginAuthJson.put("FlowToken", flowToken);
beginAuthJson.put("Method", "BeginAuth");
beginAuthMethod.setJsonBody(beginAuthJson);

config = httpClientAdapter.executeRestRequest(beginAuthMethod);
LOGGER.debug(config);

context = config.getString("Ctx");
flowToken = config.getString("FlowToken");
String sessionId = config.getString("SessionId");
if (!config.getBoolean("Success")) {
throw new IOException("Authentication failed: " + config);
}

int i = 0;
boolean success = false;
while (!success && i++ < 12) {
context = config.getString("Ctx");
flowToken = config.getString("FlowToken");
String sessionId = config.getString("SessionId");

try {
Thread.sleep(5000);
} catch (InterruptedException e) {
LOGGER.debug("Interrupted");
Thread.currentThread().interrupt();
}
int i = 0;
boolean success = false;
while (!success && i++ < 12) {

RestRequest endAuthMethod = new RestRequest(urlEndAuth);
endAuthMethod.setRequestHeader("Accept", "application/json");
endAuthMethod.setRequestHeader("canary", apiCanary);
endAuthMethod.setRequestHeader("client-request-id", clientRequestId);
endAuthMethod.setRequestHeader("hpgact", hpgact);
endAuthMethod.setRequestHeader("hpgid", hpgid);
endAuthMethod.setRequestHeader("hpgrequestid", hpgrequestid);

JSONObject endAuthJson = new JSONObject();
endAuthJson.put("AuthMethodId", "PhoneAppNotification");
endAuthJson.put("Ctx", context);
endAuthJson.put("FlowToken", flowToken);
endAuthJson.put("Method", "EndAuth");
endAuthJson.put("PollCount", "1");
endAuthJson.put("SessionId", sessionId);

endAuthMethod.setJsonBody(endAuthJson);

config = httpClientAdapter.executeRestRequest(endAuthMethod);
LOGGER.debug(config);
String resultValue = config.getString("ResultValue");
if ("PhoneAppDenied".equals(resultValue) || "PhoneAppNoResponse".equals(resultValue)) {
throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_REASON", resultValue);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
LOGGER.debug("Interrupted");
Thread.currentThread().interrupt();
}

RestRequest endAuthMethod = new RestRequest(urlEndAuth);
endAuthMethod.setRequestHeader("Accept", "application/json");
endAuthMethod.setRequestHeader("canary", apiCanary);
endAuthMethod.setRequestHeader("client-request-id", clientRequestId);
endAuthMethod.setRequestHeader("hpgact", hpgact);
endAuthMethod.setRequestHeader("hpgid", hpgid);
endAuthMethod.setRequestHeader("hpgrequestid", hpgrequestid);

JSONObject endAuthJson = new JSONObject();
endAuthJson.put("AuthMethodId", "PhoneAppNotification");
endAuthJson.put("Ctx", context);
endAuthJson.put("FlowToken", flowToken);
endAuthJson.put("Method", "EndAuth");
endAuthJson.put("PollCount", "1");
endAuthJson.put("SessionId", sessionId);
// Documentation reference will be helpful, something from StackOverflow: https://stackoverflow.com/questions/57999231/building-processauth-post-using-python-requests
// When in beginAuthMethod is used 'AuthMethodId': 'OneWaySMS', then in endAuthMethod is send SMS code via attribute 'AdditionalAuthData'
// endAuthJson.put("AdditionalAuthData", smsCode);

endAuthMethod.setJsonBody(endAuthJson);

config = httpClientAdapter.executeRestRequest(endAuthMethod);
LOGGER.debug(config);
String resultValue = config.getString("ResultValue");
if ("PhoneAppDenied".equals(resultValue) || "PhoneAppNoResponse".equals(resultValue)) {
throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_REASON", resultValue);
}
if (config.getBoolean("Success")) {
success = true;
}
}
if (config.getBoolean("Success")) {
success = true;
if (!success) {
throw new IOException("Authentication failed: " + config);
}
} finally {
if (messageDialog != null) {
messageDialog.setVisible(false);
messageDialog.dispose();
}
}
if (!success) {
throw new IOException("Authentication failed: " + config);
}

String authMethod = "PhoneAppOTP";
Expand All @@ -459,7 +490,7 @@ private URI handleMfa(HttpClientAdapter httpClientAdapter, PostRequest logonMeth
flowToken = config.getString("FlowToken");

// process auth
PostRequest processAuthMethod = new PostRequest("https://login.microsoftonline.com/" + tenantId + "/SAS/ProcessAuth");
PostRequest processAuthMethod = new PostRequest(urlProcessAuth);
processAuthMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
processAuthMethod.setParameter("type", type);
processAuthMethod.setParameter("request", context);
Expand Down
35 changes: 35 additions & 0 deletions src/java/davmail/ui/MessageDialog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package davmail.ui;

import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JTextArea;

import davmail.BundleMessage;
import davmail.ui.tray.DavGatewayTray;

public class MessageDialog extends JDialog {

public MessageDialog(String message) {
setModal(false);
setUndecorated(false);
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
try {
setIconImages(DavGatewayTray.getFrameIcons());
} catch (NoSuchMethodError error) {
DavGatewayTray.debug(new BundleMessage("LOG_UNABLE_TO_SET_ICON_IMAGE"));
}
JTextArea messageArea = new JTextArea(message);
messageArea.setFont(messageArea.getFont().deriveFont(28f));
messageArea.setEditable(false);
messageArea.getCaret().setVisible(false);
add(messageArea);
setResizable(false);
// center frame
setLocation(getToolkit().getScreenSize().width / 2 - getSize().width / 2, getToolkit().getScreenSize().height / 2 - getSize().height / 2);
setAlwaysOnTop(true);
pack();
setVisible(true);
}


}
1 change: 1 addition & 0 deletions src/java/davmailmessages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ UI_OWA_URL_HELP=Base Outlook Web Access or EWS URL
UI_EWS_HELP=Exchange Web Service URL, ends with /EWS/Exchange.asmx
UI_O365_HELP=Office 365 with classic username/password authentication or application password
UI_O365Modern_HELP=Office 365 modern authentication (Oauth2)
UI_O365Modern_MFA_PHONE_NOTIFICATION=Notification sent to phone
UI_O365Interactive_HELP=Office 365 interactive authentication
UI_O365Manual_HELP=Office 365 manual authentication
UI_O365_MANUAL_PROMPT=Office 365 - Manual authentication
Expand Down
1 change: 1 addition & 0 deletions src/java/davmailmessages_fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ UI_OWA_URL_HELP=URL de connexion Outlook Web Access
UI_EWS_HELP=URL Exchange Web Service, termine par /EWS/Exchange.asmx
UI_O365_HELP=Office 365 avec authentification classique utilisateur/mot de passe ou mot de passe applicatif
UI_O365Modern_HELP=Office 365 authentification moderne (Oauth2)
UI_O365Modern_MFA_PHONE_NOTIFICATION=Notification envoy�e au t�l�phone
UI_O365Interactive_HELP=Office 365 authentification interactive
UI_O365Manual_HELP=Office 365 authentification manuelle
UI_O365_MANUAL_PROMPT=Office 365 - Authentification manuelle
Expand Down
1 change: 1 addition & 0 deletions src/java/davmailmessages_it.properties
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ UI_CALDAV_AUTO_SCHEDULE_HELP=Abilita le notifiche della riunione gestita dal ser
UI_EWS_HELP=URL del servizio Web di Exchange, termina con /EWS/Exchange.asmx
UI_O365_HELP=Office 365 con l'autenticazione classica nome utente / password o password dell'applicazione
UI_O365Modern_HELP=Autenticazione moderna di Office 365 (Oauth2)
UI_O365Modern_MFA_PHONE_NOTIFICATION=Notifica inviata al telefono
UI_O365Interactive_HELP=Autenticazione interattiva di Office 365
UI_Auto_HELP=Modalit� automatica
UI_WebDav_HELP=Exchange 2007 o precedente
Expand Down