You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openmeetings.apache.org by so...@apache.org on 2022/12/26 05:35:51 UTC

[openmeetings] branch master updated: [OPENMEETINGS-2755] 2 factor auth is added

This is an automated email from the ASF dual-hosted git repository.

solomax pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openmeetings.git


The following commit(s) were added to refs/heads/master by this push:
     new a75f9b7cc [OPENMEETINGS-2755] 2 factor auth is added
a75f9b7cc is described below

commit a75f9b7ccf2c5c235d84258935687685bb0c4260
Author: Maxim Solodovnik <so...@gmail.com>
AuthorDate: Mon Dec 26 12:35:40 2022 +0700

    [OPENMEETINGS-2755] 2 factor auth is added
---
 .../db/dao/basic/ConfigurationDao.java             |   8 +
 .../apache/openmeetings/db/entity/user/User.java   |  24 +++
 .../installation/ImportInitvalues.java             |  17 +-
 .../apache/openmeetings/mediaserver/KStream.java   |   2 -
 .../service/calendar/caldav/IcalUtils.java         |   2 +-
 .../openmeetings/util/OpenmeetingsVariables.java   |  10 +
 .../apache/openmeetings/util/TestStoredFile.java   |   6 +-
 openmeetings-web/pom.xml                           |   8 +
 .../src/main/front/room/src/user-list.js           |   4 +-
 .../src/main/front/settings/src/WebRtcPeer.js      |   4 +-
 openmeetings-web/src/main/front/wb/src/wb-area.js  |   1 -
 .../openmeetings/web/admin/backup/BackupPanel.java |   4 +-
 .../web/admin/configurations/ConfigsPanel.html     |  14 +-
 .../web/admin/groups/GroupUsersPanel.java          |   4 +-
 .../openmeetings/web/admin/labels/LangPanel.java   |   4 +-
 .../openmeetings/web/admin/oauth/OAuthForm.java    |   4 +-
 .../openmeetings/web/admin/rooms/RoomForm.java     |   8 +-
 .../openmeetings/web/admin/users/UserForm.html     |   6 +
 .../openmeetings/web/admin/users/UserForm.java     |  19 +-
 .../web/app/Application.properties.xml             |  10 +
 .../web/app/Application_ar.properties.xml          |  10 +
 .../web/app/Application_bg.properties.xml          |  10 +
 .../web/app/Application_bn.properties.xml          |  10 +
 .../web/app/Application_ca.properties.xml          |  10 +
 .../web/app/Application_cs.properties.xml          |  10 +
 .../web/app/Application_da.properties.xml          |  10 +
 .../web/app/Application_de.properties.xml          |  10 +
 .../web/app/Application_el.properties.xml          |  10 +
 .../web/app/Application_es.properties.xml          |  10 +
 .../web/app/Application_fa.properties.xml          |  10 +
 .../web/app/Application_fi.properties.xml          |  10 +
 .../web/app/Application_fr.properties.xml          |  10 +
 .../web/app/Application_gl.properties.xml          |  10 +
 .../web/app/Application_he.properties.xml          |  10 +
 .../web/app/Application_hi.properties.xml          |  10 +
 .../web/app/Application_hu.properties.xml          |  10 +
 .../web/app/Application_id.properties.xml          |  10 +
 .../web/app/Application_it.properties.xml          |  10 +
 .../web/app/Application_ja.properties.xml          |  10 +
 .../web/app/Application_ko.properties.xml          |  10 +
 .../web/app/Application_ku.properties.xml          |  10 +
 .../web/app/Application_lo.properties.xml          |  10 +
 .../web/app/Application_nl.properties.xml          |  10 +
 .../web/app/Application_pl.properties.xml          |  10 +
 .../web/app/Application_pt.properties.xml          |  10 +
 .../web/app/Application_pt_BR.properties.xml       |  10 +
 .../web/app/Application_ru.properties.xml          |  10 +
 .../web/app/Application_sk.properties.xml          |  10 +
 .../web/app/Application_sv.properties.xml          |  10 +
 .../web/app/Application_ta.properties.xml          |  10 +
 .../web/app/Application_th.properties.xml          |  10 +
 .../web/app/Application_tk.properties.xml          |  10 +
 .../web/app/Application_tr.properties.xml          |  10 +
 .../web/app/Application_uk.properties.xml          |  10 +
 .../web/app/Application_ur.properties.xml          |  10 +
 .../web/app/Application_zh_CN.properties.xml       |  10 +
 .../web/app/Application_zh_TW.properties.xml       |  10 +
 .../apache/openmeetings/web/app/OtpManager.java    | 108 +++++++++++
 .../apache/openmeetings/web/common/Captcha.java    |   4 +-
 .../openmeetings/web/common/CommunityUserForm.html |   6 +-
 .../web/common/UploadableImagePanel.html           |   2 +-
 .../web/common/UploadableImagePanel.java           |   4 +-
 .../common/datetime/AbstractOmDateTimePicker.java  |  16 +-
 .../web/common/tree/FileTreePanel.java             |   4 +-
 .../openmeetings/web/common/upload/UploadForm.java |   2 +-
 .../apache/openmeetings/web/pages/BasePage.java    |   4 +-
 .../apache/openmeetings/web/pages/ResetPage.java   |   2 +-
 .../web/pages/auth/ForgetPasswordDialog.java       |  16 +-
 .../openmeetings/web/pages/auth/OtpDialog.html     |  51 +++++
 .../openmeetings/web/pages/auth/OtpDialog.java     | 215 +++++++++++++++++++++
 .../web/pages/auth/RegisterDialog.java             |  27 ++-
 .../web/pages/auth/ResetPasswordDialog.java        |   6 +-
 .../openmeetings/web/pages/auth/SignInDialog.java  | 113 +++++++----
 .../openmeetings/web/pages/auth/SignInPage.html    |   1 +
 .../openmeetings/web/pages/auth/SignInPage.java    |  10 +-
 .../openmeetings/web/room/IconTextModal.java       |   8 +-
 .../openmeetings/web/room/menu/RoomMenuPanel.java  |   4 +-
 .../web/room/poll/PollResultsDialog.java           |   6 +-
 .../web/user/profile/ChangePasswordDialog.html     |  32 +--
 .../web/user/profile/ChangePasswordDialog.java     |  12 +-
 .../web/user/profile/EditProfileForm.html          |   1 +
 .../web/user/profile/EditProfileForm.java          |  60 ++++--
 .../web/user/profile/EditProfilePanel.html         |   3 +-
 .../web/user/profile/EditProfilePanel.java         |   9 +-
 .../web/user/profile/MessagesContactsPanel.java    |   6 +-
 ...ngePasswordDialog.html => ToggleOtpDialog.html} |  30 +--
 .../web/user/profile/ToggleOtpDialog.java          | 129 +++++++++++++
 .../openmeetings/web/user/rooms/RoomListPanel.java |   4 +-
 .../webapp/WEB-INF/classes/openmeetings.properties |  10 +
 openmeetings-web/src/main/webapp/css/raw-admin.css |   2 +-
 .../src/main/webapp/css/raw-calendar.css           |   4 +-
 .../src/main/webapp/css/raw-general.css            |   6 +-
 openmeetings-web/src/main/webapp/css/raw-wb.css    |   2 +-
 .../webservice/AbstractWebServiceTest.java         |   1 -
 .../webservice/TestRecordingService.java           |   1 -
 pom.xml                                            |  26 ++-
 src/license/license-template.ftl                   |   2 +-
 97 files changed, 1265 insertions(+), 213 deletions(-)

diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/basic/ConfigurationDao.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/basic/ConfigurationDao.java
index 02c13e2cb..b2e8c25c8 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/basic/ConfigurationDao.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/basic/ConfigurationDao.java
@@ -326,6 +326,9 @@ public class ConfigurationDao implements IDataProviderDao<Configuration> {
 			case CONFIG_THEME:
 				reloadTheme();
 				break;
+			case CONFIG_OTP_ENABLED:
+				reloadOtpEnabled();
+				break;
 			default:
 				break;
 		}
@@ -475,6 +478,10 @@ public class ConfigurationDao implements IDataProviderDao<Configuration> {
 		app.updateTheme();
 	}
 
+	private void reloadOtpEnabled() {
+		setOtpEnabled(getBool(CONFIG_OTP_ENABLED, false));
+	}
+
 	public void reinit() {
 		reloadMaxUpload();
 		reloadCrypt();
@@ -503,6 +510,7 @@ public class ConfigurationDao implements IDataProviderDao<Configuration> {
 		reloadAppointmentSettings();
 		reloadRecordingEnabled();
 		reloadTheme();
+		reloadOtpEnabled();
 
 		updateCsp();
 	}
diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/user/User.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/user/User.java
index 7c7882cc8..ed3365a82 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/user/User.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/user/User.java
@@ -373,6 +373,14 @@ public class User extends HistoricalEntity {
 	@XmlJavaTypeAdapter(LongAdapter.class)
 	private Long domainId; // LDAP config id for LDAP, OAuth server id for OAuth
 
+	@Column(name = "otp_secret")
+	@XmlElement(name = "otpSecret", required = false)
+	private String otpSecret;
+
+	@Column(name = "otp_recovery", length=350)
+	@XmlElement(name = "otpRecovery", required = false)
+	private String otpRecoveryCodes;
+
 	@Override
 	public Long getId() {
 		return id;
@@ -658,6 +666,22 @@ public class User extends HistoricalEntity {
 		this.domainId = domainId;
 	}
 
+	public String getOtpSecret() {
+		return otpSecret;
+	}
+
+	public void setOtpSecret(String otpSecret) {
+		this.otpSecret = otpSecret;
+	}
+
+	public String getOtpRecoveryCodes() {
+		return otpRecoveryCodes;
+	}
+
+	public void setOtpRecoveryCodes(String otpRecoveryCodes) {
+		this.otpRecoveryCodes = otpRecoveryCodes;
+	}
+
 	@Override
 	public String toString() {
 		return "User [id=" + id + ", firstname=" + firstname
diff --git a/openmeetings-install/src/main/java/org/apache/openmeetings/installation/ImportInitvalues.java b/openmeetings-install/src/main/java/org/apache/openmeetings/installation/ImportInitvalues.java
index d94f9e4c4..14d4a4e6a 100644
--- a/openmeetings-install/src/main/java/org/apache/openmeetings/installation/ImportInitvalues.java
+++ b/openmeetings-install/src/main/java/org/apache/openmeetings/installation/ImportInitvalues.java
@@ -106,6 +106,7 @@ import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_SMTP_TIM
 import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_SMTP_TLS;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_SMTP_USER;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_THEME;
+import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_OTP_ENABLED;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.DEFAULT_APP_NAME;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.DEFAULT_CSP_DATA;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.DEFAULT_CSP_FONT;
@@ -294,8 +295,12 @@ public class ImportInitvalues {
 		// additional settings
 		// ***************************************
 
-		addCfg(list, CONFIG_SCREENSHARING_QUALITY, "1", Configuration.Type.NUMBER,
-				"Default selection in ScreenSharing Quality:\n 0 - bigger frame rate, no resize\n 1 - no resize\n 2 - size == 1/2 of selected area\n 3 - size == 3/8 of selected area", VER_3_0_3);
+		addCfg(list, CONFIG_SCREENSHARING_QUALITY, "1", Configuration.Type.NUMBER, """
+				Default selection in ScreenSharing Quality:
+				 0 - bigger frame rate, no resize
+				 1 - no resize
+				 2 - size == 1/2 of selected area
+				 3 - size == 3/8 of selected area""", VER_3_0_3);
 
 		addCfg(list, CONFIG_SCREENSHARING_FPS, "10", Configuration.Type.NUMBER, "Default selection in ScreenSharing FPS", VER_3_0_3);
 		addCfg(list, CONFIG_SCREENSHARING_FPS_SHOW, String.valueOf(true), Configuration.Type.BOOL, "Is screensharing FPS should be displayed or not", VER_3_0_3);
@@ -345,9 +350,9 @@ public class ImportInitvalues {
 						+ ", admin/group, admin/room, admin/config, admin/lang, admin/ldap, admin/oauth2, admin/backup, admin/email", "2.1.x");
 
 		// oauth2 params
-		addCfg(list, CONFIG_IGNORE_BAD_SSL, String.valueOf(false), Configuration.Type.BOOL,
-				"Set \"yes\" or \"no\" to enable/disable ssl certifications checking for OAuth2\n"
-				+ "WARNING: it is not secure", VER_3_0);
+		addCfg(list, CONFIG_IGNORE_BAD_SSL, String.valueOf(false), Configuration.Type.BOOL, """
+				Set "yes" or "no" to enable/disable ssl certifications checking for OAuth2
+				WARNING: it is not secure to ignore bad SSL""", VER_3_0);
 
 		addCfg(list, CONFIG_REDIRECT_URL_FOR_EXTERNAL, "", Configuration.Type.STRING,
 				"Users entered the room via invitationHash or secureHash will be redirected to this URL on connection lost", VER_3_0);
@@ -401,6 +406,8 @@ public class ImportInitvalues {
 		addCfg(list, CONFIG_THEME, getTheme(), Configuration.Type.STRING, "UI theme, possible values are Cerulean, Cosmo, Cyborg, Darkly, Flatly, "
 				+ "Journal, Litera, Lumen, Lux, Materia, Minty, Pulse, Sandstone, Simplex, Sketchy, Slate, Solar, Spacelab, Superhero, "
 				+ "United, Yeti", "6.1.0");
+
+		addCfg(list, CONFIG_OTP_ENABLED, String.valueOf(false), Configuration.Type.BOOL, "Whether or not Time-based One Time Passwords are enabled", "6.3.0");
 		return list;
 	}
 
diff --git a/openmeetings-mediaserver/src/main/java/org/apache/openmeetings/mediaserver/KStream.java b/openmeetings-mediaserver/src/main/java/org/apache/openmeetings/mediaserver/KStream.java
index 97f7da622..b6bddfc35 100644
--- a/openmeetings-mediaserver/src/main/java/org/apache/openmeetings/mediaserver/KStream.java
+++ b/openmeetings-mediaserver/src/main/java/org/apache/openmeetings/mediaserver/KStream.java
@@ -115,8 +115,6 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 		streamType = sd.getType();
 		this.connectedSince = new Date();
 		Injector.get().inject(this);
-		//TODO Min/MaxVideoSendBandwidth
-		//TODO Min/Max Audio/Video RecvBandwidth
 	}
 
 	public void startBroadcast(final StreamDesc sd, final String sdpOffer, Runnable then) {
diff --git a/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/IcalUtils.java b/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/IcalUtils.java
index 2372d78a8..0b8bfb807 100644
--- a/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/IcalUtils.java
+++ b/openmeetings-service/src/main/java/org/apache/openmeetings/service/calendar/caldav/IcalUtils.java
@@ -348,7 +348,7 @@ public class IcalUtils {
 	 * @param amount Amount to be Added
 	 * @return New Date
 	 */
-	public Date addTimetoDate(Date date, int field, int amount) { //FIXME TODO
+	public Date addTimetoDate(Date date, int field, int amount) {
 		java.util.Calendar c = java.util.Calendar.getInstance();
 		c.setTime(date);
 		c.add(field, amount);
diff --git a/openmeetings-util/src/main/java/org/apache/openmeetings/util/OpenmeetingsVariables.java b/openmeetings-util/src/main/java/org/apache/openmeetings/util/OpenmeetingsVariables.java
index e28c3b3dd..1ec5ed310 100644
--- a/openmeetings-util/src/main/java/org/apache/openmeetings/util/OpenmeetingsVariables.java
+++ b/openmeetings-util/src/main/java/org/apache/openmeetings/util/OpenmeetingsVariables.java
@@ -121,6 +121,7 @@ public class OpenmeetingsVariables {
 	public static final String CONFIG_CSP_ENABLED = "header.csp.enabled";
 	public static final String CONFIG_RECORDING_ENABLED = "recording.enabled";
 	public static final String CONFIG_THEME = "ui.theme";
+	public static final String CONFIG_OTP_ENABLED = "otp.enabled";
 
 	public static final int RECENT_ROOMS_COUNT = 5;
 	public static final int USER_LOGIN_MINIMUM_LENGTH = 4;
@@ -188,6 +189,7 @@ public class OpenmeetingsVariables {
 	private static int appointmentPreStartMinutes = 5;
 	private static boolean recordingsEnabled = true;
 	private static String theme = "Sandstone";
+	private static boolean otpEnabled = false;
 
 	private OpenmeetingsVariables() {}
 
@@ -640,4 +642,12 @@ public class OpenmeetingsVariables {
 	public static void setTheme(String inTheme) {
 		theme = inTheme;
 	}
+
+	public static boolean isOtpEnabled() {
+		return otpEnabled;
+	}
+
+	public static void setOtpEnabled(boolean enabled) {
+		otpEnabled = enabled;
+	}
 }
diff --git a/openmeetings-util/src/test/java/org/apache/openmeetings/util/TestStoredFile.java b/openmeetings-util/src/test/java/org/apache/openmeetings/util/TestStoredFile.java
index 8e8614e5b..9fb802aee 100644
--- a/openmeetings-util/src/test/java/org/apache/openmeetings/util/TestStoredFile.java
+++ b/openmeetings-util/src/test/java/org/apache/openmeetings/util/TestStoredFile.java
@@ -29,7 +29,7 @@ import org.junit.jupiter.api.Test;
 class TestStoredFile {
 	@Test
 	void testAudio() {
-		final String[] exts = {"aif", "aifc", "aiff", "au", "mp3", "flac", "wav"}; //TODO enlarge
+		final String[] exts = {"aif", "aifc", "aiff", "au", "mp3", "flac", "wav"};
 		for (String ext : exts) {
 			StoredFile sf = new StoredFile("test", ext, (InputStream)null);
 			assertTrue(sf.isVideo(), String.format("Files of type '%s' should be treated as Video", ext));
@@ -39,7 +39,7 @@ class TestStoredFile {
 
 	@Test
 	void testVideo() {
-		final String[] exts = {"avi", "mov", "flv", "mp4"}; //TODO enlarge
+		final String[] exts = {"avi", "mov", "flv", "mp4"};
 		for (String ext : exts) {
 			StoredFile sf = new StoredFile("test", ext, (InputStream)null);
 			assertTrue(sf.isVideo(), String.format("Files of type '%s' should be treated as Video", ext));
@@ -58,7 +58,7 @@ class TestStoredFile {
 				"wpg", // Word Perfect Graphics
 				"bmp", "ico", // Microsoft Icon
 				"tga", // Truevision Targa
-				"jpg", "jpeg"}; //TODO enlarge
+				"jpg", "jpeg"};
 		for (String ext : exts) {
 			StoredFile sf = new StoredFile("test", ext, (InputStream)null);
 			assertTrue(sf.isImage(), String.format("Files of type '%s' should be treated as Image", ext));
diff --git a/openmeetings-web/pom.xml b/openmeetings-web/pom.xml
index 6799e47e8..2c94d67a3 100644
--- a/openmeetings-web/pom.xml
+++ b/openmeetings-web/pom.xml
@@ -573,6 +573,14 @@
 			<groupId>org.webjars</groupId>
 			<artifactId>jquery-ui-touch-punch</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>dev.samstevens.totp</groupId>
+			<artifactId>totp</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>commons-net</groupId>
+			<artifactId>commons-net</artifactId>
+		</dependency>
 		<!-- Test dependencies -->
 		<dependency>
 			<groupId>org.apache.openmeetings</groupId>
diff --git a/openmeetings-web/src/main/front/room/src/user-list.js b/openmeetings-web/src/main/front/room/src/user-list.js
index e56eed39b..75306a260 100644
--- a/openmeetings-web/src/main/front/room/src/user-list.js
+++ b/openmeetings-web/src/main/front/room/src/user-list.js
@@ -78,8 +78,8 @@ function __rightVideoIcon(c, elem) {
 function __rightOtherIcons(c, elem) {
 	__rightIcon(c, elem, ['PRESENTER'], '.right.presenter', () => !options.interview && $('.wb-area').is(':visible'));
 	__rightIcon(c, elem, ['WHITEBOARD', 'PRESENTER'], '.right.wb', () => !options.interview && $('.wb-area').is(':visible'));
-	__rightIcon(c, elem, ['SHARE'], '.right.screen-share', () => true); //FIXME TODO getRoomPanel().screenShareAllowed()
-	__rightIcon(c, elem, ['REMOTE_CONTROL'], '.right.remote-control', () => true); //FIXME TODO getRoomPanel().screenShareAllowed()
+	__rightIcon(c, elem, ['SHARE'], '.right.screen-share', () => true);
+	__rightIcon(c, elem, ['REMOTE_CONTROL'], '.right.remote-control', () => true);
 	__rightIcon(c, elem, ['MODERATOR'], '.right.moderator', () => true);
 }
 function __setStatus(c, le) {
diff --git a/openmeetings-web/src/main/front/settings/src/WebRtcPeer.js b/openmeetings-web/src/main/front/settings/src/WebRtcPeer.js
index 903065988..5d1b84751 100644
--- a/openmeetings-web/src/main/front/settings/src/WebRtcPeer.js
+++ b/openmeetings-web/src/main/front/settings/src/WebRtcPeer.js
@@ -230,13 +230,13 @@ class WebRtcPeer {
 
 						OmUtil.info(`[createOffer] Video sender Degradation Preference set: ${sendParams.degradationPreference}`);
 
-						// FIXME: Firefox implements degradationPreference on each individual encoding!
+						// Firefox implements degradationPreference on each individual encoding!
 						// (set it on every element of the sendParams.encodings array)
 
 						needSetParams = true;
 					}
 
-					// FIXME: Check that the simulcast encodings were applied.
+					// Check that the simulcast encodings were applied.
 					// Firefox doesn't implement `RTCRtpTransceiverInit.sendEncodings`
 					// so the only way to enable simulcast is with `RTCRtpSender.setParameters()`.
 					//
diff --git a/openmeetings-web/src/main/front/wb/src/wb-area.js b/openmeetings-web/src/main/front/wb/src/wb-area.js
index e8705db7d..3132def91 100644
--- a/openmeetings-web/src/main/front/wb/src/wb-area.js
+++ b/openmeetings-web/src/main/front/wb/src/wb-area.js
@@ -243,7 +243,6 @@ module.exports = class DrawWbArea extends WbAreaBase {
 			this.wsinit();
 			_doInit(callback);
 		};
-		//FIXME TODO self.getWb = _getWb;
 		this.setRole = (_role) => {
 			if (!_inited) {
 				return;
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/backup/BackupPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/backup/BackupPanel.java
index 6b7de1d72..62041df81 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/backup/BackupPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/backup/BackupPanel.java
@@ -57,7 +57,7 @@ import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
 import de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel;
 import de.agilecoders.wicket.core.markup.html.bootstrap.components.progress.UpdatableProgressBar;
 import de.agilecoders.wicket.core.markup.html.bootstrap.utilities.BackgroundColorBehavior;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 /**
  * Panel component to manage Backup Import/Export
  *
@@ -186,7 +186,7 @@ public class BackupPanel extends AdminBasePanel {
 					target.add(feedback);
 				}
 			};
-			download.setIconType(FontAwesome5IconType.file_download_s);
+			download.setIconType(FontAwesome6IconType.file_arrow_down_s);
 			progressBar = new UpdatableProgressBar("progress", new Model<>(0), BackgroundColorBehavior.Color.Info, true) {
 				private static final long serialVersionUID = 1L;
 
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/configurations/ConfigsPanel.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/configurations/ConfigsPanel.html
index 808102b73..35e9d7a4a 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/configurations/ConfigsPanel.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/configurations/ConfigsPanel.html
@@ -73,7 +73,7 @@
 						</div>
 						<div wicket:id="boolean-box" class="row">
 							<label wicket:for="valueB" class="form-label col-3 text-right"><wicket:message key="271" /></label>
-							<div class="onoffswitch">
+							<div class="onoffswitch col-8 p-0">
 								<input type="checkbox" class="onoff-checkbox" wicket:id="valueB"/>
 								<label class="onoff-label clickable" wicket:for="valueB"></label>
 							</div>
@@ -86,12 +86,16 @@
 						</div>
 					</div>
 					<div class="formelement row">
-						<label class="form-labelcol-3 text-right"><wicket:message key="268" /></label>
-						<span wicket:id="updated"/>
+						<label class="form-label col-3 text-right"><wicket:message key="268" /></label>
+						<div class="col-8 p-0">
+							<span wicket:id="updated"/>
+						</div>
 					</div>
 					<div class="formelement row">
-						<label class="form-labelcol-3 text-right"><wicket:message key="269" /></label>
-						<span wicket:id="user.login"/>
+						<label class="form-label col-3 text-right"><wicket:message key="269" /></label>
+						<div class="col-8 p-0">
+							<span wicket:id="user.login"/>
+						</div>
 					</div>
 					<div class="formelement row">
 						<label class="form-label col-3 text-right" wicket:for="comment"><wicket:message key="196" /></label>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/groups/GroupUsersPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/groups/GroupUsersPanel.java
index 572ad9b82..cd295b95f 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/groups/GroupUsersPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/groups/GroupUsersPanel.java
@@ -44,7 +44,7 @@ import de.agilecoders.wicket.core.markup.html.bootstrap.badge.BootstrapBadge;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
 import de.agilecoders.wicket.core.markup.html.bootstrap.utilities.BackgroundColorBehavior;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public class GroupUsersPanel extends Panel {
 	private static final long serialVersionUID = 1L;
@@ -92,7 +92,7 @@ public class GroupUsersPanel extends Panel {
 						target.add(GroupUsersPanel.this);
 					}
 				};
-				del.setIconType(FontAwesome5IconType.times_s)
+				del.setIconType(FontAwesome6IconType.xmark_s)
 						.add(newOkCancelDangerConfirm(this, getString("833")));
 				item.add(del);
 				item.add(new BootstrapBadge("new", new ResourceModel("lbl.new"), BackgroundColorBehavior.Color.Warning).setVisible((grpUser.getId() == null)));
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/labels/LangPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/labels/LangPanel.java
index 471fdeddb..046938763 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/labels/LangPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/labels/LangPanel.java
@@ -65,7 +65,7 @@ import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxButt
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
 import de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 /**
  * Language Editor, add/insert/update Label and add/delete language contains several Forms and one list
@@ -242,7 +242,7 @@ public class LangPanel extends AdminBasePanel {
 				target.add(listContainer);
 			}
 		};
-		langForm.add(delLngBtn.setIconType(FontAwesome5IconType.times_s)
+		langForm.add(delLngBtn.setIconType(FontAwesome6IconType.xmark_s)
 				.add(newOkCancelDangerConfirm(this, getString("833"))));
 		super.onInitialize();
 	}
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/oauth/OAuthForm.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/oauth/OAuthForm.java
index adb89d7c2..d3c28f4c1 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/oauth/OAuthForm.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/oauth/OAuthForm.java
@@ -55,7 +55,7 @@ import org.apache.wicket.util.string.Strings;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxButton;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public class OAuthForm extends AdminBaseForm<OAuthServer> {
 	private static final long serialVersionUID = 1L;
@@ -79,7 +79,7 @@ public class OAuthForm extends AdminBaseForm<OAuthServer> {
 					target.add(attrsContainer);
 				}
 			};
-			del.setIconType(FontAwesome5IconType.times_s)
+			del.setIconType(FontAwesome6IconType.xmark_s)
 					.add(newOkCancelDangerConfirm(this, getString("833")));
 			item.add(new Label("key", Model.of(entry.getKey())))
 				.add(new Label("value", Model.of(entry.getValue())))
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java
index a55b78fab..32db3737d 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java
@@ -84,7 +84,7 @@ import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxButt
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
 import de.agilecoders.wicket.core.markup.html.bootstrap.utilities.BackgroundColorBehavior;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public class RoomForm extends AdminBaseForm<Room> {
 	private static final long serialVersionUID = 1L;
@@ -110,7 +110,7 @@ public class RoomForm extends AdminBaseForm<Room> {
 					updateClients(target);
 				}
 			};
-			del.setIconType(FontAwesome5IconType.times_s)
+			del.setIconType(FontAwesome6IconType.xmark_s)
 					.add(newOkCancelDangerConfirm(this, getString("833")));
 			item.add(new Label("clientId", "" + c.getUserId()))
 				.add(new Label("clientLogin", "" + c.getUser().getLogin()))
@@ -317,7 +317,7 @@ public class RoomForm extends AdminBaseForm<Room> {
 						target.add(moderatorContainer);
 					}
 				};
-				del.setIconType(FontAwesome5IconType.times_s)
+				del.setIconType(FontAwesome6IconType.xmark_s)
 						.add(newOkCancelDangerConfirm(this, getString("833")));
 				item.add(new CheckBox("superModerator", new PropertyModel<>(moderator, "superModerator")))
 					.add(new Label("userId", String.valueOf(moderator.getUser().getId())))
@@ -403,7 +403,7 @@ public class RoomForm extends AdminBaseForm<Room> {
 						target.add(filesContainer);
 					}
 				};
-				del.setIconType(FontAwesome5IconType.times_s)
+				del.setIconType(FontAwesome6IconType.xmark_s)
 						.add(newOkCancelDangerConfirm(this, getString("833")));
 				item.add(new Label("name", new PropertyModel<>(rf.getFile(), "name")))
 					.add(new Label("wbIdx", new PropertyModel<>(rf, "wbIdx")))
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.html
index a314ed2e1..969858c5e 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.html
@@ -42,6 +42,12 @@
 					<input type="password" wicket:id="password" class="form-control"/>
 				</div>
 			</div>
+			<div class="formelement row">
+				<label wicket:for="otp-enabled" class="form-check-label col-3 text-right"><wicket:message key="otp.enabled" /></label>
+				<div class="col-8 p-0 form-check">
+					<input type="checkbox" class="formcheckbox form-check-input ms-0" wicket:id="otp-enabled" />
+				</div>
+			</div>
 			<form wicket:id="general"></form>
 			<div class="formelement row">
 				<label wicket:for="type" class="form-label col-3 text-right"><wicket:message key="45" /></label>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java
index 1e6a08c1e..afabc5061 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java
@@ -25,6 +25,7 @@ import static org.apache.openmeetings.db.util.AuthLevelUtil.hasAdminLevel;
 import static org.apache.openmeetings.db.util.AuthLevelUtil.hasGroupAdminLevel;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.getMinLoginLength;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.isSendRegisterEmail;
+import static org.apache.openmeetings.util.OpenmeetingsVariables.isOtpEnabled;
 import static org.apache.openmeetings.web.app.WebSession.getRights;
 import static org.apache.openmeetings.web.app.WebSession.getUserId;
 import static org.apache.wicket.validation.validator.StringValidator.minimumLength;
@@ -56,6 +57,7 @@ import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.form.OnChangeAjaxBehavior;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.CheckBox;
 import org.apache.wicket.markup.html.form.ChoiceRenderer;
 import org.apache.wicket.markup.html.form.DropDownChoice;
 import org.apache.wicket.markup.html.form.Form;
@@ -96,6 +98,7 @@ public class UserForm extends AdminBaseForm<User> {
 	private final DropDownChoice<Long> domainId = new DropDownChoice<>("domainId");
 	private final PasswordDialog adminPass;
 	private final UploadableProfileImagePanel avatar = new UploadableProfileImagePanel("avatar", null);
+	private final CheckBox otpEnabled = new CheckBox("otp-enabled", Model.of(false));
 	@SpringBean
 	private UserDao userDao;
 	@SpringBean
@@ -121,6 +124,7 @@ public class UserForm extends AdminBaseForm<User> {
 		mainContainer.add(generalForm = new GeneralUserForm("general", getModel(), true));
 		mainContainer.add(password.setResetPassword(false).setLabel(new ResourceModel("110")).setRequired(false)
 				.add(passValidator = new StrongPasswordValidator(getModelObject())));
+		mainContainer.add(otpEnabled.setLabel(new ResourceModel("otp.enabled")));
 		login.setLabel(new ResourceModel("108"));
 		mainContainer.add(login.add(minimumLength(getMinLoginLength())));
 
@@ -180,16 +184,19 @@ public class UserForm extends AdminBaseForm<User> {
 	@Override
 	protected void onModelChanged() {
 		super.onModelChanged();
-		boolean nd = !getModelObject().isDeleted();
-		boolean isNew = getModelObject().getId() == null;
+		User u = getModelObject();
+		boolean nd = !u.isDeleted();
+		boolean isNew = u.getId() == null;
 		mainContainer.setEnabled(nd);
+		otpEnabled.setModelObject(u.getOtpSecret() != null)
+				.setEnabled(isOtpEnabled() && u.getOtpSecret() != null); // admin can only disable OTP
 		setSaveVisible(nd);
 		setDelVisible(nd && !isNew);
 		setRestoreVisible(!nd);
 		setPurgeVisible(!isNew);
 		password.setModelObject(null);
-		generalForm.updateModelObject(getModelObject(), true);
-		passValidator.setUser(getModelObject());
+		generalForm.updateModelObject(u, true);
+		passValidator.setUser(u);
 	}
 
 	@Override
@@ -255,6 +262,10 @@ public class UserForm extends AdminBaseForm<User> {
 		if (isNew && DISPLAY_NAME_NA.equals(u.getDisplayName())) {
 			u.resetDisplayName();
 		}
+		if (Boolean.FALSE.equals(otpEnabled.getModelObject())) {
+			u.setOtpSecret(null);
+			u.setOtpRecoveryCodes(null);
+		}
 		try {
 			u = userDao.update(u, pass, getUserId());
 		} catch (Exception e) {
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.properties.xml
index dd9f98409..81ac794cd 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numeric 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Yes/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ar.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ar.properties.xml
index cfbc96ead..3b769f636 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ar.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ar.properties.xml
@@ -881,6 +881,16 @@ see https://openmeetings.apache.org/LanguageEditor.html for Details
 	<entry key="network.test.upl.time"><![CDATA[وقت التحميل]]></entry>
 	<entry key="notification.chat.message"><![CDATA[لديك رسالة (رسائل) دردشة جديدة]]></entry>
 	<entry key="notification.room.activity"><![CDATA[لديك أنشطة جديدة ، يرجى تحديد "الأنشطة والعمل"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[استنساخ]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[عددي من 1 الى 10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[نعم / لا]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_bg.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_bg.properties.xml
index d0bfc4e4d..2ab090e4e 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_bg.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_bg.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Числов 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Да/Не]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_bn.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_bn.properties.xml
index 943964a62..d235dfa61 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_bn.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_bn.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numeric 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Yes/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ca.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ca.properties.xml
index e56508a89..012844388 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ca.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ca.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Valor numèric [1-10]]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Sí/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_cs.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_cs.properties.xml
index fd533655d..83e7bcc3e 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_cs.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_cs.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Čísla 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Ano/Ne]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_da.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_da.properties.xml
index 3fca24f1b..6a27b279e 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_da.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_da.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numerisk 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Ja/Nej]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_de.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_de.properties.xml
index 03ec631ec..45a4e9af2 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_de.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_de.properties.xml
@@ -887,6 +887,16 @@ Bitte <tt>openmeetings.log</tt> prüfen und die OpenMeetings-Entwickeler kontakt
 	<entry key="network.test.upl.time"><![CDATA[Upload Zeit]]></entry>
 	<entry key="notification.chat.message"><![CDATA[Sie haben neue Chat-Nachrichten]]></entry>
 	<entry key="notification.room.activity"><![CDATA[Es gibt neue Aktivitäten. Bitte sehen Sie unter "Aktivitäten & Aktionen" nach.]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clonen]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numerisch 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Ja/Nein]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_el.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_el.properties.xml
index aab8a8f18..2ba1a175e 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_el.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_el.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Αριθμός 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Ναι/Όχι]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_es.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_es.properties.xml
index e1c57ece6..8a05180f3 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_es.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_es.properties.xml
@@ -883,6 +883,16 @@ por favor revise <tt> openmeetings.log </tt> y contacte a los desarrolladores de
 	<entry key="network.test.upl.time"><![CDATA[Tiempo de subida]]></entry>
 	<entry key="notification.chat.message"><![CDATA[Usted tiene nuevo mensaje(s) de chat]]></entry>
 	<entry key="notification.room.activity"><![CDATA[Usted tiene nuevas actividades, marque "Actividades&Acción"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clonar]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Valor numérico [1-10]]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Sí/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fa.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fa.properties.xml
index f5dd65af0..7c2c547e9 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fa.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fa.properties.xml
@@ -878,6 +878,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[زمان بارگذاری]]></entry>
 	<entry key="notification.chat.message"><![CDATA[پیام (های) گپ جدید دارید]]></entry>
 	<entry key="notification.room.activity"><![CDATA[شما فعالیت های جدیدی دارید ، لطفاً "فعالیتها و اقدام" را بررسی کنید]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[اعداد 10-1]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[خير/بله]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fi.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fi.properties.xml
index 65e7ec05b..d236119b0 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fi.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fi.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numeerinen 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Kyllä/Ei]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fr.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fr.properties.xml
index b73c3708c..dbd90d5ee 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fr.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_fr.properties.xml
@@ -883,6 +883,16 @@ allez sur <tt>openmeetings.log</tt> et contactez les développeurs d'OpenMeeting
 	<entry key="network.test.upl.time"><![CDATA[Temps de téléchargement]]></entry>
 	<entry key="notification.chat.message"><![CDATA[Vous avez un nouveau message]]></entry>
 	<entry key="notification.room.activity"><![CDATA[Vous avez des nouvelles notifications, regardez dans "Notifications & Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Cloner]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numérique 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Oui/Non]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_gl.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_gl.properties.xml
index bba5f5613..bd1c7fe53 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_gl.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_gl.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Valor numérico 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Sí/Non]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_he.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_he.properties.xml
index 135b38c93..13dc739f4 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_he.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_he.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[יש לך הודעות צ'אט חדשות]]></entry>
 	<entry key="notification.room.activity"><![CDATA[יש לך פעילויות חדשות, אנא בדוק "פעילויות ופעולה"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numeric 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Yes/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_hi.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_hi.properties.xml
index 72a73f9fc..5d459c155 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_hi.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_hi.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[राशि (१-१०)]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Yes/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_hu.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_hu.properties.xml
index ee2f98b16..789e40ea9 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_hu.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_hu.properties.xml
@@ -870,6 +870,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Szám 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Igen/Nem]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_id.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_id.properties.xml
index 4090525a7..220afc69d 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_id.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_id.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Angka dari 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Ya/Tidak]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_it.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_it.properties.xml
index 7d0e8aa99..a6f767806 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_it.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_it.properties.xml
@@ -883,6 +883,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Tempo Upload]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numerico 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Si/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ja.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ja.properties.xml
index 12796dec5..b842ca3a1 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ja.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ja.properties.xml
@@ -882,6 +882,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[アップロード時間]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[1~10の数字]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[はい/いいえ]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ko.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ko.properties.xml
index 5d7edd53f..76c1efa5d 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ko.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ko.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[숫자 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[네/아니오]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ku.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ku.properties.xml
index a1dafe489..2e3bac866 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ku.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ku.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[Peyama weyên nû yên sohbetê hene]]></entry>
 	<entry key="notification.room.activity"><![CDATA[Çalakiyên we yên nû hene, ji kerema xwe "Çalakî & Çalakî" bigerin]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numeric 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Yes/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_lo.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_lo.properties.xml
index 90f758bc5..5ae55fe09 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_lo.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_lo.properties.xml
@@ -881,6 +881,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numeric 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Yes/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_nl.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_nl.properties.xml
index 8cc24031e..c26188aec 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_nl.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_nl.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Getal 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Ja/Nee]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pl.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pl.properties.xml
index 2cdfd524a..ef5555785 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pl.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pl.properties.xml
@@ -881,6 +881,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Czas wysyłania]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Sklonuj]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Liczba 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Tak/Nie]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pt.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pt.properties.xml
index 04fe17abb..019ca2670 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pt.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pt.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numérico 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Sim/Não]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pt_BR.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pt_BR.properties.xml
index a85a08612..4f90f84d5 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pt_BR.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_pt_BR.properties.xml
@@ -881,6 +881,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numérico 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Sim/Não]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ru.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ru.properties.xml
index f67309add..4c5ff7652 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ru.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ru.properties.xml
@@ -882,6 +882,16 @@ see https://openmeetings.apache.org/LanguageEditor.html for Details
 	<entry key="network.test.upl.time"><![CDATA[Время загрузки]]></entry>
 	<entry key="notification.chat.message"><![CDATA[У Вас есть новые сообщения в чате]]></entry>
 	<entry key="notification.room.activity"><![CDATA[У Вас новый запрос, пожалуйста проверьте "Активность и действия"]]></entry>
+	<entry key="otp.disable"><![CDATA[Выключить одноразовые коды]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Вы уверены что хотите отключить вход по одноразовому коду? Уровень безопасности будет понижен.]]></entry>
+	<entry key="otp.enable"><![CDATA[Включить одноразовые коды]]></entry>
+	<entry key="otp.enabled"><![CDATA[Проверка одноразового кода включена]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Пожалуйста сохраните эти одноразовые пароли в секретном месте, ими можно будет воспользоваться если Вы потеряете доступ к своему мобильному.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Неверный запасной код]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Сохранённый код]]></entry>
+	<entry key="otp.invalid"><![CDATA[Неверный код]]></entry>
+	<entry key="otp.label"><![CDATA[Одноразовый код]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Пожалуйста откройте приложение Google Authenticator и просканируйте этот код.]]></entry>
 	<entry key="poll.clone"><![CDATA[Клонировать]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Число 1 - 10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Да/Нет]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_sk.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_sk.properties.xml
index be7995299..9f07850bd 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_sk.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_sk.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Čísla 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Áno/Nie]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_sv.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_sv.properties.xml
index 246f18898..346962bf4 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_sv.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_sv.properties.xml
@@ -888,6 +888,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Uppladningstid]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Klona]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numerisk 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Ja/Nej]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ta.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ta.properties.xml
index 86d319261..d73953d50 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ta.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ta.properties.xml
@@ -892,6 +892,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numeric 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Yes/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_th.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_th.properties.xml
index 5e217632a..438b1d916 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_th.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_th.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[เลข 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[ใช่/ไม่]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_tk.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_tk.properties.xml
index f8197519a..894acb1c0 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_tk.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_tk.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numeric 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Yes/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_tr.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_tr.properties.xml
index eb84ca12a..21ab30137 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_tr.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_tr.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Sayısal 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Evet/Hayır]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_uk.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_uk.properties.xml
index 6e6fc2cd8..9a796ebfb 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_uk.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_uk.properties.xml
@@ -881,6 +881,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[В числовому порядку 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Так/Ні]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ur.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ur.properties.xml
index dd9f98409..81ac794cd 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ur.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_ur.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[Numeric 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[Yes/No]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_zh_CN.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_zh_CN.properties.xml
index 4bdce2246..6428f69d2 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_zh_CN.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_zh_CN.properties.xml
@@ -882,6 +882,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[数字 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[是/否]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_zh_TW.properties.xml b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_zh_TW.properties.xml
index e154396c7..64c126d22 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_zh_TW.properties.xml
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application_zh_TW.properties.xml
@@ -890,6 +890,16 @@ please check <tt>openmeetings.log</tt> and contact OpenMeetings developers]]></e
 	<entry key="network.test.upl.time"><![CDATA[Upload time]]></entry>
 	<entry key="notification.chat.message"><![CDATA[You have new chat message(s)]]></entry>
 	<entry key="notification.room.activity"><![CDATA[You have new activities, please check "Activities&Action"]]></entry>
+	<entry key="otp.disable"><![CDATA[Disable One Time Password]]></entry>
+	<entry key="otp.disable.confirm"><![CDATA[Are you sure you want to disable OTP? Security level will be lowered.]]></entry>
+	<entry key="otp.enable"><![CDATA[Enable One Time Password]]></entry>
+	<entry key="otp.enabled"><![CDATA[One Time Password enabled]]></entry>
+	<entry key="otp.fallback.desc"><![CDATA[Please save these one-time codes in secret place, they can be used in case you will loose access to your mobile device.]]></entry>
+	<entry key="otp.fallback.invalid"><![CDATA[Wrong Recovery code]]></entry>
+	<entry key="otp.fallback.label"><![CDATA[Recovery code]]></entry>
+	<entry key="otp.invalid"><![CDATA[Wrong One Time Password]]></entry>
+	<entry key="otp.label"><![CDATA[One Time Password]]></entry>
+	<entry key="otp.qr.desc"><![CDATA[Please open Google Authenticator and scan this QR code]]></entry>
 	<entry key="poll.clone"><![CDATA[Clone]]></entry>
 	<entry key="poll.type.NUMERIC"><![CDATA[數字的 1-10]]></entry>
 	<entry key="poll.type.YES_NO"><![CDATA[是/否]]></entry>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/OtpManager.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/OtpManager.java
new file mode 100644
index 000000000..8715cde17
--- /dev/null
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/OtpManager.java
@@ -0,0 +1,108 @@
+/*
+
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") +  you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openmeetings.web.app;
+
+import static dev.samstevens.totp.util.Utils.getDataUriForImage;
+import static org.apache.openmeetings.util.OpenmeetingsVariables.getApplicationName;
+
+import java.net.UnknownHostException;
+
+import javax.annotation.PostConstruct;
+
+import static org.apache.openmeetings.util.OmFileHelper.PNG_MIME_TYPE;
+
+import org.apache.wicket.util.string.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import dev.samstevens.totp.code.CodeGenerator;
+import dev.samstevens.totp.code.CodeVerifier;
+import dev.samstevens.totp.code.DefaultCodeGenerator;
+import dev.samstevens.totp.code.DefaultCodeVerifier;
+import dev.samstevens.totp.code.HashingAlgorithm;
+import dev.samstevens.totp.exceptions.QrGenerationException;
+import dev.samstevens.totp.qr.QrData;
+import dev.samstevens.totp.qr.QrGenerator;
+import dev.samstevens.totp.qr.ZxingPngQrGenerator;
+import dev.samstevens.totp.recovery.RecoveryCodeGenerator;
+import dev.samstevens.totp.secret.DefaultSecretGenerator;
+import dev.samstevens.totp.secret.SecretGenerator;
+import dev.samstevens.totp.time.NtpTimeProvider;
+
+@Service
+public class OtpManager {
+	private static final Logger log = LoggerFactory.getLogger(OtpManager.class);
+	// these properties are hardcoded into Google Authenticator :(
+	private static final int digits = 6;
+	private static final int period = 30;
+	private static final HashingAlgorithm alg = HashingAlgorithm.SHA1;
+
+	private final SecretGenerator secretGenerator = new DefaultSecretGenerator(128);
+	private CodeGenerator codeGenerator;
+	private CodeVerifier codeVerifier;
+	@Value("${otp.issuer}")
+	private String issuer = "";
+	@Value("${otp.ntp.server}")
+	private String ntpServer = "pool.ntp.org";
+	@Value("${otp.ntp.timeout}")
+	private int ntpTimeout = 6;
+
+	@PostConstruct
+	public void init() throws UnknownHostException {
+		codeGenerator = new DefaultCodeGenerator(alg, digits);
+		final DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, new NtpTimeProvider(ntpServer, ntpTimeout));
+		verifier.setTimePeriod(period);
+		codeVerifier = verifier;
+	}
+
+	public String generateSecret() {
+		return secretGenerator.generate();
+	}
+
+	public String getQr(String userEmail, String secret) {
+		QrData data = new QrData.Builder()
+			.label(userEmail)
+			.secret(secret)
+			.issuer(Strings.isEmpty(issuer) ? getApplicationName() : issuer)
+			.algorithm(alg)
+			.digits(digits)
+			.period(period)
+			.build();
+		QrGenerator generator = new ZxingPngQrGenerator();
+		try {
+			byte[] imageData = generator.generate(data);
+			return getDataUriForImage(imageData, PNG_MIME_TYPE);
+		} catch (QrGenerationException e) {
+			log.error("Unexpected exception while generating QR", e);
+		}
+		return "";
+	}
+
+	public String[] getRecoveryCodes() {
+		RecoveryCodeGenerator recoveryCodes = new RecoveryCodeGenerator();
+		return recoveryCodes.generateCodes(16);
+	}
+
+	public boolean verify(String secret, String code) {
+		return codeVerifier.isValidCode(secret, code);
+	}
+}
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/Captcha.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/Captcha.java
index eec5dac68..e5a6d3827 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/Captcha.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/Captcha.java
@@ -41,7 +41,7 @@ import org.apache.wicket.validation.ValidationError;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
 import de.agilecoders.wicket.core.markup.html.bootstrap.image.Icon;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public class Captcha extends Panel {
 	private static final long serialVersionUID = 1L;
@@ -105,7 +105,7 @@ public class Captcha extends Panel {
 
 			@Override
 			protected Icon newIcon(String markupId) {
-				return new Icon(markupId, FontAwesome5IconType.sync_s);
+				return new Icon(markupId, FontAwesome6IconType.rotate_s);
 			}
 		});
 	}
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/CommunityUserForm.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/CommunityUserForm.html
index bdb2c885a..8aad69dab 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/CommunityUserForm.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/CommunityUserForm.html
@@ -24,21 +24,21 @@
 	<div wicket:id="community_settings">
 		<div>
 			<div class="d-inline-block col-3"></div>
-			<div class="d-inline-block col-8">
+			<div class="d-inline-block col-8 form-check">
 				<input wicket:id="everybody" type="radio" class="form-check-input me-2"/>
 				<label wicket:for="everybody" class="form-check-label"><wicket:message key="1160"/></label>
 			</div>
 		</div>
 		<div>
 			<div class="d-inline-block col-3"></div>
-			<div class="d-inline-block col-8">
+			<div class="d-inline-block col-8 form-check">
 				<input wicket:id="contact" type="radio" class="form-check-input me-2"/>
 				<label wicket:for="contact" class="form-check-label"><wicket:message key="1168"/></label>
 			</div>
 		</div>
 		<div>
 			<div class="d-inline-block col-3"></div>
-			<div class="d-inline-block col-8">
+			<div class="d-inline-block col-8 form-check">
 				<input wicket:id="nobody" type="radio" class="form-check-input me-2"/>
 				<label wicket:for="nobody" class="form-check-label"><wicket:message key="1169"/></label>
 			</div>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/UploadableImagePanel.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/UploadableImagePanel.html
index 5730cc680..568fa706a 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/UploadableImagePanel.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/UploadableImagePanel.html
@@ -21,7 +21,7 @@
 <!DOCTYPE html>
 <html xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-9.xsd">
 <wicket:extend>
-	<button type="button" class="btn btn-xs btn-outline-secondary remove" wicket:id="remove">
+	<button type="button" class="btn btn-xs btn-outline-secondary remove" wicket:id="remove" wicket:message="title:80">
 		<span aria-hidden="true">×</span>
 	</button>
 	<form wicket:id="form" class="img-upload">
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/UploadableImagePanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/UploadableImagePanel.java
index 959f212a6..c75aed681 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/UploadableImagePanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/UploadableImagePanel.java
@@ -44,7 +44,7 @@ import org.slf4j.LoggerFactory;
 
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public abstract class UploadableImagePanel extends ImagePanel {
 	private static final long serialVersionUID = 1L;
@@ -90,7 +90,7 @@ public abstract class UploadableImagePanel extends ImagePanel {
 				}
 			};
 			add(remove
-					.setIconType(FontAwesome5IconType.times_s)
+					.setIconType(FontAwesome6IconType.xmark_s)
 					.add(newOkCancelConfirm(this, getString("833"))));
 			fileUploadField.add(new AjaxFormSubmitBehavior(form, "change") {
 				private static final long serialVersionUID = 1L;
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/datetime/AbstractOmDateTimePicker.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/datetime/AbstractOmDateTimePicker.java
index 8659b455d..431851fbc 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/datetime/AbstractOmDateTimePicker.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/datetime/AbstractOmDateTimePicker.java
@@ -40,7 +40,7 @@ import org.apache.wicket.request.resource.ResourceReference;
 import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.datetime.AbstractDateTimePickerWithIcon;
 import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.datetime.DatetimePickerConfig;
 import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.datetime.DatetimePickerIconConfig;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public abstract class AbstractOmDateTimePicker<T extends Serializable> extends FormComponentPanel<T> {
 	private static final long serialVersionUID = 1L;
@@ -66,13 +66,13 @@ public abstract class AbstractOmDateTimePicker<T extends Serializable> extends F
 				.useLocale(WebSession.get().getLocale().toLanguageTag())
 				.withFormat(patch(format))
 				.withKeepInvalid(true)
-				.with(new DatetimePickerIconConfig().useDateIcon(FontAwesome5IconType.calendar_s)
-						.useTimeIcon(FontAwesome5IconType.clock_s).useUpIcon(FontAwesome5IconType.arrow_up_s)
-						.useDownIcon(FontAwesome5IconType.arrow_down_s)
-						.usePreviousIcon(FontAwesome5IconType.arrow_left_s)
-						.useNextIcon(FontAwesome5IconType.arrow_right_s)
-						.useTodayIcon(FontAwesome5IconType.calendar_check_s).useClearIcon(FontAwesome5IconType.eraser_s)
-						.useCloseIcon(FontAwesome5IconType.times_s));
+				.with(new DatetimePickerIconConfig().useDateIcon(FontAwesome6IconType.calendar_s)
+						.useTimeIcon(FontAwesome6IconType.clock_s).useUpIcon(FontAwesome6IconType.arrow_up_s)
+						.useDownIcon(FontAwesome6IconType.arrow_down_s)
+						.usePreviousIcon(FontAwesome6IconType.arrow_left_s)
+						.useNextIcon(FontAwesome6IconType.arrow_right_s)
+						.useTodayIcon(FontAwesome6IconType.calendar_check_s).useClearIcon(FontAwesome6IconType.eraser_s)
+						.useCloseIcon(FontAwesome6IconType.xmark_s));
 		picker = new AbstractDateTimePickerWithIcon<>("picker", new Model<>(getModelObject()), config) {
 			private static final long serialVersionUID = 1L;
 
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/tree/FileTreePanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/tree/FileTreePanel.java
index 9c4afcde8..5fe0f2a10 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/tree/FileTreePanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/tree/FileTreePanel.java
@@ -85,7 +85,7 @@ import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.dropdown.SplitButton;
 import de.agilecoders.wicket.core.markup.html.bootstrap.image.IconType;
 import de.agilecoders.wicket.extensions.markup.html.bootstrap.confirmation.ConfirmationBehavior;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public abstract class FileTreePanel extends Panel {
 	private static final long serialVersionUID = 1L;
@@ -266,7 +266,7 @@ public abstract class FileTreePanel extends Panel {
 					public void onClick(AjaxRequestTarget target) {
 						onDownlownClick(target, ext);
 					}
-				}.setIconType(FontAwesome5IconType.download_s);
+				}.setIconType(FontAwesome6IconType.download_s);
 			}
 
 			@Override
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/upload/UploadForm.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/upload/UploadForm.java
index e67e6de61..55737ef63 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/upload/UploadForm.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/upload/UploadForm.java
@@ -71,7 +71,7 @@ public abstract class UploadForm extends Panel {
 		// set max upload size in form as info text
 		Long maxBytes = getMaxUploadSize();
 		double megaBytes = maxBytes.doubleValue() / 1024 / 1024;
-		DecimalFormat formatter = new DecimalFormat("#,###.00"); //FIXME TODO locale based format
+		DecimalFormat formatter = new DecimalFormat("#,###.00");
 		form.add(new Label("MaxUploadSize", formatter.format(megaBytes)));
 		form.add(new Label("btn-label", new ResourceModel(buttonLabelKey())));
 
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/BasePage.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/BasePage.java
index 6e4890f33..00501de1f 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/BasePage.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/BasePage.java
@@ -53,7 +53,7 @@ import org.apache.wicket.util.string.StringValue;
 import org.apache.wicket.util.string.Strings;
 import org.wicketstuff.urlfragment.AsyncUrlFragmentAwarePage;
 
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5CssReference;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6CssReference;
 
 public abstract class BasePage extends AsyncUrlFragmentAwarePage {
 	private static final long serialVersionUID = 1L;
@@ -144,7 +144,7 @@ public abstract class BasePage extends AsyncUrlFragmentAwarePage {
 					.append(getGaCode()).append("', ").append(isMainPage()).append(");");
 			response.render(OnDomReadyHeaderItem.forScript(script));
 		}
-		response.render(CssHeaderItem.forReference(FontAwesome5CssReference.instance()));
+		response.render(CssHeaderItem.forReference(FontAwesome6CssReference.instance()));
 		response.render(new FilteredHeaderItem(CssHeaderItem.forUrl("css/custom.css"), CUSTOM_CSS_FILTER));
 	}
 
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/ResetPage.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/ResetPage.java
index b0630b997..84c1d647b 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/ResetPage.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/ResetPage.java
@@ -50,7 +50,7 @@ public class ResetPage extends BaseNotInitedPage {
 		if (resetHash != null) {
 			User user = userDao.getUserByHash(resetHash);
 			if (user != null) {
-				add(new ResetPasswordDialog("resetPassword", user, resetInfo));
+				add(new ResetPasswordDialog("resetPassword", user));
 				add(resetInfo.header(new ResourceModel("325"))
 						.addButton(OmModalCloseButton.of("54"))
 						.setUseCloseHandler(true));
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/ForgetPasswordDialog.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/ForgetPasswordDialog.java
index baf1e8440..fe26e94ce 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/ForgetPasswordDialog.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/ForgetPasswordDialog.java
@@ -72,8 +72,6 @@ public class ForgetPasswordDialog extends Modal<String> {
 	private final WebMarkupContainer label = new WebMarkupContainer("label");
 	private final Captcha captcha = new Captcha("captcha");
 	private ForgetPasswordForm form = new ForgetPasswordForm("form");
-	private SignInDialog s;
-	private final Modal<String> forgetInfoDialog;
 	private boolean wasReset = false;
 
 	@SpringBean
@@ -86,9 +84,8 @@ public class ForgetPasswordDialog extends Modal<String> {
 		, LOGIN
 	}
 
-	public ForgetPasswordDialog(String id, Modal<String> forgetInfoDialog) {
+	public ForgetPasswordDialog(String id) {
 		super(id);
-		this.forgetInfoDialog = forgetInfoDialog;
 	}
 
 	@Override
@@ -131,16 +128,15 @@ public class ForgetPasswordDialog extends Modal<String> {
 	@Override
 	public void onClose(IPartialPageRequestHandler handler) {
 		if (wasReset) {
-			forgetInfoDialog.show(handler);
+			@SuppressWarnings("unchecked")
+			Modal<String> forgetInfo = (Modal<String>)getPage().get("forgetInfo");
+			forgetInfo.show(handler);
 		} else {
-			s.show(handler);
+			SignInDialog signin = (SignInDialog)getPage().get("signin");
+			signin.show(handler);
 		}
 	}
 
-	public void setSignInDialog(SignInDialog s) {
-		this.s = s;
-	}
-
 	private class ForgetPasswordForm extends Form<String> {
 		private static final long serialVersionUID = 1L;
 
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/OtpDialog.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/OtpDialog.html
new file mode 100644
index 000000000..a7af19d91
--- /dev/null
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/OtpDialog.html
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+
+-->
+<!DOCTYPE html>
+<html xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-9.xsd">
+<wicket:extend>
+	<form wicket:id="form" class="signin-forget">
+		<div class="row mb-1 ms-2 me-2 g-0" wicket:id="type">
+			<div class="col-6">
+				<div class="form-check form-check-inline">
+					<label class="form-check-label" wicket:for="type-otp"><wicket:message key="otp.label" /></label>
+					<input class="form-check-input" type="radio" wicket:id="type-otp"/>
+				</div>
+			</div>
+			<div class="col-5">
+				<div class="form-check form-check-inline">
+					<label class="form-check-label" wicket:for="type-fallback"><wicket:message key="otp.fallback.label" /></label>
+					<input class="form-check-input" type="radio" wicket:id="type-fallback"/>
+				</div>
+			</div>
+		</div>
+		<div class="input-group mb-1 g-0" wicket:id="otp-block">
+			<span class="input-group-text"><i class="fa fa-key"></i></span>
+			<input wicket:id="otp" class="form-control auto-focus" type="number" value="" maxlength="6"/>
+		</div>
+		<div class="input-group mb-1 g-0" wicket:id="fallback-block">
+			<span class="input-group-text"><i class="fa fa-key"></i></span>
+			<input wicket:id="fallback" class="form-control auto-focus" type="text" value=""/>
+		</div>
+		<div wicket:id="feedback"></div>
+		<input type="submit" wicket:id="submit" hidden="hidden"/>
+	</form>
+</wicket:extend>
+</html>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/OtpDialog.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/OtpDialog.java
new file mode 100644
index 000000000..682229d8b
--- /dev/null
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/OtpDialog.java
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") +  you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openmeetings.web.pages.auth;
+
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.openmeetings.db.dao.user.UserDao;
+import org.apache.openmeetings.db.entity.user.User;
+import org.apache.openmeetings.db.entity.user.User.Type;
+import org.apache.openmeetings.web.app.OtpManager;
+import org.apache.openmeetings.web.app.WebSession;
+import org.apache.openmeetings.web.common.OmModalCloseButton;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.Radio;
+import org.apache.wicket.markup.html.form.RadioGroup;
+import org.apache.wicket.markup.html.form.RequiredTextField;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.ResourceModel;
+import org.apache.wicket.request.cycle.RequestCycle;
+import org.apache.wicket.spring.injection.annot.SpringBean;
+import org.apache.wicket.util.string.Strings;
+
+import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
+import de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel;
+import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.spinner.SpinnerAjaxButton;
+
+public class OtpDialog extends Modal<User> {
+	private static final long serialVersionUID = 1L;
+	private static final String TYPE_OTP = "type-otp";
+	private static final String TYPE_FALLBACK = "type-fallback";
+	private final NotificationPanel feedback = new NotificationPanel("feedback");
+	private final OtpForm form = new OtpForm("form");
+	private final RadioGroup<String> radioGroup = new RadioGroup<>("type", Model.of(TYPE_OTP));
+	@SpringBean
+	private OtpManager otpManager;
+	@SpringBean
+	private UserDao userDao;
+
+	public OtpDialog(String id, IModel<User> model) {
+		super(id, model);
+	}
+
+	@Override
+	protected void onInitialize() {
+		header(new ResourceModel("otp.label"));
+		setUseCloseHandler(true);
+
+		addButton(new SpinnerAjaxButton(BUTTON_MARKUP_ID, new ResourceModel("54"), form, Buttons.Type.Outline_Primary)); // OK
+		addButton(OmModalCloseButton.of());
+
+		add(form);
+		super.onInitialize();
+	}
+
+	@Override
+	public Modal<User> show(IPartialPageRequestHandler handler) {
+		form.reset(handler);
+		return super.show(handler);
+	}
+
+	private SignInDialog getSignin() {
+		return (SignInDialog)getPage().get("signin");
+	}
+
+	@Override
+	public void onClose(IPartialPageRequestHandler handler) {
+		getSignin().show(handler);
+	}
+
+	private class OtpForm extends Form<String> {
+		private static final long serialVersionUID = 1L;
+		private final WebMarkupContainer otpBlock = new WebMarkupContainer("otp-block");
+		private final WebMarkupContainer fallbackBlock = new WebMarkupContainer("fallback-block");
+		private final RequiredTextField<String> otpField = new RequiredTextField<>("otp", Model.of("")) {
+			private static final long serialVersionUID = 1L;
+
+			protected String[] getInputTypes() {
+				return new String[]{"number"};
+			};
+		};
+		private final RequiredTextField<String> fallbackField = new RequiredTextField<>("fallback", Model.of(""));
+
+		public OtpForm(String id) {
+			super(id);
+		}
+
+		@Override
+		protected void onInitialize() {
+			super.onInitialize();
+			add(feedback.setOutputMarkupId(true));
+			add(otpBlock.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
+			otpBlock.add(otpField.setLabel(new ResourceModel("otp.label")));
+			add(fallbackBlock.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
+			fallbackBlock.add(fallbackField.setLabel(new ResourceModel("otp.fallback.label")));
+			add(radioGroup.add(new Radio<>(TYPE_OTP, Model.of(TYPE_OTP)))
+					.add(new Radio<>(TYPE_FALLBACK, Model.of(TYPE_FALLBACK)))
+					.setOutputMarkupId(true));
+			radioGroup.add(new AjaxFormChoiceComponentUpdatingBehavior() {
+				private static final long serialVersionUID = 1L;
+
+				@Override
+				protected void onUpdate(AjaxRequestTarget target) {
+					updateForm(target);
+				}
+			});
+			add(new AjaxButton("submit") { //FAKE button so "submit-on-enter" works as expected
+				private static final long serialVersionUID = 1L;
+
+				@Override
+				protected void onSubmit(AjaxRequestTarget target) {
+					OtpForm.this.onSubmit(target);
+				}
+
+				@Override
+				protected void onError(AjaxRequestTarget target) {
+					OtpForm.this.onError(target);
+				}
+			});
+		}
+
+		@Override
+		protected void onError() {
+			RequestCycle.get().find(AjaxRequestTarget.class).ifPresent(this::onError);
+		}
+
+		private void onError(AjaxRequestTarget target) {
+			SignInDialog.penalty();
+			target.add(feedback);
+		}
+
+		@Override
+		protected void onSubmit() {
+			RequestCycle.get().find(AjaxRequestTarget.class).ifPresent(this::onSubmit);
+		}
+
+		@Override
+		protected void onValidate() {
+			final User u = OtpDialog.this.getModelObject();
+			String otp = otpField.getConvertedInput();
+			try {
+				Integer.valueOf(otp);
+				if (otpManager.verify(u.getOtpSecret(), otp)) {
+					return;
+				} else {
+					error(getString("otp.invalid"));
+				}
+			} catch (NumberFormatException e) {
+				// fallback
+				String fallback = fallbackField.getConvertedInput();
+				final String[] codes = Optional.of(u.getOtpRecoveryCodes())
+						.map(str -> str.split(" "))
+						.orElse(new String[]{});
+				if (Stream.of(codes).anyMatch(str -> str.equalsIgnoreCase(fallback))) {
+					// match found
+					// let's remove recovery code
+					u.setOtpRecoveryCodes(Stream.of(codes)
+							.filter(str -> !Strings.isEmpty(str))
+							.filter(str -> !str.equalsIgnoreCase(fallback))
+							.collect(Collectors.joining(" "))
+							);
+					userDao.update(u, u.getId());
+					return;
+				}
+				error(getString("otp.fallback.invalid"));
+			}
+		}
+
+		private void onSubmit(AjaxRequestTarget target) {
+			final User u = OtpDialog.this.getModelObject();
+			WebSession ws = WebSession.get();
+			ws.signIn(u);
+			getSignin().finalStep(true, Type.USER, target);
+		}
+
+		private void updateForm(IPartialPageRequestHandler handler) {
+			final boolean isOtp = TYPE_OTP.equals(radioGroup.getModelObject());
+			otpBlock.setVisible(isOtp);
+			fallbackBlock.setVisible(!isOtp);
+			handler.add(otpBlock, fallbackBlock);
+		}
+
+		private void reset(IPartialPageRequestHandler handler) {
+			radioGroup.setModelObject(TYPE_OTP);
+			otpField.setModelObject("");
+			fallbackField.setModelObject("");
+			handler.add(radioGroup);
+			updateForm(handler);
+		}
+	}
+}
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/RegisterDialog.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/RegisterDialog.java
index f026a54a8..0ec636d04 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/RegisterDialog.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/RegisterDialog.java
@@ -65,7 +65,6 @@ public class RegisterDialog extends Modal<String> {
 	private final NotificationPanel feedback = new NotificationPanel("feedback");
 	private final IModel<String> tzModel = Model.of(WebSession.get().getClientTZCode());
 	private final RegisterForm form = new RegisterForm("form");
-	private SignInDialog s;
 	private Captcha captcha;
 	private String firstName;
 	private String lastName;
@@ -76,15 +75,13 @@ public class RegisterDialog extends Modal<String> {
 	private Long lang;
 	private boolean wasRegistered = false;
 
-	private final Modal<String> registerInfo;
 	@SpringBean
 	private IUserManager userManager;
 	@SpringBean
 	private UserDao userDao;
 
-	public RegisterDialog(String id, Modal<String> registerInfo) {
+	public RegisterDialog(String id) {
 		super(id);
-		this.registerInfo = registerInfo;
 	}
 
 	@Override
@@ -100,10 +97,6 @@ public class RegisterDialog extends Modal<String> {
 		super.onInitialize();
 	}
 
-	public void setSignInDialog(SignInDialog s) {
-		this.s = s;
-	}
-
 	public void setClientTimeZone() {
 		tzModel.setObject(WebSession.get().getClientTZCode());
 	}
@@ -130,6 +123,7 @@ public class RegisterDialog extends Modal<String> {
 		if (sendConfirmation && sendEmailAtRegister) {
 			messageCode = "warn.notverified";
 		}
+		Modal<String> registerInfo = getRegisterInfo();
 		registerInfo.setModelObject(getString(messageCode));
 		handler.add(registerInfo.get("content"));
 		reset(handler);
@@ -140,7 +134,8 @@ public class RegisterDialog extends Modal<String> {
 	@Override
 	public void onClose(IPartialPageRequestHandler handler) {
 		if (!wasRegistered) {
-			s.show(handler);
+			SignInDialog signin = (SignInDialog)getPage().get("signin");
+			signin.show(handler);
 		}
 	}
 
@@ -150,6 +145,11 @@ public class RegisterDialog extends Modal<String> {
 		super.onDetach();
 	}
 
+	@SuppressWarnings("unchecked")
+	private Modal<String> getRegisterInfo() {
+		return (Modal<String>)getPage().get("registerInfo");
+	}
+
 	class RegisterForm extends StatelessForm<Void> {
 		private static final long serialVersionUID = 1L;
 		private PasswordTextField confirmPassword;
@@ -219,13 +219,7 @@ public class RegisterDialog extends Modal<String> {
 				error(getString("error.login.inuse"));
 			}
 			if (hasErrorMessage()) {
-				// add random timeout
-				try {
-					Thread.sleep((long)(10 * Math.random() * 1000));
-				} catch (InterruptedException e) {
-					log.error("Unexpected exception while sleeting", e);
-					Thread.currentThread().interrupt();
-				}
+				SignInDialog.penalty();
 			}
 		}
 
@@ -244,6 +238,7 @@ public class RegisterDialog extends Modal<String> {
 		}
 
 		private void onSubmit(AjaxRequestTarget target) {
+			Modal<String> registerInfo = getRegisterInfo();
 			wasRegistered = true;
 			try {
 				Object o = userManager.registerUser(login, password, lastName
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/ResetPasswordDialog.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/ResetPasswordDialog.java
index b25bf8edc..dff43f077 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/ResetPasswordDialog.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/ResetPasswordDialog.java
@@ -42,15 +42,13 @@ public class ResetPasswordDialog extends Modal<String> {
 	private final NotificationPanel feedback = new NotificationPanel("feedback");
 	private PasswordTextField password;
 	private final User user;
-	private final Modal<String> resetInfo;
 
 	@SpringBean
 	private UserDao userDao;
 
-	public ResetPasswordDialog(String id, final User user, Modal<String> resetInfo) {
+	public ResetPasswordDialog(String id, final User user) {
 		super(id);
 		this.user = user;
-		this.resetInfo = resetInfo;
 	}
 
 	@Override
@@ -132,6 +130,8 @@ public class ResetPasswordDialog extends Modal<String> {
 			try {
 				userDao.resetPassword(user, password.getConvertedInput());
 				ResetPasswordDialog.this.close(target);
+				@SuppressWarnings("unchecked")
+				Modal<String> resetInfo = (Modal<String>)getPage().get("resetInfo");
 				resetInfo.show(target);
 			} catch (Exception e) {
 				error(e.getMessage());
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInDialog.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInDialog.java
index 8765fcd44..06b9320a1 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInDialog.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInDialog.java
@@ -19,6 +19,7 @@
 package org.apache.openmeetings.web.pages.auth;
 
 import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_DEFAULT_LDAP_ID;
+import static org.apache.openmeetings.util.OpenmeetingsVariables.isOtpEnabled;
 import static org.apache.openmeetings.web.app.Application.getAuthenticationStrategy;
 import static org.apache.openmeetings.web.app.UserManager.showAuth;
 import static org.apache.openmeetings.web.pages.HashPage.APP;
@@ -29,8 +30,10 @@ import java.util.List;
 import org.apache.openmeetings.db.dao.basic.ConfigurationDao;
 import org.apache.openmeetings.db.dao.server.LdapConfigDao;
 import org.apache.openmeetings.db.dao.server.OAuth2Dao;
+import org.apache.openmeetings.db.dao.user.UserDao;
 import org.apache.openmeetings.db.entity.server.LdapConfig;
 import org.apache.openmeetings.db.entity.server.OAuthServer;
+import org.apache.openmeetings.db.entity.user.User;
 import org.apache.openmeetings.db.entity.user.User.Type;
 import org.apache.openmeetings.util.OmException;
 import org.apache.openmeetings.util.OpenmeetingsVariables;
@@ -82,8 +85,6 @@ public class SignInDialog extends Modal<String> {
 	private final PasswordTextField passField = new PasswordTextField("pass", Model.of(""));
 	private final RequiredTextField<String> loginField = new RequiredTextField<>("login", Model.of(""));
 	private boolean rememberMe = false;
-	private RegisterDialog register;
-	private ForgetPasswordDialog f;
 	private LdapConfig domain;
 	private NotificationPanel feedback = new NotificationPanel("feedback");
 	@SpringBean
@@ -92,6 +93,8 @@ public class SignInDialog extends Modal<String> {
 	private LdapConfigDao ldapDao;
 	@SpringBean
 	private OAuth2Dao oauthDao;
+	@SpringBean
+	private UserDao userDao;
 
 	public SignInDialog(String id) {
 		super(id);
@@ -112,11 +115,14 @@ public class SignInDialog extends Modal<String> {
 
 			public void onClick(AjaxRequestTarget target) {
 				SignInDialog.this.close(target);
+
+				RegisterDialog register = (RegisterDialog)getPage().get("register");
 				register.setClientTimeZone();
 				register.show(target);
 			}
 		}.setVisible(OpenmeetingsVariables.isAllowRegisterFrontend()));
 
+
 		super.onInitialize();
 	}
 
@@ -125,14 +131,6 @@ public class SignInDialog extends Modal<String> {
 		return super.createHeaderCloseButton(id).setVisible(false);
 	}
 
-	public void setRegisterDialog(RegisterDialog r) {
-		this.register = r;
-	}
-
-	public void setForgetPasswordDialog(ForgetPasswordDialog f) {
-		this.f = f;
-	}
-
 	class SignInForm extends StatelessForm<String> {
 		private static final long serialVersionUID = 1L;
 		private final WebMarkupContainer credentials = new WebMarkupContainer("credentials");
@@ -170,7 +168,9 @@ public class SignInDialog extends Modal<String> {
 				@Override
 				public void onClick(AjaxRequestTarget target) {
 					SignInDialog.this.close(target);
-					f.show(target);
+
+					ForgetPasswordDialog forget = (ForgetPasswordDialog)getPage().get("forget");
+					forget.show(target);
 				}
 			});
 			add(new WebMarkupContainer("netTest").add(AttributeModifier.append("href"
@@ -241,41 +241,86 @@ public class SignInDialog extends Modal<String> {
 			RequestCycle.get().find(AjaxRequestTarget.class).ifPresent(this::onSubmit);
 		}
 
+		private void processOtp(final String login, AjaxRequestTarget target) {
+			boolean signedIn = false;
+			boolean checkOtp = false;
+			final String password = passField.getModelObject();
+			User u = null;
+			try {
+				u = userDao.login(login, password);
+				signedIn = u != null;
+				checkOtp = signedIn && u.getOtpSecret() != null;
+			} catch (OmException e) {
+				error(getString(e.getKey()));
+				target.add(feedback);
+			}
+			if (signedIn) {
+				if (checkOtp) {
+					OtpDialog otp = (OtpDialog)getPage().get("otpDialog");
+					otp.setModelObject(u);
+					SignInDialog.this.close(target);
+					otp.show(target);
+					return;
+				}
+				WebSession ws = WebSession.get();
+				ws.signIn(u);
+			}
+			finalStep(signedIn, Type.USER, target);
+		}
+
 		protected void onSubmit(AjaxRequestTarget target) {
-			final String login = String.format(domain.getAddDomainToUserName() ? "%s@%s" : "%s"
-					, loginField.getModelObject(), domain.getDomain());
+			final String login = getLogin();
 			final String password = passField.getModelObject();
-			OmAuthenticationStrategy strategy = getAuthenticationStrategy();
 			WebSession ws = WebSession.get();
 			Type type = domain.getId() > 0 ? Type.LDAP : Type.USER;
-			boolean signIn = false;
+			if (isOtpEnabled() && Type.USER == type) {
+				processOtp(login, target);
+				return;
+			}
+			boolean signedIn = false;
 			try {
-				signIn = ws.signIn(login, password, type, domain.getId());
+				signedIn = ws.signIn(login, password, type, domain.getId());
 			} catch (OmException e) {
 				error(getString(e.getKey()));
 				target.add(feedback);
 			}
-			if (signIn) {
-				setResponsePage(Application.get().getHomePage());
-				if (rememberMe) {
-					strategy.save(login, password, type, domain.getId());
-				} else {
-					strategy.remove();
-				}
+			finalStep(signedIn, type, target);
+		}
+
+	}
+
+	private String getLogin() {
+		return String.format(domain.getAddDomainToUserName() ? "%s@%s" : "%s", loginField.getModelObject(), domain.getDomain());
+	}
+
+	// package private for OTP-Dialog
+	void finalStep(boolean signedIn, final Type type, AjaxRequestTarget target) {
+		OmAuthenticationStrategy strategy = getAuthenticationStrategy();
+		if (signedIn) {
+			setResponsePage(Application.get().getHomePage());
+			if (rememberMe) {
+				final String login = getLogin();
+				strategy.save(login, passField.getModelObject(), type, domain.getId());
 			} else {
-				if (!hasErrorMessage()) {
-					error(getString("error.bad.credentials"));
-					target.add(feedback);
-				}
-				// add random timeout
-				try {
-					Thread.sleep(6 + (long)(10 * Math.random() * 1000));
-				} catch (InterruptedException e) {
-					log.error("Unexpected exception while sleeping", e);
-					Thread.currentThread().interrupt();
-				}
 				strategy.remove();
 			}
+		} else {
+			if (!hasErrorMessage()) {
+				error(getString("error.bad.credentials"));
+				target.add(feedback);
+			}
+			penalty();
+			strategy.remove();
+		}
+	}
+
+	public static void penalty() {
+		// add random timeout
+		try {
+			Thread.sleep(6 + (long)(10 * Math.random() * 1000));
+		} catch (InterruptedException e) {
+			log.error("Unexpected exception while sleeping", e);
+			Thread.currentThread().interrupt();
 		}
 	}
 }
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.html
index d381fce30..ec55be940 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.html
@@ -27,5 +27,6 @@
 	<div wicket:id="forget" id="forget-dialog"></div>
 	<div wicket:id="forgetInfo"></div>
 	<div wicket:id="kick"></div>
+	<div wicket:id="otpDialog" id="otp-dialog"></div>
 </wicket:extend>
 </html>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.java
index 3d15e66fc..71130f459 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.java
@@ -84,7 +84,7 @@ public class SignInPage extends BaseInitedPage {
 			signin.show(handler);
 		}
 	};
-	private final ForgetPasswordDialog forget = new ForgetPasswordDialog("forget", forgetInfoDialog);
+	private final ForgetPasswordDialog forget = new ForgetPasswordDialog("forget");
 	private final Modal<String> registerInfoDialog = new TextContentModal("registerInfo", Model.of("")) {
 		private static final long serialVersionUID = 1L;
 
@@ -107,7 +107,8 @@ public class SignInPage extends BaseInitedPage {
 			signin.show(handler);
 		}
 	};
-	RegisterDialog r = new RegisterDialog("register", registerInfoDialog);
+	RegisterDialog r = new RegisterDialog("register");
+	private final OtpDialog otpDialog = new OtpDialog("otpDialog", Model.of());
 	@SpringBean
 	private ConfigurationDao cfgDao;
 	@SpringBean
@@ -168,10 +169,6 @@ public class SignInPage extends BaseInitedPage {
 		super.onInitialize();
 
 		signin = new SignInDialog("signin");
-		signin.setRegisterDialog(r);
-		signin.setForgetPasswordDialog(forget);
-		r.setSignInDialog(signin);
-		forget.setSignInDialog(signin);
 		add(signin.setVisible(!WebSession.get().isKickedByAdmin()),
 				r.setVisible(allowRegister()), forget, kick.setVisible(WebSession.get().isKickedByAdmin()));
 		add(forgetInfoDialog
@@ -184,6 +181,7 @@ public class SignInPage extends BaseInitedPage {
 				.addButton(OmModalCloseButton.of("54"))
 				.setUseCloseHandler(true)
 		);
+		add(otpDialog);
 	}
 
 	@Override
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/IconTextModal.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/IconTextModal.java
index c7349e9ae..4103e8ec6 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/IconTextModal.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/IconTextModal.java
@@ -26,8 +26,8 @@ import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
 import de.agilecoders.wicket.core.markup.html.bootstrap.image.Icon;
 import de.agilecoders.wicket.core.markup.html.bootstrap.image.IconType;
 import de.agilecoders.wicket.core.markup.html.bootstrap.utilities.ColorBehavior;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconTypeBuilder;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconTypeBuilder.FontAwesome5Solid;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconTypeBuilder;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconTypeBuilder.FontAwesome6Solid;
 
 public class IconTextModal extends Modal<String> {
 	private static final long serialVersionUID = 1L;
@@ -69,8 +69,8 @@ public class IconTextModal extends Modal<String> {
 
 	public IconTextModal withErrorIcon(ColorBehavior.Color color) {
 		add(new ColorBehavior(color));
-		return withIcon(FontAwesome5IconTypeBuilder.on(FontAwesome5Solid.exclamation_triangle)
-			.size(FontAwesome5IconTypeBuilder.Size.three)
+		return withIcon(FontAwesome6IconTypeBuilder.on(FontAwesome6Solid.triangle_exclamation)
+			.size(FontAwesome6IconTypeBuilder.Size.three)
 			.build());
 	}
 }
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomMenuPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomMenuPanel.java
index 4c0829b1c..bba3616c5 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomMenuPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomMenuPanel.java
@@ -69,7 +69,7 @@ import org.apache.openmeetings.mediaserver.StreamProcessor;
 import com.github.openjson.JSONObject;
 
 import de.agilecoders.wicket.core.markup.html.bootstrap.navbar.INavbarComponent;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public class RoomMenuPanel extends Panel {
 	private static final long serialVersionUID = 1L;
@@ -142,7 +142,7 @@ public class RoomMenuPanel extends Panel {
 
 	@Override
 	protected void onInitialize() {
-		exitMenuItem = new OmMenuItem(getString("308"), getString("309"), FontAwesome5IconType.sign_out_alt_s) {
+		exitMenuItem = new OmMenuItem(getString("308"), getString("309"), FontAwesome6IconType.right_from_bracket_s) {
 			private static final long serialVersionUID = 1L;
 
 			@Override
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/poll/PollResultsDialog.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/poll/PollResultsDialog.java
index d650d145d..08ada8e24 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/poll/PollResultsDialog.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/poll/PollResultsDialog.java
@@ -64,7 +64,7 @@ import org.wicketstuff.jqplot.lib.elements.RendererOptions;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
 import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public class PollResultsDialog extends Modal<RoomPoll> {
 	private static final long serialVersionUID = 1L;
@@ -118,7 +118,7 @@ public class PollResultsDialog extends Modal<RoomPoll> {
 				close(target);
 			}
 		});
-		close.setIconType(FontAwesome5IconType.times_s).add(newOkCancelDangerConfirm(this, getString("1419")));
+		close.setIconType(FontAwesome6IconType.xmark_s).add(newOkCancelDangerConfirm(this, getString("1419")));
 		close.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true);
 		addButton(delete = new BootstrapAjaxLink<>(BUTTON_MARKUP_ID, null, Buttons.Type.Outline_Danger, new ResourceModel("1420")) {
 			private static final long serialVersionUID = 1L;
@@ -132,7 +132,7 @@ public class PollResultsDialog extends Modal<RoomPoll> {
 				close(target);
 			}
 		});
-		delete.setIconType(FontAwesome5IconType.trash_s).add(newOkCancelDangerConfirm(this, getString("1421")));
+		delete.setIconType(FontAwesome6IconType.trash_s).add(newOkCancelDangerConfirm(this, getString("1421")));
 		delete.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true);
 		addButton(clone = new BootstrapAjaxLink<>(BUTTON_MARKUP_ID, null, Buttons.Type.Outline_Danger, new ResourceModel("poll.clone")) {
 			private static final long serialVersionUID = 1L;
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ChangePasswordDialog.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ChangePasswordDialog.html
index de8363365..8c0cb5712 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ChangePasswordDialog.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ChangePasswordDialog.html
@@ -23,20 +23,24 @@
 <wicket:extend>
 	<form wicket:id="form">
 		<div wicket:id="feedback" class="error"></div>
-		<table>
-			<tr>
-				<td class="desc"><label wicket:for="current" class="form-label"><wicket:message key="current.password" /></label></td>
-				<td><input wicket:id="current" type="password" value="" class="form-control"/></td>
-			</tr>
-			<tr>
-				<td class="desc"><label wicket:for="pass" class="form-label"><wicket:message key="328" /></label></td>
-				<td><input wicket:id="pass" type="password" value="" class="form-control"/></td>
-			</tr>
-			<tr>
-				<td class="desc"><label wicket:for="pass2" class="form-label"><wicket:message key="116" /></label></td>
-				<td><input wicket:id="pass2" type="password" value="" class="form-control"/></td>
-			</tr>
-		</table>
+		<div class="formelement row">
+			<label class="form-label col-4 text-right" wicket:for="current"><wicket:message key="current.password"/></label>
+			<div class="col-7 p-0">
+				<input type="password" wicket:id="current" class="form-control"/>
+			</div>
+		</div>
+		<div class="formelement row">
+			<label class="form-label col-4 text-right" wicket:for="pass"><wicket:message key="328"/></label>
+			<div class="col-7 p-0">
+				<input type="password" wicket:id="pass" class="form-control"/>
+			</div>
+		</div>
+		<div class="formelement row">
+			<label class="form-label col-4 text-right" wicket:for="pass2"><wicket:message key="116"/></label>
+			<div class="col-7 p-0">
+				<input type="password" wicket:id="pass2" class="form-control"/>
+			</div>
+		</div>
 	</form>
 </wicket:extend>
 </html>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ChangePasswordDialog.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ChangePasswordDialog.java
index bb9e82cf3..f2280c5e5 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ChangePasswordDialog.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ChangePasswordDialog.java
@@ -23,6 +23,7 @@ import static org.apache.openmeetings.web.app.WebSession.getUserId;
 import org.apache.openmeetings.core.util.StrongPasswordValidator;
 import org.apache.openmeetings.db.dao.user.UserDao;
 import org.apache.openmeetings.web.common.OmModalCloseButton;
+import org.apache.openmeetings.web.pages.auth.SignInDialog;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
 import org.apache.wicket.markup.html.form.Form;
@@ -31,8 +32,6 @@ import org.apache.wicket.model.Model;
 import org.apache.wicket.model.ResourceModel;
 import org.apache.wicket.spring.injection.annot.SpringBean;
 import org.apache.wicket.util.string.Strings;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
 import de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel;
@@ -41,7 +40,6 @@ import de.agilecoders.wicket.extensions.markup.html.bootstrap.spinner.SpinnerAja
 
 public class ChangePasswordDialog extends Modal<String> {
 	private static final long serialVersionUID = 1L;
-	private static final Logger log = LoggerFactory.getLogger(ChangePasswordDialog.class);
 	private final PasswordTextField current = new PasswordTextField("current", Model.of((String)null));
 	private final PasswordTextField pass = new PasswordTextField("pass", Model.of((String)null));
 	private final PasswordTextField pass2 = new PasswordTextField("pass2", Model.of((String)null));
@@ -53,13 +51,7 @@ public class ChangePasswordDialog extends Modal<String> {
 			String p = current.getConvertedInput();
 			if (!Strings.isEmpty(p) && !userDao.verifyPassword(getUserId(), p)) {
 				error(getString("231"));
-				// add random timeout
-				try {
-					Thread.sleep(6 + (long)(10 * Math.random() * 1000));
-				} catch (InterruptedException e) {
-					log.error("Unexpected exception while sleeping", e);
-					Thread.currentThread().interrupt();
-				}
+				SignInDialog.penalty();
 			}
 			String p1 = pass.getConvertedInput();
 			if (!Strings.isEmpty(p1) && !p1.equals(pass2.getConvertedInput())) {
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfileForm.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfileForm.html
index b06f9a9a1..3254db6c5 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfileForm.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfileForm.html
@@ -30,6 +30,7 @@
 						<wicket:message key="143" />
 					</legend>
 					<button type="button" wicket:id="changePwd" id="changePwd"></button>
+					<button type="button" wicket:id="toggleOtp" id="changeOtp"></button>
 					<div class="formelement row" wicket:enclosure="passwd">
 						<label class="form-label col-3 text-right" wicket:for="passwd"><wicket:message key="current.password" /></label>
 						<div class="col-8 p-0">
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfileForm.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfileForm.java
index 8077360c6..2ac5b1372 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfileForm.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfileForm.java
@@ -19,6 +19,8 @@
 package org.apache.openmeetings.web.user.profile;
 
 import static org.apache.openmeetings.web.app.WebSession.getUserId;
+import static org.apache.openmeetings.web.common.confirmation.ConfirmationHelper.newOkCancelDangerConfirm;
+import static org.apache.openmeetings.util.OpenmeetingsVariables.isOtpEnabled;
 
 import java.time.Duration;
 
@@ -31,8 +33,10 @@ import org.apache.openmeetings.web.common.FormActionsPanel;
 import org.apache.openmeetings.web.common.GeneralUserForm;
 import org.apache.openmeetings.web.common.UploadableProfileImagePanel;
 import org.apache.openmeetings.web.pages.PrivacyPage;
+import org.apache.openmeetings.web.pages.auth.SignInDialog;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.form.AjaxFormValidatingBehavior;
+import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
 import org.apache.wicket.markup.head.IHeaderResponse;
 import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
 import org.apache.wicket.markup.html.form.Form;
@@ -45,29 +49,27 @@ import org.apache.wicket.model.Model;
 import org.apache.wicket.model.ResourceModel;
 import org.apache.wicket.spring.injection.annot.SpringBean;
 import org.apache.wicket.util.string.Strings;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.confirmation.ConfirmationBehavior;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public class EditProfileForm extends Form<User> {
 	private static final long serialVersionUID = 1L;
-	private static final Logger log = LoggerFactory.getLogger(EditProfileForm.class);
 	private final PasswordTextField passwd = new PasswordTextField("passwd", new Model<>());
 	private final GeneralUserForm userForm;
-	private final ChangePasswordDialog chPwdDlg;
 	private boolean checkPassword;
 	private FormActionsPanel<User> actions;
+	private BootstrapAjaxLink<String> toggleOtp;
 
 	@SpringBean
 	private UserDao userDao;
 
-	public EditProfileForm(String id, final ChangePasswordDialog chPwdDlg) {
+	public EditProfileForm(String id) {
 		super(id);
 		setModel(new CompoundPropertyModel<>(userDao.get(getUserId())));
 		userForm = new GeneralUserForm("general", getModel(), false);
-		this.chPwdDlg = chPwdDlg;
 		this.checkPassword = User.Type.OAUTH != getModelObject().getType();
 	}
 
@@ -118,9 +120,30 @@ public class EditProfileForm extends Form<User> {
 
 			@Override
 			public void onClick(AjaxRequestTarget target) {
-				chPwdDlg.show(target);
+				ChangePasswordDialog dlg = (ChangePasswordDialog)findParent(EditProfilePanel.class).get("changePasswdDlg");
+				dlg.show(target);
 			}
 		}.setVisible(checkPassword));
+		toggleOtp = new BootstrapAjaxLink<>("toggleOtp", null, Buttons.Type.Outline_Danger, Model.of("")) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public void onClick(AjaxRequestTarget target) {
+				User u = EditProfileForm.this.getModelObject();
+				if (u.getOtpSecret() == null) {
+					ToggleOtpDialog dlg = (ToggleOtpDialog)findParent(EditProfilePanel.class).get("toggleOtpDlg");
+					dlg.setModel(EditProfileForm.this.getModel());
+					dlg.show(target);
+				} else {
+					u.setOtpSecret(null);
+					u.setOtpRecoveryCodes(null);
+					updateOtpButton(false, target);
+				}
+			}
+		};
+		add(toggleOtp.setOutputMarkupId(true).setVisible(isOtpEnabled() && checkPassword));
+		updateOtpButton(getModelObject().getOtpSecret() != null, null);
+
 		add(userForm);
 		add(new UploadableProfileImagePanel("img", getUserId()));
 		add(new CommunityUserForm("comunity", getModel()));
@@ -143,13 +166,7 @@ public class EditProfileForm extends Form<User> {
 			String p = passwd.getConvertedInput();
 			if (!Strings.isEmpty(p) && !userDao.verifyPassword(getModelObject().getId(), p)) {
 				error(getString("231"));
-				// add random timeout
-				try {
-					Thread.sleep(6 + (long)(10 * Math.random() * 1000));
-				} catch (InterruptedException e) {
-					log.error("Unexpected exception while sleeping", e);
-					Thread.currentThread().interrupt();
-				}
+				SignInDialog.penalty();
 			}
 		}
 		super.onValidate();
@@ -165,4 +182,19 @@ public class EditProfileForm extends Form<User> {
 		super.renderHead(response);
 		response.render(OnDomReadyHeaderItem.forScript("$('.profile-edit-form .my-info').off().click(function() {showUserInfo(" + getUserId() + ");});"));
 	}
+
+	// package private for ToggleOtpDialog
+	void updateOtpButton(boolean enabled, IPartialPageRequestHandler handler) {
+		if (enabled) {
+			toggleOtp.add(newOkCancelDangerConfirm(this, getString("otp.disable.confirm")));
+		} else {
+			toggleOtp.getBehaviors(ConfirmationBehavior.class).stream().forEach(b -> toggleOtp.remove(b));
+		}
+		toggleOtp.setLabel(new ResourceModel(enabled ? "otp.disable" : "otp.enable"));
+		toggleOtp.setIconType(enabled ? FontAwesome6IconType.square_check_r : FontAwesome6IconType.square_r);
+		toggleOtp.setType(enabled ? Buttons.Type.Outline_Danger : Buttons.Type.Primary);
+		if (handler != null) {
+			handler.add(toggleOtp);
+		}
+	}
 }
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfilePanel.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfilePanel.html
index eb27de1cb..ab12ef068 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfilePanel.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfilePanel.html
@@ -22,6 +22,7 @@
 <html xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-9.xsd">
 <wicket:panel>
 	<form wicket:id="form" class="adminForm profile-panel container"/>
-	<div wicket:id="changePasswd"></div>
+	<div wicket:id="changePasswdDlg"></div>
+	<div wicket:id="toggleOtpDlg"></div>
 </wicket:panel>
 </html>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfilePanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfilePanel.java
index c2ece82f2..296ec6c6e 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfilePanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/EditProfilePanel.java
@@ -26,8 +26,13 @@ public class EditProfilePanel extends UserBasePanel {
 	public EditProfilePanel(String id) {
 		super(id);
 		setOutputMarkupId(true);
+	}
 
-		final ChangePasswordDialog chPwdDlg = new ChangePasswordDialog("changePasswd");
-		add(chPwdDlg, new EditProfileForm("form", chPwdDlg));
+	@Override
+	protected void onInitialize() {
+		super.onInitialize();
+		add(new ChangePasswordDialog("changePasswdDlg"));
+		add(new ToggleOtpDialog("toggleOtpDlg"));
+		add(new EditProfileForm("form"));
 	}
 }
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/MessagesContactsPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/MessagesContactsPanel.java
index 9be682bb9..b2014fd64 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/MessagesContactsPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/MessagesContactsPanel.java
@@ -80,7 +80,7 @@ import org.apache.wicket.spring.injection.annot.SpringBean;
 
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public class MessagesContactsPanel extends UserBasePanel {
 	private static final long serialVersionUID = 1L;
@@ -227,7 +227,7 @@ public class MessagesContactsPanel extends UserBasePanel {
 						target.add(folders, moveDropDown);
 					}
 				};
-				del.setIconType(FontAwesome5IconType.times_s)
+				del.setIconType(FontAwesome6IconType.xmark_s)
 						.add(newOkCancelDangerConfirm(this, getString("833")));
 				item.add(del);
 				item.add(AjaxEventBehavior.onEvent(EVT_CLICK, target -> selectFolder(item, item.getModelObject().getId(), target)));
@@ -471,7 +471,7 @@ public class MessagesContactsPanel extends UserBasePanel {
 						updateContacts(target);
 					}
 				};
-				del.setIconType(FontAwesome5IconType.times_s)
+				del.setIconType(FontAwesome6IconType.xmark_s)
 						.add(newOkCancelDangerConfirm(this, getString("833")));
 				item.add(del.setVisible(!uc.isPending()));
 			}
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ChangePasswordDialog.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ToggleOtpDialog.html
similarity index 60%
copy from openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ChangePasswordDialog.html
copy to openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ToggleOtpDialog.html
index de8363365..ed67296a7 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ChangePasswordDialog.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ToggleOtpDialog.html
@@ -22,21 +22,23 @@
 <html xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-9.xsd">
 <wicket:extend>
 	<form wicket:id="form">
+		<div class="formelement row pb-2">
+			<label class="form-label col-4 text-right" wicket:for="current"><wicket:message key="current.password"/></label>
+			<div class="col-7 p-0">
+				<input type="password" wicket:id="current" class="form-control"/>
+			</div>
+		</div>
+		<div class="row">
+			<div class="col-6">
+				<div><wicket:message key="otp.qr.desc"/></div>
+				<img class="col-12" wicket:id="qr"/>
+			</div>
+			<div class="col-6">
+				<div><wicket:message key="otp.fallback.desc"/></div>
+				<textarea class="col-12" wicket:id="codes" readonly="readonly" rows="5"></textarea>
+			</div>
+		</div>
 		<div wicket:id="feedback" class="error"></div>
-		<table>
-			<tr>
-				<td class="desc"><label wicket:for="current" class="form-label"><wicket:message key="current.password" /></label></td>
-				<td><input wicket:id="current" type="password" value="" class="form-control"/></td>
-			</tr>
-			<tr>
-				<td class="desc"><label wicket:for="pass" class="form-label"><wicket:message key="328" /></label></td>
-				<td><input wicket:id="pass" type="password" value="" class="form-control"/></td>
-			</tr>
-			<tr>
-				<td class="desc"><label wicket:for="pass2" class="form-label"><wicket:message key="116" /></label></td>
-				<td><input wicket:id="pass2" type="password" value="" class="form-control"/></td>
-			</tr>
-		</table>
 	</form>
 </wicket:extend>
 </html>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ToggleOtpDialog.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ToggleOtpDialog.java
new file mode 100644
index 000000000..72df0279a
--- /dev/null
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/profile/ToggleOtpDialog.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") +  you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openmeetings.web.user.profile;
+
+import static org.apache.openmeetings.util.OpenmeetingsVariables.PARAM_SRC;
+
+import org.apache.openmeetings.db.dao.user.UserDao;
+import org.apache.openmeetings.db.entity.user.User;
+import org.apache.openmeetings.web.app.OtpManager;
+import org.apache.openmeetings.web.common.OmModalCloseButton;
+import org.apache.openmeetings.web.pages.auth.SignInDialog;
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.PasswordTextField;
+import org.apache.wicket.markup.html.form.TextArea;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.ResourceModel;
+import org.apache.wicket.spring.injection.annot.SpringBean;
+import org.apache.wicket.util.string.Strings;
+
+import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
+import de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel;
+import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.spinner.SpinnerAjaxButton;
+
+public class ToggleOtpDialog extends Modal<User> {
+	private final NotificationPanel feedback = new NotificationPanel("feedback");
+	private final PasswordTextField current = new PasswordTextField("current", Model.of((String)null));
+	private final WebMarkupContainer qr = new WebMarkupContainer("qr");
+	private final TextArea<String> codesArea = new TextArea<>("codes", Model.of(""));
+	private String secret;
+	private String[] codes;
+
+	@SpringBean
+	private OtpManager otpManager;
+	@SpringBean
+	private UserDao userDao;
+
+	public ToggleOtpDialog(String id) {
+		super(id);
+	}
+
+	@Override
+	protected void onInitialize() {
+		header(new ResourceModel("otp.enable"));
+		setUseCloseHandler(true);
+
+		final Form<String> form = new Form<>("form") {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected void onValidate() {
+				final User u = ToggleOtpDialog.this.getModelObject();
+				String p = current.getConvertedInput();
+				if (!Strings.isEmpty(p) && !userDao.verifyPassword(u.getId(), p)) {
+					error(getString("231"));
+					SignInDialog.penalty();
+				}
+				super.onValidate();
+			}
+			};
+		add(form.add(current.setLabel(new ResourceModel("current.password")).setOutputMarkupId(true))
+				.add(qr.setOutputMarkupId(true))
+				.add(codesArea.setOutputMarkupId(true))
+				.add(feedback.setOutputMarkupId(true)));
+
+		addButton(new SpinnerAjaxButton(BUTTON_MARKUP_ID, new ResourceModel("otp.enable"), form, Buttons.Type.Outline_Primary) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected void onError(AjaxRequestTarget target) {
+				target.add(feedback);
+			}
+
+			@Override
+			protected void onSubmit(AjaxRequestTarget target) {
+				final User u = ToggleOtpDialog.this.getModelObject();
+				u.setOtpSecret(secret);
+				u.setOtpRecoveryCodes(String.join(" ", codes));
+				EditProfileForm editForm = (EditProfileForm)findParent(EditProfilePanel.class).get("form");
+				editForm.updateOtpButton(true, target);
+				userDao.update(u, u.getId());
+				ToggleOtpDialog.this.close(target);
+			}
+		});
+		addButton(OmModalCloseButton.of());
+		super.onInitialize();
+	}
+
+	@Override
+	public void onClose(IPartialPageRequestHandler handler) {
+		secret = null;
+		current.setModelObject(null);
+		qr.add(AttributeModifier.remove(PARAM_SRC));
+		codesArea.setModelObject("");
+		handler.add(current, qr, codesArea);
+	}
+
+	@Override
+	public Modal<User> show(IPartialPageRequestHandler target) {
+		secret = otpManager.generateSecret();
+		codes = otpManager.getRecoveryCodes();
+		final User u = getModelObject();
+		current.setModelObject(null);
+		qr.add(AttributeModifier.replace(PARAM_SRC, otpManager.getQr(u.getAddress().getEmail(), secret)));
+		codesArea.setModelObject(String.join("\n", codes));
+		target.add(current, qr, codesArea);
+		return super.show(target);
+	}
+}
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomListPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomListPanel.java
index 3576fc8e9..0402ecdee 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomListPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomListPanel.java
@@ -46,7 +46,7 @@ import org.apache.wicket.spring.injection.annot.SpringBean;
 import de.agilecoders.wicket.core.markup.html.bootstrap.behavior.CssClassNameAppender;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxLink;
 import de.agilecoders.wicket.core.markup.html.bootstrap.button.Buttons;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType;
 
 public class RoomListPanel extends Panel {
 	private static final long serialVersionUID = 1L;
@@ -85,7 +85,7 @@ public class RoomListPanel extends Panel {
 				private static final long serialVersionUID = 1L;
 
 				{
-					setIconType(FontAwesome5IconType.sync_alt_s);
+					setIconType(FontAwesome6IconType.rotate_s);
 				}
 
 				@Override
diff --git a/openmeetings-web/src/main/webapp/WEB-INF/classes/openmeetings.properties b/openmeetings-web/src/main/webapp/WEB-INF/classes/openmeetings.properties
index 54796a37b..681f3baa7 100644
--- a/openmeetings-web/src/main/webapp/WEB-INF/classes/openmeetings.properties
+++ b/openmeetings-web/src/main/webapp/WEB-INF/classes/openmeetings.properties
@@ -78,3 +78,13 @@ sip.ws.local.host=
 sip.ws.remote.port=8088
 sip.ws.remote.user=omsip_user
 sip.ws.remote.password=12345
+
+################## Time-based One Time Password ##################
+## Please NOTE these values need to be changed BEFORE users will set-up OTP for themselves
+## otherwise they can't login
+
+# NOTE Config->application.name will be used if blank
+otp.issuer=
+otp.ntp.server=pool.ntp.org
+## milliseconds
+otp.ntp.timeout=3000
diff --git a/openmeetings-web/src/main/webapp/css/raw-admin.css b/openmeetings-web/src/main/webapp/css/raw-admin.css
index 2691a269b..745c12a6b 100644
--- a/openmeetings-web/src/main/webapp/css/raw-admin.css
+++ b/openmeetings-web/src/main/webapp/css/raw-admin.css
@@ -82,7 +82,7 @@
 	margin: 0;
 }
 .onoff-label::before {
-	font-family: 'Font Awesome 5 Free';
+	font-family: 'Font Awesome 6 Free';
 	font-weight: 900;
 	font-size: 2.2em;
 }
diff --git a/openmeetings-web/src/main/webapp/css/raw-calendar.css b/openmeetings-web/src/main/webapp/css/raw-calendar.css
index d4c729397..a57978afe 100644
--- a/openmeetings-web/src/main/webapp/css/raw-calendar.css
+++ b/openmeetings-web/src/main/webapp/css/raw-calendar.css
@@ -1,6 +1,6 @@
 /* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */
 #contents #calendar .fc-gotoBtn-button::before {
-	font-family: 'Font Awesome 5 Free';
+	font-family: 'Font Awesome 6 Free';
 	font-weight: 900;
 	content: "\f133";
 	font-size: 1em;
@@ -31,7 +31,7 @@
 #wrapper-panel-frame .main-form, #calendar {
 	height: 100%;
 }
-/* FIXME TODO bootstrap override */
+/* bootstrap override */
 .table-bordered {
 	border: 1px solid #dee2e6;
 }
diff --git a/openmeetings-web/src/main/webapp/css/raw-general.css b/openmeetings-web/src/main/webapp/css/raw-general.css
index 652dda0a3..e4d2da485 100644
--- a/openmeetings-web/src/main/webapp/css/raw-general.css
+++ b/openmeetings-web/src/main/webapp/css/raw-general.css
@@ -218,7 +218,7 @@ html, body {
 	height: 34px;
 }
 .om-icon::before {
-	font-family: 'Font Awesome 5 Free';
+	font-family: 'Font Awesome 6 Free';
 	font-weight: 900;
 	color: var(--bs-secondary);
 	font-size: 1.2em;
@@ -576,7 +576,7 @@ select.messages.selector {
 	height: 100%;
 }
 .dragbox .dragbox-header .dragbox-toggle, .dragbox .dragbox-header .dragbox-actions .icon, .sort-icon a {
-	font-family: "Font Awesome 5 Free";
+	font-family: "Font Awesome 6 Free";
 	font-weight: 900;
 	display: inline-block;
 	font-style: normal;
@@ -743,7 +743,7 @@ select.messages.selector {
 	z-index: calc(var(--chat-zindex) + 2);
 }
 .popover.confirmation.show {
-	z-index: 3000; /* FIXME TODO move this to variables */
+	z-index: 3000;
 }
 .installer {
 	overflow-y: auto;
diff --git a/openmeetings-web/src/main/webapp/css/raw-wb.css b/openmeetings-web/src/main/webapp/css/raw-wb.css
index e234e7a73..f62803098 100644
--- a/openmeetings-web/src/main/webapp/css/raw-wb.css
+++ b/openmeetings-web/src/main/webapp/css/raw-wb.css
@@ -30,7 +30,7 @@ html[dir="rtl"] .room-block .sb-wb .wb-block {
 	background-position: center;
 }
 .room-block .sb-wb .wb-block.droppable-hover .wb-drop-area::before {
-	font-family: 'Font Awesome 5 Free';
+	font-family: 'Font Awesome 6 Free';
 	font-weight: 400;
 	font-size: 20em;
 	content: '\f358';
diff --git a/openmeetings-web/src/test/java/org/apache/openmeetings/webservice/AbstractWebServiceTest.java b/openmeetings-web/src/test/java/org/apache/openmeetings/webservice/AbstractWebServiceTest.java
index 8082d9694..a9c15588d 100644
--- a/openmeetings-web/src/test/java/org/apache/openmeetings/webservice/AbstractWebServiceTest.java
+++ b/openmeetings-web/src/test/java/org/apache/openmeetings/webservice/AbstractWebServiceTest.java
@@ -136,7 +136,6 @@ public abstract class AbstractWebServiceTest {
 		assertEquals(r.getName(), room1.getName(), "Room with same Name should be returned");
 		assertEquals(r.getExternalType(), room1.getExternalType(), "Room with same ExternalType should be returned");
 		assertEquals(r.getExternalId(), room1.getExternalId(), "Room with same ExternalId should be returned");
-		//TODO check other fields
 		return new CallResult<>(sid, room1);
 	}
 
diff --git a/openmeetings-web/src/test/java/org/apache/openmeetings/webservice/TestRecordingService.java b/openmeetings-web/src/test/java/org/apache/openmeetings/webservice/TestRecordingService.java
index e23738012..e3f7ffe21 100644
--- a/openmeetings-web/src/test/java/org/apache/openmeetings/webservice/TestRecordingService.java
+++ b/openmeetings-web/src/test/java/org/apache/openmeetings/webservice/TestRecordingService.java
@@ -62,7 +62,6 @@ class TestRecordingService extends AbstractWebServiceTest {
 		boolean found = false;
 		for (RecordingDTO rdo : recs) {
 			if (r.getId().equals(rdo.getId())) {
-				//TODO check room, user
 				found = true;
 				break;
 			}
diff --git a/pom.xml b/pom.xml
index 4b7587740..3ac6bfda6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -96,26 +96,27 @@
 		<site.basedir>${project.basedir}</site.basedir>
 		<src.pack.skip>false</src.pack.skip>
 		<h2.version>2.1.214</h2.version>
-		<commons-lang3.version>3.12.0</commons-lang3.version>
 		<jakarta.mail.version>2.0.1</jakarta.mail.version>
 		<openjpa.version>3.2.2</openjpa.version>
 		<asterisk-java.version>3.37.0</asterisk-java.version>
+		<commons-lang3.version>3.12.0</commons-lang3.version>
 		<commons-dbcp.version>2.9.0</commons-dbcp.version>
 		<commons-pool2.version>2.11.1</commons-pool2.version>
 		<commons-cli.version>1.5.0</commons-cli.version>
-		<dom4j.version>2.1.3</dom4j.version>
 		<commons-codec.version>1.15</commons-codec.version>
 		<commons-io.version>2.11.0</commons-io.version>
+		<commons-collections4.version>4.4</commons-collections4.version>
+		<commons-text.version>1.10.0</commons-text.version>
+		<commons-net.version>3.9.0</commons-net.version>
+		<dom4j.version>2.1.3</dom4j.version>
 		<postgresql.version>42.5.1</postgresql.version>
 		<mysql.version>8.0.31</mysql.version>
 		<mssql.version>11.2.1.jre17</mssql.version>
 		<ojdbc.version>19.17.0.0</ojdbc.version>
-		<commons-collections4.version>4.4</commons-collections4.version>
 		<xstream.version>1.4.19</xstream.version>
 		<api-all.version>2.1.2</api-all.version>
 		<caldav4j.version>1.0.5</caldav4j.version>
 		<tika-parsers.version>2.6.0</tika-parsers.version>
-		<commons-text.version>1.10.0</commons-text.version>
 		<slf4j.version>2.0.6</slf4j.version>
 		<logback.version>1.4.5</logback.version>
 		<jetty.version>9.4.50.v20221201</jetty.version>
@@ -135,6 +136,7 @@
 		<bytebuddy.version>1.12.20</bytebuddy.version>
 		<annotation-api.version>1.3.2</annotation-api.version>
 		<jsr305.version>3.0.2</jsr305.version>
+		<totp.version>1.7.1</totp.version>
 		<!--  Exclude all generated code  -->
 		<sonar.exclusions>file:**/generated-sources/**, file:**/jquery-ui.css, file:**/cssemoticons.js, file:**/bootstrap-confirmation.js</sonar.exclusions>
 		<sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin>
@@ -1022,6 +1024,18 @@
 				<version>${tomcat.version}</version>
 				<scope>test</scope>
 			</dependency>
+			<!-- OTP block -->
+			<dependency>
+				<groupId>dev.samstevens.totp</groupId>
+				<artifactId>totp</artifactId>
+				<version>${totp.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>commons-net</groupId>
+				<artifactId>commons-net</artifactId>
+				<version>${commons-net.version}</version>
+			</dependency>
+			<!-- OTP block -->
 		</dependencies>
 	</dependencyManagement>
 	<dependencies>
@@ -1215,7 +1229,7 @@
 									<property name="fileExtensions" value="java"/>
 								</module>
 								<module name="SuppressWithPlainTextCommentFilter">
-									<property name="offCommentFormat" value="[=+](\s+)&amp;quot;&amp;quot;&amp;quot;"/>
+									<property name="offCommentFormat" value="(\s+)&amp;quot;&amp;quot;&amp;quot;"/>
 									<property name="onCommentFormat" value="^\s+.*&amp;quot;&amp;quot;&amp;quot;;"/>
 								</module>
 								<module name="TreeWalker">
@@ -1244,7 +1258,7 @@
 						<dependency>
 							<groupId>com.puppycrawl.tools</groupId>
 							<artifactId>checkstyle</artifactId>
-							<version>10.3.2</version> <!-- FIXME TODO: should be removed after checkstyle plugin will be updated -->
+							<version>10.5.0</version> <!-- FIXME TODO: should be removed after checkstyle plugin will be updated -->
 						</dependency>
 					</dependencies>
 				</plugin>
diff --git a/src/license/license-template.ftl b/src/license/license-template.ftl
index 4e3e209be..2d29ab44c 100644
--- a/src/license/license-template.ftl
+++ b/src/license/license-template.ftl
@@ -49,7 +49,7 @@
 <#function licensesKey licenses>
   <#local result = "">
   <#list licenses?sort as license>
-      <#if license?contains("Apache License, Version 2.0")><#return "Apache License Version 2.0"></#if><#-- FIXME TODO overriding mapping-->
+      <#if license?contains("Apache License, Version 2.0")><#return "Apache License Version 2.0"></#if>
       <#if license?lower_case?contains("apache")><#return license></#if>
       <#if license?contains("Eclipse Public License")><#return license></#if>
       <#if license?contains("Common Development and Distribution License")><#return license></#if>