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

generic OTP(one time password)-based 2FA(2-factor-authentication) support #215

Merged
merged 33 commits into from
Mar 31, 2022
Merged
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
27c82fa
persisting OTP secrets - uml
rkrenn Mar 20, 2022
94f37bb
persist&retrieve OTP secret (user service)
rkrenn Mar 20, 2022
309d082
error messages when activating 2FA
rkrenn Mar 20, 2022
37f2db3
OTP authenticator types enumeration
rkrenn Mar 20, 2022
068e52b
return OTP type with PasswordOutVO
rkrenn Mar 20, 2022
29ef56e
define some default OTP authenticator for new passwords
rkrenn Mar 20, 2022
d965ae9
wrap-up persisting OTP secrets (service layer)
rkrenn Mar 20, 2022
fdd70a9
2fa checkbox and OTP authenticator selector for admin password UI
rkrenn Mar 20, 2022
5ec8ea1
GoogleAuthenticator OTPAuthenticator
rkrenn Mar 20, 2022
2848cd8
UserService.getOTPRegistrationInfo(), .verifyOTP() - uml
rkrenn Mar 21, 2022
1a2f146
AuthenticationVO.otp field
rkrenn Mar 21, 2022
bc52cd7
.vsl templates for OTP authentication service descriptions
rkrenn Mar 21, 2022
9318bc9
UserService.getOTPRegistrationInfo(), .verifyOTP() - impl
rkrenn Mar 21, 2022
8453cab
refactor OTPAuthenticators: sendOTP(), verifyOTP() methods
rkrenn Mar 21, 2022
78d1171
refactor GoogleAuthenticator - sendOTP(), verifyOTP()
rkrenn Mar 21, 2022
4b40570
constants for data in OTP auth service description vsl templates
rkrenn Mar 21, 2022
33b0044
UserService.getOTPRegistrationInfo(), .verifyOTP() - test stubs
rkrenn Mar 21, 2022
4ae211e
send OTP after successfully verifying credentials
rkrenn Mar 21, 2022
b7340f8
OTP prompt for login page
rkrenn Mar 21, 2022
a1c16db
add Password.showOtpRegistrationInfo field - uml
rkrenn Mar 30, 2022
16fcb79
OTPAuthenticator abstraction, Google Authenticator impl.
rkrenn Mar 30, 2022
66f6c09
set/reset showOtpRegistrationInfo flag
rkrenn Mar 30, 2022
d442023
expose spring applicationContext via CoreUtil
rkrenn Mar 30, 2022
ec9ea79
messages for failing OTP authentication
rkrenn Mar 30, 2022
ad11ba9
otp registration info .vsl templates describing Google Authenticator
rkrenn Mar 30, 2022
0523fe8
Google Authenticator default settings
rkrenn Mar 30, 2022
c114e05
variables used in otp registrationinfo message templates
rkrenn Mar 30, 2022
294df01
urlencoding utility method for velocity templates
rkrenn Mar 30, 2022
039c451
login prompt: otp verification input and registration info message
rkrenn Mar 30, 2022
1d5edc9
VO graph serialisation params for otp registration info message template
rkrenn Mar 30, 2022
57c44c5
non auto-ddl database changes
rkrenn Mar 30, 2022
9222239
by defaul, disable 2FA for trusted hosts
rkrenn Mar 31, 2022
9de4432
2fa/otp password UI labels
rkrenn Mar 31, 2022
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
Prev Previous commit
Next Next commit
OTP prompt for login page
rkrenn committed Mar 21, 2022
commit b7340f8d2cbd105f9f641601fa52fc7df3a09bb3
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@
import java.util.TimeZone;

import javax.annotation.PostConstruct;
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
import javax.faces.context.ExternalContext;
@@ -200,6 +201,7 @@ public static Collection<TimeZoneVO> getTimeZones() {
private int failedAttempts;
private boolean authenticationFailed;
private boolean localPasswordRequired;
private boolean otpRequired;
private String authenticationFailedMessage;
private MenuModel userMenuModel;
private ColumnManagementBean columnManager;
@@ -212,21 +214,22 @@ public SessionScopeBean() {
failedAttempts = 0;
authenticationFailed = false;
localPasswordRequired = false;
otpRequired = false;
authenticationFailedMessage = null;
}

public synchronized void changePassword() {
// try {
// logon = WebUtil.getServiceLocator().getUserService().setPassword(auth, newPassword, oldPassword);
// auth.setPassword(newPassword);
// initSets();
// logout(JsUtil.encodeBase64(WebUtil.createViewUrl(Urls.USER, false, GetParamNames.USER_ID, logon.getInheritedUser().getId()), true));
// } catch (ServiceException | AuthorisationException | IllegalArgumentException e) {
// Messages.addMessage(FacesMessage.SEVERITY_ERROR, e.getMessage());
// } catch (AuthenticationException e) {
// Messages.addMessage(FacesMessage.SEVERITY_ERROR, e.getMessage());
// WebUtil.publishException(e);
// }
try {
logon = WebUtil.getServiceLocator().getUserService().setPassword(auth, newPassword, oldPassword, false);
auth.setPassword(newPassword);
initSets();
logout(JsUtil.encodeBase64(WebUtil.createViewUrl(Urls.USER, false, GetParamNames.USER_ID, logon.getInheritedUser().getId()), true));
} catch (ServiceException | AuthorisationException | IllegalArgumentException e) {
Messages.addMessage(FacesMessage.SEVERITY_ERROR, e.getMessage());
} catch (AuthenticationException e) {
Messages.addMessage(FacesMessage.SEVERITY_ERROR, e.getMessage());
WebUtil.publishException(e);
}
}

private void clearAuthenticationFailedMessage() {
@@ -976,12 +979,16 @@ public synchronized boolean isLocalAuthMethod() {
return true;
}

public synchronized boolean isOtpRequired() {
return otpRequired;
}

public synchronized boolean isLocalPasswordRequired() {
return localPasswordRequired;
}

public synchronized boolean isLoggedIn() {
return logon != null;
return logon != null && !otpRequired;
}

public synchronized boolean isShowStatusBarInfo() {
@@ -1188,16 +1195,22 @@ private void loadUserMenuModel() {

public synchronized String login() {
logon = null;
otpRequired = false;
auth.setHost(WebUtil.getRemoteHost());
String outcome;
try {
logon = WebUtil.getServiceLocator().getToolsService().logon(auth);
ApplicationScopeBean.registerActiveUser(logon.getInheritedUser());
WebUtil.setSessionTimeout();
failedAttempts = 0;
auth.setLocalPassword(null);
clearAuthenticationFailedMessage();
outcome = getLoginOutcome(true);
if (logon.getEnable2fa()) {
otpRequired = true;
outcome = getLoginOutcome(false);
} else {
ApplicationScopeBean.registerActiveUser(logon.getInheritedUser());
WebUtil.setSessionTimeout();
failedAttempts = 0;
outcome = getLoginOutcome(true);
}
} catch (ServiceException e) {
failedAttempts++;
authenticationFailed = true;
@@ -1234,6 +1247,58 @@ public synchronized String login() {
return outcome;
}

public synchronized String verifyOtp() {
String outcome;
if (logon != null && otpRequired) {
try {
WebUtil.getServiceLocator().getUserService().verifyOTP(auth, logon.getInheritedUser().getId(), logon.getOtpSent(), null);
otpRequired = false;
auth.setOtp(null);
ApplicationScopeBean.registerActiveUser(logon.getInheritedUser());
WebUtil.setSessionTimeout();
failedAttempts = 0;
outcome = getLoginOutcome(true);
} catch (ServiceException e) {
logon = null;
failedAttempts++;
authenticationFailed = true;
authenticationFailedMessage = e.getMessage();
outcome = getLoginOutcome(false);
} catch (AuthenticationException e) {
logon = null;
failedAttempts++;
authenticationFailed = true;
if (Settings.getBoolean(SettingCodes.HIDE_DETAILED_AUTHENTICATION_ERROR, Bundle.SETTINGS, DefaultSettings.HIDE_DETAILED_AUTHENTICATION_ERROR)) {
authenticationFailedMessage = Messages.getString(MessageCodes.OPAQUE_AUTHENTICATION_ERROR_MESSAGE);
} else {
authenticationFailedMessage = e.getMessage();
}
outcome = getLoginOutcome(false);
} catch (AuthorisationException e) {
logon = null;
failedAttempts++;
authenticationFailed = true;
authenticationFailedMessage = e.getMessage();
outcome = getLoginOutcome(false);
} catch (IllegalArgumentException e) {
logon = null;
failedAttempts++;
authenticationFailed = true;
authenticationFailedMessage = e.getMessage();
outcome = getLoginOutcome(false);
} finally {
initSets();
}
} else {
logon = null;
failedAttempts++;
authenticationFailed = true;
authenticationFailedMessage = e.getMessage();
outcome = getLoginOutcome(false);
}
return outcome;
}

public synchronized void logout() {
FacesContext context = FacesContext.getCurrentInstance();
logout(WebUtil.getRefererBase64((HttpServletRequest) context.getExternalContext().getRequest()));
@@ -1276,6 +1341,7 @@ public synchronized String resetLoginInputs() {
auth.setUsername(null);
auth.setPassword(null);
auth.setLocalPassword(null);
auth.setOtp(null);
clearAuthenticationFailedMessage();
return getLoginOutcome(false);
}
36 changes: 34 additions & 2 deletions web/src/main/webapp/login.xhtml
Original file line number Diff line number Diff line change
@@ -43,7 +43,26 @@
<h:panelGrid rendered="#{!sessionScopeBean.loggedIn}" columns="1"
cellpadding="0" styleClass="ctsms-login-panelgrid"
rowClasses="ctsms-input-tied-row,ctsms-input-row,ctsms-message-row,ctsms-message-row,ctsms-message-row,ctsms-message-row,ctsms-toolbar-row">

<h:panelGrid columns="3" cellpadding="2"
rendered="#{sessionScopeBean.otpRequired}"
columnClasses="ctsms-label-for-column,ctsms-input-column,ctsms-message-for-column">
<h:outputLabel for="otp"
value="#{labels.login_otp_label}" />
<h:panelGroup>
<p:inputText id="otp" value="#{sessionScopeBean.otp}"
required="true" label="#{labels.login_otp}"
styleClass="ctsms-control" />
<p:tooltip rendered="true" for="otp">
<h:outputText value="#{labels.login_otp_tooltip}"
escape="false" />
</p:tooltip>
</h:panelGroup>
<p:message for="otp" />
</h:panelGrid>

<h:panelGrid columns="3" cellpadding="2"
rendered="#{!sessionScopeBean.otpRequired}"
columnClasses="ctsms-label-for-column,ctsms-input-column,ctsms-message-for-column">
<h:outputLabel for="username"
value="#{labels.login_username_label}" />
@@ -89,7 +108,7 @@
style="#{sessionScopeBean.localPasswordRequired ? '' : 'display:none;'}"
for="localPassword" />
</h:panelGrid>
<h:panelGrid rendered="#{sessionScopeBean.captchaRequired}"
<h:panelGrid rendered="#{!sessionScopeBean.otpRequired and sessionScopeBean.captchaRequired}"
columns="2" cellpadding="2"
columnClasses="ctsms-input-column,ctsms-message-for-column">
<ctsms:captcha required="false" id="captcha" secure="true"
@@ -99,7 +118,8 @@
</h:panelGrid>

<h:outputLink
rendered="#{!sessionScopeBean.captchaRequired and !sessionScopeBean.localPasswordRequired}"
rendered="#{(!sessionScopeBean.otpRequired and !sessionScopeBean.captchaRequired and !sessionScopeBean.localPasswordRequired)
or (sessionScopeBean.otpRequired and XXXXXX)}"
value="http://phoenixctms.org">
<h:graphicImage style="border:0px;"
value="resources/images/phoenix_banner.png" />
@@ -135,12 +155,24 @@
<p:toolbarGroup align="right">
<p:commandButton value="#{labels.login_button_label}"
action="#{sessionScopeBean.login}"
rendered="#{!sessionScopeBean.otpRequired}"
icon="ui-icon ui-icon-arrowreturnthick-1-e" ajax="false"
disabled="false">
<f:param
name="#{applicationScopeBean.parameterName('REFERER')}"
value="#{refererBase64 == null ? '' : refererBase64}" />
</p:commandButton>

<p:commandButton value="#{labels.login_button_label}"
action="#{sessionScopeBean.login}"
rendered="#{sessionScopeBean.otpRequired}"
icon="ui-icon ui-icon-arrowreturnthick-1-e" ajax="false"
disabled="false">
<f:param
name="#{applicationScopeBean.parameterName('REFERER')}"
value="#{refererBase64 == null ? '' : refererBase64}" />
</p:commandButton>

<p:commandButton value="#{labels.reset_button_label}"
action="#{sessionScopeBean.resetLoginInputs}"
icon="ui-icon ui-icon-close" immediate="true" ajax="false"