keycloak-memoizeit

Changes

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index 4a7396c..cd52d7c 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -99,6 +99,7 @@ public class RealmRepresentation {
     protected Integer otpPolicyDigits;
     protected Integer otpPolicyLookAheadWindow;
     protected Integer otpPolicyPeriod;
+    protected List<String> otpSupportedApplications;
 
     protected List<UserRepresentation> users;
     protected List<UserRepresentation> federatedUsers;
@@ -854,6 +855,14 @@ public class RealmRepresentation {
         this.otpPolicyPeriod = otpPolicyPeriod;
     }
 
+    public List<String> getOtpSupportedApplications() {
+        return otpSupportedApplications;
+    }
+
+    public void setOtpSupportedApplications(List<String> otpSupportedApplications) {
+        this.otpSupportedApplications = otpSupportedApplications;
+    }
+
     public String getBrowserFlow() {
         return browserFlow;
     }
diff --git a/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java b/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java
index 0acf02d..83a27fc 100755
--- a/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java
+++ b/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java
@@ -25,6 +25,8 @@ import java.io.Serializable;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -44,6 +46,8 @@ public class OTPPolicy implements Serializable {
 
     private static final Map<String, String> algToKeyUriAlg = new HashMap<>();
 
+    private static final OtpApp[] allApplications = new OtpApp[] { new FreeOTP(), new GoogleAuthenticator() };
+
     static {
         algToKeyUriAlg.put(HmacOTP.HMAC_SHA1, "SHA1");
         algToKeyUriAlg.put(HmacOTP.HMAC_SHA256, "SHA256");
@@ -151,4 +155,60 @@ public class OTPPolicy implements Serializable {
             throw new RuntimeException(e);
         }
     }
+
+    public List<String> getSupportedApplications() {
+        List<String> applications = new LinkedList<>();
+        for (OtpApp a : allApplications) {
+            if (a.supports(this)) {
+                applications.add(a.getName());
+            }
+        }
+        return applications;
+    }
+
+    public interface OtpApp {
+
+        String getName();
+
+        boolean supports(OTPPolicy policy);
+    }
+
+    public static class GoogleAuthenticator implements OtpApp {
+
+        @Override
+        public String getName() {
+            return "Google Authenticator";
+        }
+
+        @Override
+        public boolean supports(OTPPolicy policy) {
+            if (policy.digits != 6) {
+                return false;
+            }
+
+            if (!policy.getAlgorithm().equals("HmacSHA1")) {
+                return false;
+            }
+
+            if (policy.getType().equals("totp") && policy.getPeriod() != 30) {
+                return false;
+            }
+
+            return true;
+        }
+    }
+
+    public static class FreeOTP implements OtpApp {
+
+        @Override
+        public String getName() {
+            return "FreeOTP";
+        }
+
+        @Override
+        public boolean supports(OTPPolicy policy) {
+            return true;
+        }
+    }
+
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java
index debc52f..a61c0a9 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java
@@ -66,4 +66,6 @@ public interface AccountProvider extends Provider {
     AccountProvider setStateChecker(String stateChecker);
 
     AccountProvider setFeatures(boolean social, boolean events, boolean passwordUpdateSupported);
+
+    AccountProvider setAttribute(String key, String value);
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 492773c..aa6b42c 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -282,6 +282,7 @@ public class ModelToRepresentation {
         rep.setOtpPolicyInitialCounter(otpPolicy.getInitialCounter());
         rep.setOtpPolicyType(otpPolicy.getType());
         rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
+        rep.setOtpSupportedApplications(otpPolicy.getSupportedApplications());
         if (realm.getBrowserFlow() != null) rep.setBrowserFlow(realm.getBrowserFlow().getAlias());
         if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
         if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
index de7a078..e85ec7e 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
@@ -46,10 +46,15 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
     @Override
     public void requiredActionChallenge(RequiredActionContext context) {
         Response challenge = context.form()
+                .setAttribute("mode", getMode(context))
                 .createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
         context.challenge(challenge);
     }
 
+    private String getMode(RequiredActionContext context) {
+        return context.getUriInfo().getQueryParameters().getFirst("mode");
+    }
+
     @Override
     public void processAction(RequiredActionContext context) {
         EventBuilder event = context.getEvent();
@@ -60,12 +65,14 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
 
         if (Validation.isBlank(totp)) {
             Response challenge = context.form()
+                    .setAttribute("mode", getMode(context))
                     .setError(Messages.MISSING_TOTP)
                     .createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
             context.challenge(challenge);
             return;
         } else if (!CredentialValidation.validOTP(context.getRealm(), totp, totpSecret)) {
             Response challenge = context.form()
+                    .setAttribute("mode", getMode(context))
                     .setError(Messages.INVALID_TOTP)
                     .createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
             context.challenge(challenge);
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java b/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java
index 5b236fc..8957f70 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java
@@ -86,6 +86,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
     protected KeycloakSession session;
     protected FreeMarkerUtil freeMarker;
     protected HttpHeaders headers;
+    protected Map<String, Object> attributes;
 
     protected UriInfo uriInfo;
 
@@ -110,7 +111,11 @@ public class FreeMarkerAccountProvider implements AccountProvider {
 
     @Override
     public Response createResponse(AccountPages page) {
-        Map<String, Object> attributes = new HashMap<String, Object>();
+        Map<String, Object> attributes = new HashMap<>();
+
+        if (this.attributes != null) {
+            attributes.putAll(this.attributes);
+        }
 
         Theme theme;
         try {
@@ -156,7 +161,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
 
         switch (page) {
             case TOTP:
-                attributes.put("totp", new TotpBean(session, realm, user));
+                attributes.put("totp", new TotpBean(session, realm, user, uriInfo.getRequestUriBuilder()));
                 break;
             case FEDERATED_IDENTITY:
                 attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker));
@@ -362,6 +367,15 @@ public class FreeMarkerAccountProvider implements AccountProvider {
     }
 
     @Override
+    public AccountProvider setAttribute(String key, String value) {
+        if (attributes == null) {
+            attributes = new HashMap<>();
+        }
+        attributes.put(key, value);
+        return this;
+    }
+
+    @Override
     public void close() {
     }
 
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java
index 21afcff..9af43db 100644
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java
@@ -18,25 +18,32 @@
 package org.keycloak.forms.account.freemarker.model;
 
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.OTPPolicy;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.HmacOTP;
 import org.keycloak.utils.TotpUtils;
 
+import javax.ws.rs.core.UriBuilder;
+
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public class TotpBean {
 
+    private final RealmModel realm;
     private final String totpSecret;
     private final String totpSecretEncoded;
     private final String totpSecretQrCode;
     private final boolean enabled;
+    private final UriBuilder uriBuilder;
 
-    public TotpBean(KeycloakSession session, RealmModel realm, UserModel user) {
+    public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) {
+        this.uriBuilder = uriBuilder;
         this.enabled = session.userCredentialManager().isConfiguredFor(realm, user, realm.getOTPPolicy().getType());
 
+        this.realm = realm;
         this.totpSecret = HmacOTP.generateSecret(20);
         this.totpSecretEncoded = TotpUtils.encode(totpSecret);
         this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
@@ -58,5 +65,17 @@ public class TotpBean {
         return totpSecretQrCode;
     }
 
+    public String getManualUrl() {
+        return uriBuilder.replaceQueryParam("mode", "manual").build().toString();
+    }
+
+    public String getQrUrl() {
+        return uriBuilder.replaceQueryParam("mode", "qr").build().toString();
+    }
+
+    public OTPPolicy getPolicy() {
+        return realm.getOTPPolicy();
+    }
+
 }
 
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index 05da6b2..2adbb05 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -180,7 +180,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
 
         switch (page) {
             case LOGIN_CONFIG_TOTP:
-                attributes.put("totp", new TotpBean(session, realm, user));
+                attributes.put("totp", new TotpBean(session, realm, user, uriInfo.getRequestUriBuilder()));
                 break;
             case LOGIN_UPDATE_PROFILE:
                 UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java
index 6626018..7659459 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java
@@ -18,22 +18,29 @@ package org.keycloak.forms.login.freemarker.model;
 
 import org.keycloak.credential.CredentialModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.OTPPolicy;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.HmacOTP;
 import org.keycloak.utils.TotpUtils;
 
+import javax.ws.rs.core.UriBuilder;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public class TotpBean {
 
+    private final RealmModel realm;
     private final String totpSecret;
     private final String totpSecretEncoded;
     private final String totpSecretQrCode;
     private final boolean enabled;
+    private UriBuilder uriBuilder;
 
-    public TotpBean(KeycloakSession session, RealmModel realm, UserModel user) {
+    public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) {
+        this.realm = realm;
+        this.uriBuilder = uriBuilder;
         this.enabled = session.userCredentialManager().isConfiguredFor(realm, user, CredentialModel.OTP);
         this.totpSecret = HmacOTP.generateSecret(20);
         this.totpSecretEncoded = TotpUtils.encode(totpSecret);
@@ -56,5 +63,18 @@ public class TotpBean {
         return totpSecretQrCode;
     }
 
+    public String getManualUrl() {
+        return uriBuilder.replaceQueryParam("mode", "manual").build().toString();
+    }
+
+    public String getQrUrl() {
+        return uriBuilder.replaceQueryParam("mode", "qr").build().toString();
+    }
+
+    public OTPPolicy getPolicy() {
+        return realm.getOTPPolicy();
+    }
+
+
 }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
index f9d48bc..68973cf 100755
--- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
@@ -17,6 +17,7 @@
 package org.keycloak.services.resources.account;
 
 import org.jboss.logging.Logger;
+import org.keycloak.authentication.RequiredActionContext;
 import org.keycloak.common.util.Base64Url;
 import org.keycloak.common.util.Time;
 import org.keycloak.common.util.UriUtils;
@@ -230,6 +231,7 @@ public class AccountFormService extends AbstractSecuredLocalService {
     @Path("totp")
     @GET
     public Response totpPage() {
+        account.setAttribute("mode", uriInfo.getQueryParameters().getFirst("mode"));
         return forwardToPage("totp", AccountPages.TOTP);
     }
 
@@ -442,6 +444,8 @@ public class AccountFormService extends AbstractSecuredLocalService {
 
         auth.require(AccountRoles.MANAGE_ACCOUNT);
 
+        account.setAttribute("mode", uriInfo.getQueryParameters().getFirst("mode"));
+
         String action = formData.getFirst("submitAction");
         if (action != null && action.equals("Cancel")) {
             setReferrerOnPage();
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java
index 6527476..6e3dd0f 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java
@@ -39,6 +39,12 @@ public class AccountTotpPage extends AbstractAccountPage {
     @FindBy(id = "remove-mobile")
     private WebElement removeLink;
 
+    @FindBy(id = "mode-barcode")
+    private WebElement barcodeLink;
+
+    @FindBy(id = "mode-manual")
+    private WebElement manualLink;
+
     private String getPath() {
         return AccountFormService.totpUrl(UriBuilder.fromUri(getAuthServerRoot())).build("test").toString();
     }
@@ -64,4 +70,12 @@ public class AccountTotpPage extends AbstractAccountPage {
         removeLink.click();
     }
 
+    public void clickManual() {
+        manualLink.click();
+    }
+
+    public void clickBarcode() {
+        barcodeLink.click();
+    }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java
index da289f8..f5183ac 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java
@@ -33,6 +33,12 @@ public class LoginConfigTotpPage extends AbstractPage {
     @FindBy(css = "input[type=\"submit\"]")
     private WebElement submitButton;
 
+    @FindBy(id = "mode-barcode")
+    private WebElement barcodeLink;
+
+    @FindBy(id = "mode-manual")
+    private WebElement manualLink;
+
     public void configure(String totp) {
         totpInput.sendKeys(totp);
         submitButton.click();
@@ -50,4 +56,12 @@ public class LoginConfigTotpPage extends AbstractPage {
         throw new UnsupportedOperationException();
     }
 
+    public void clickManual() {
+        manualLink.click();
+    }
+
+    public void clickBarcode() {
+        barcodeLink.click();
+    }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java
index abe91d4..462e10b 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java
@@ -68,7 +68,9 @@ import java.util.Map;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.hasItems;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -770,6 +772,39 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
 
         assertFalse(driver.getPageSource().contains("Remove Google"));
 
+        String pageSource = driver.getPageSource();
+
+        assertTrue(pageSource.contains("Install one of the following applications on your mobile"));
+        assertTrue(pageSource.contains("FreeOTP"));
+        assertTrue(pageSource.contains("Google Authenticator"));
+
+        assertTrue(pageSource.contains("Open the application and scan the barcode"));
+        assertFalse(pageSource.contains("Open the application and enter the key"));
+
+        assertTrue(pageSource.contains("Unable to scan?"));
+        assertFalse(pageSource.contains("Scan barcode?"));
+
+        totpPage.clickManual();
+
+        pageSource = driver.getPageSource();
+
+        assertTrue(pageSource.contains("Install one of the following applications on your mobile"));
+        assertTrue(pageSource.contains("FreeOTP"));
+        assertTrue(pageSource.contains("Google Authenticator"));
+
+        assertFalse(pageSource.contains("Open the application and scan the barcode"));
+        assertTrue(pageSource.contains("Open the application and enter the key"));
+
+        assertFalse(pageSource.contains("Unable to scan?"));
+        assertTrue(pageSource.contains("Scan barcode?"));
+
+        assertTrue(driver.findElement(By.id("kc-totp-secret-key")).getText().matches("[\\w]{4}( [\\w]{4}){7}"));
+
+        assertEquals("Type: Time-based", driver.findElement(By.id("kc-totp-type")).getText());
+        assertEquals("Algorithm: HmacSHA1", driver.findElement(By.id("kc-totp-algorithm")).getText());
+        assertEquals("Digits: 6", driver.findElement(By.id("kc-totp-digits")).getText());
+        assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
+
         // Error with false code
         totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret() + "123"));
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
index a06079c..f42a146 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
@@ -22,6 +22,7 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.events.Details;
 import org.keycloak.events.EventType;
 import org.keycloak.models.AuthenticationExecutionModel;
@@ -46,11 +47,17 @@ import org.keycloak.testsuite.pages.LoginTotpPage;
 import org.keycloak.testsuite.pages.RegisterPage;
 import org.keycloak.testsuite.util.RealmBuilder;
 import org.keycloak.testsuite.util.UserBuilder;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
 
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -123,19 +130,105 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
 
         String userId = events.expectRegister("setupTotp", "email@mail.com").assertEvent().getUserId();
 
-        Assert.assertTrue(totpPage.isCurrent());
+        assertTrue(totpPage.isCurrent());
 
         totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));
 
         String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent()
                 .getDetails().get(Details.CODE_ID);
 
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
         events.expectLogin().user(userId).session(authSessionId).detail(Details.USERNAME, "setuptotp").assertEvent();
     }
 
     @Test
+    public void setupTotpRegisterManual() {
+        loginPage.open();
+        loginPage.clickRegister();
+        registerPage.register("firstName", "lastName", "checkQrCode@mail.com", "checkQrCode", "password", "password");
+
+        String pageSource = driver.getPageSource();
+
+        assertTrue(pageSource.contains("Install one of the following applications on your mobile"));
+        assertTrue(pageSource.contains("FreeOTP"));
+        assertTrue(pageSource.contains("Google Authenticator"));
+
+        assertTrue(pageSource.contains("Open the application and scan the barcode"));
+        assertFalse(pageSource.contains("Open the application and enter the key"));
+
+        assertTrue(pageSource.contains("Unable to scan?"));
+        assertFalse(pageSource.contains("Scan barcode?"));
+
+        totpPage.clickManual();
+
+        pageSource = driver.getPageSource();
+
+        assertTrue(pageSource.contains("Install one of the following applications on your mobile"));
+        assertTrue(pageSource.contains("FreeOTP"));
+        assertTrue(pageSource.contains("Google Authenticator"));
+
+        assertFalse(pageSource.contains("Open the application and scan the barcode"));
+        assertTrue(pageSource.contains("Open the application and enter the key"));
+
+        assertFalse(pageSource.contains("Unable to scan?"));
+        assertTrue(pageSource.contains("Scan barcode?"));
+
+        assertTrue(driver.findElement(By.id("kc-totp-secret-key")).getText().matches("[\\w]{4}( [\\w]{4}){7}"));
+
+        assertEquals("Type: Time-based", driver.findElement(By.id("kc-totp-type")).getText());
+        assertEquals("Algorithm: HmacSHA1", driver.findElement(By.id("kc-totp-algorithm")).getText());
+        assertEquals("Digits: 6", driver.findElement(By.id("kc-totp-digits")).getText());
+        assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
+
+        totpPage.clickBarcode();
+
+        pageSource = driver.getPageSource();
+
+        assertTrue(pageSource.contains("Install one of the following applications on your mobile"));
+        assertTrue(pageSource.contains("FreeOTP"));
+        assertTrue(pageSource.contains("Google Authenticator"));
+
+        assertTrue(pageSource.contains("Open the application and scan the barcode"));
+        assertFalse(pageSource.contains("Open the application and enter the key"));
+
+        assertTrue(pageSource.contains("Unable to scan?"));
+        assertFalse(pageSource.contains("Scan barcode?"));
+    }
+
+    @Test
+    public void setupTotpModifiedPolicy() {
+        RealmResource realm = testRealm();
+        RealmRepresentation rep = realm.toRepresentation();
+        rep.setOtpPolicyDigits(8);
+        rep.setOtpPolicyType("hotp");
+        rep.setOtpPolicyAlgorithm("HmacSHA256");
+        realm.update(rep);
+        try {
+            loginPage.open();
+            loginPage.clickRegister();
+            registerPage.register("firstName", "lastName", "setupTotpModifiedPolicy@mail.com", "setupTotpModifiedPolicy", "password", "password");
+
+            String pageSource = driver.getPageSource();
+
+            assertTrue(pageSource.contains("FreeOTP"));
+            assertFalse(pageSource.contains("Google Authenticator"));
+
+            totpPage.clickManual();
+
+            assertEquals("Type: Counter-based", driver.findElement(By.id("kc-totp-type")).getText());
+            assertEquals("Algorithm: HmacSHA256", driver.findElement(By.id("kc-totp-algorithm")).getText());
+            assertEquals("Digits: 8", driver.findElement(By.id("kc-totp-digits")).getText());
+            assertEquals("Interval: 30", driver.findElement(By.id("kc-totp-period")).getText());
+        } finally {
+            rep.setOtpPolicyDigits(6);
+            rep.setOtpPolicyType("totp");
+            rep.setOtpPolicyAlgorithm("HmacSHA1");
+            realm.update(rep);
+        }
+    }
+
+    @Test
     public void setupTotpExisting() {
         loginPage.open();
         loginPage.login("test-user@localhost", "password");
@@ -149,7 +242,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
         String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
                 .getDetails().get(Details.CODE_ID);
 
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
         EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent();
 
@@ -162,7 +255,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
         String src = driver.getPageSource();
         loginTotpPage.login(totp.generateTOTP(totpSecret));
 
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
         events.expectLogin().assertEvent();
     }
@@ -185,7 +278,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
         totpPage.configure(totp.generateTOTP(totpCode));
 
         // After totp config, user should be on the app page
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
         events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent();
 
@@ -202,7 +295,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
         // Totp is already configured, thus one-time password is needed, login page should be loaded
         String uri = driver.getCurrentUrl();
         String src = driver.getPageSource();
-        Assert.assertTrue(loginPage.isCurrent());
+        assertTrue(loginPage.isCurrent());
         Assert.assertFalse(totpPage.isCurrent());
 
         // Login with one-time password
@@ -234,7 +327,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
         String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent()
                 .getDetails().get(Details.CODE_ID);
 
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
         events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setupTotp2").assertEvent();
     }
@@ -266,7 +359,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
         String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
                 .getDetails().get(Details.CODE_ID);
 
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
         EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
 
@@ -278,10 +371,10 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
         loginPage.login("test-user@localhost", "password");
         String src = driver.getPageSource();
         String token = timeBased.generateTOTP(totpSecret);
-        Assert.assertEquals(8, token.length());
+        assertEquals(8, token.length());
         loginTotpPage.login(token);
 
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
         events.expectLogin().assertEvent();
 
@@ -318,7 +411,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
         String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
             .getDetails().get(Details.CODE_ID);
 
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
         EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
 
@@ -331,7 +424,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
         String token = otpgen.generateHOTP(totpSecret, 1);
         loginTotpPage.login(token);
 
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
         events.expectLogin().assertEvent();
 
@@ -356,7 +449,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
         loginTotpPage.assertCurrent();
         loginTotpPage.login(token);
 
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
         events.expectLogin().assertEvent();
 
diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
index e98874b..b943edc 100755
--- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -98,10 +98,23 @@ revoke=Revoke Grant
 
 configureAuthenticators=Configured Authenticators
 mobile=Mobile
-totpStep1=Install <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> or Google Authenticator on your device. Both applications are available in <a href="https://play.google.com">Google Play</a> and Apple App Store.
-totpStep2=Open the application and scan the barcode or enter the key.
+totpStep1=Install one of the following applications on your mobile
+totpStep2=Open the application and scan the barcode
 totpStep3=Enter the one-time code provided by the application and click Save to finish the setup.
 
+totpManualStep2=Open the application and enter the key
+totpManualStep3=Use the following configuration values if the application allows setting them
+totpUnableToScan=Unable to scan?
+totpScanBarcode=Scan barcode?
+
+totp.totp=Time-based
+totp.hotp=Counter-based
+
+totpType=Type
+totpAlgorithm=Algorithm
+totpDigits=Digits
+totpInterval=Interval
+
 missingUsernameMessage=Please specify username.
 missingFirstNameMessage=Please specify first name.
 invalidEmailMessage=Invalid email address.
diff --git a/themes/src/main/resources/theme/base/account/totp.ftl b/themes/src/main/resources/theme/base/account/totp.ftl
index 7115938..468260e 100755
--- a/themes/src/main/resources/theme/base/account/totp.ftl
+++ b/themes/src/main/resources/theme/base/account/totp.ftl
@@ -30,13 +30,37 @@
 
 <ol>
     <li>
-        <p>${msg("totpStep1")?no_esc}</p>
-    </li>
-    <li>
-        <p>${msg("totpStep2")}</p>
-        <p><img src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"></p>
-        <p><span class="code">${totp.totpSecretEncoded}</span></p>
+        <p>${msg("totpStep1")}</p>
+
+        <ul>
+            <#list totp.policy.supportedApplications as app>
+            <li>${app}</li>
+            </#list>
+        </ul>
     </li>
+
+    <#if mode?? && mode = "manual">
+        <li>
+            <p>${msg("totpManualStep2")}</p>
+            <p><span id="kc-totp-secret-key">${totp.totpSecretEncoded}</span></p>
+            <p><a href="${totp.qrUrl}" id="mode-barcode">${msg("totpScanBarcode")}</a></p>
+        </li>
+        <li>
+            <p>${msg("totpManualStep3")}</p>
+            <ul>
+                <li id="kc-totp-type">${msg("totpType")}: ${msg("totp." + totp.policy.type)}</li>
+                <li id="kc-totp-algorithm">${msg("totpAlgorithm")}: ${totp.policy.algorithm}</li>
+                <li id="kc-totp-digits">${msg("totpDigits")}: ${totp.policy.digits}</li>
+                <li id="kc-totp-period">${msg("totpInterval")}: ${totp.policy.period}</li>
+            </ul>
+        </li>
+    <#else>
+        <li>
+            <p>${msg("totpStep2")}</p>
+            <p><img src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"></p>
+            <p><a href="${totp.manualUrl}" id="mode-manual">${msg("totpUnableToScan")}</a></p>
+        </li>
+    </#if>
     <li>
         <p>${msg("totpStep3")}</p>
     </li>
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index dd69970..443a5c8 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -891,6 +891,8 @@ initial-counter=Initial Counter
 otp.initial-counter.tooltip=What should the initial counter value be?
 otp-token-period=OTP Token Period
 otp-token-period.tooltip=How many seconds should an OTP token be valid? Defaults to 30 seconds.
+otp-supported-applications=Supported Applications
+otp-supported-applications.tooltip=Applications that are known to work with the current OTP policy
 table-of-password-policies=Table of Password Policies
 add-policy.placeholder=Add policy...
 policy-type=Policy Type
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index b54ea0f..283fc6b 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -338,7 +338,7 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, ser
     };
 });
 
-function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, url) {
+function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, url) {
     $scope.realm = angular.copy(realm);
     $scope.serverInfo = serverInfo;
     $scope.registrationAllowed = $scope.realm.registrationAllowed;
@@ -359,16 +359,7 @@ function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $l
         $scope.changed = false;
         console.log('oldCopy.realm - ' + oldCopy.realm);
         Realm.update({ id : oldCopy.realm}, realmCopy, function () {
-            var data = Realm.query(function () {
-                Current.realms = data;
-                for (var i = 0; i < Current.realms.length; i++) {
-                    if (Current.realms[i].realm == realmCopy.realm) {
-                        Current.realm = Current.realms[i];
-                        oldCopy = angular.copy($scope.realm);
-                    }
-                }
-            });
-            $location.url(url);
+            $route.reload();
             Notifications.success("Your changes have been saved to the realm.");
             $scope.registrationAllowed = $scope.realm.registrationAllowed;
         });
@@ -380,17 +371,16 @@ function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $l
     };
 
     $scope.cancel = function() {
-        //$location.url("/realms");
-        window.history.back();
+        $route.reload();
     };
 
 }
 
-module.controller('DefenseHeadersCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
-    genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/defense/headers");
+module.controller('DefenseHeadersCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
+    genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/defense/headers");
 });
 
-module.controller('RealmLoginSettingsCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
+module.controller('RealmLoginSettingsCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
     // KEYCLOAK-5474: Make sure duplicateEmailsAllowed is disabled if loginWithEmailAllowed
     $scope.$watch('realm.loginWithEmailAllowed', function() {
         if ($scope.realm.loginWithEmailAllowed) {
@@ -398,18 +388,18 @@ module.controller('RealmLoginSettingsCtrl', function($scope, Current, Realm, rea
         }
     });
     
-    genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/login-settings");
+    genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/login-settings");
 });
 
-module.controller('RealmOtpPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
+module.controller('RealmOtpPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
     $scope.optionsDigits = [ 6, 8 ];
 
-    genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/otp-policy");
+    genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/otp-policy");
 });
 
 
-module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
-    genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");
+module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
+    genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");
 
     $scope.supportedLocalesOptions = {
         'multiple' : true,
@@ -1975,7 +1965,7 @@ module.controller('IdentityProviderMapperCreateCtrl', function($scope, realm, id
 
 });
 
-module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
+module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
     $scope.flows = [];
     $scope.clientFlows = [];
     for (var i=0 ; i<flows.length ; i++) {
@@ -1988,7 +1978,7 @@ module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm
 
     $scope.profileInfo = serverInfo.profileInfo;
 
-    genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/flow-bindings");
+    genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/flow-bindings");
 });
 
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html b/themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html
index 941daad..2e4a3e8 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html
@@ -65,6 +65,14 @@
             <kc-tooltip>{{:: 'otp-token-period.tooltip' | translate}}</kc-tooltip>
         </div>
 
+        <div class="form-group">
+            <label class="col-md-2 control-label">{{:: 'otp-supported-applications' | translate}}</label>
+            <div class="col-md-6">
+                {{realm.otpSupportedApplications.join(', ')}}
+            </div>
+            <kc-tooltip>{{:: 'otp-supported-applications.tooltip' | translate}}</kc-tooltip>
+        </div>
+
 
         <div class="form-group" data-ng-show="access.manageRealm">
             <div class="col-md-10 col-md-offset-2">
diff --git a/themes/src/main/resources/theme/base/login/login-config-totp.ftl b/themes/src/main/resources/theme/base/login/login-config-totp.ftl
index ea2d6b0..24383ec 100755
--- a/themes/src/main/resources/theme/base/login/login-config-totp.ftl
+++ b/themes/src/main/resources/theme/base/login/login-config-totp.ftl
@@ -5,19 +5,46 @@
     <#elseif section = "header">
         ${msg("loginTotpTitle")}
     <#elseif section = "form">
-<ol id="kc-totp-settings">
-    <li>
-        <p>${msg("loginTotpStep1")?no_esc}</p>
-        </li>
-    <li>
-        <p>${msg("loginTotpStep2")}</p>
-        <img id="kc-totp-secret-qr-code" src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"><br/>
-        <span class="code">${totp.totpSecretEncoded}</span>
+
+
+    <ol id="kc-totp-settings">
+        <li>
+            <p>${msg("loginTotpStep1")}</p>
+
+            <ul id="kc-totp-supported-apps">
+                <#list totp.policy.supportedApplications as app>
+                <li>${app}</li>
+                </#list>
+            </ul>
         </li>
-    <li>
-        <p>${msg("loginTotpStep3")}</p>
+
+        <#if mode?? && mode = "manual">
+            <li>
+                <p>${msg("loginTotpManualStep2")}</p>
+                <p><span id="kc-totp-secret-key">${totp.totpSecretEncoded}</span></p>
+                <p><a href="${totp.qrUrl}" id="mode-barcode">${msg("loginTotpScanBarcode")}</a></p>
+            </li>
+            <li>
+                <p>${msg("loginTotpManualStep3")}</p>
+                <ul>
+                    <li id="kc-totp-type">${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}</li>
+                    <li id="kc-totp-algorithm">${msg("loginTotpAlgorithm")}: ${totp.policy.algorithm}</li>
+                    <li id="kc-totp-digits">${msg("loginTotpDigits")}: ${totp.policy.digits}</li>
+                    <li id="kc-totp-period">${msg("loginTotpInterval")}: ${totp.policy.period}</li>
+                </ul>
+            </li>
+        <#else>
+            <li>
+                <p>${msg("loginTotpStep2")}</p>
+                <img id="kc-totp-secret-qr-code" src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"><br/>
+                <p><a href="${totp.manualUrl}" id="mode-manual">${msg("loginTotpUnableToScan")}</a></p>
+            </li>
+        </#if>
+        <li>
+            <p>${msg("loginTotpStep3")}</p>
         </li>
     </ol>
+
     <form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-totp-settings-form" method="post">
         <div class="${properties.kcFormGroupClass!}">
             <div class="${properties.kcInputWrapperClass!}">
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 03f2c47..7c5a22d 100755
--- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -68,10 +68,22 @@ country=Country
 emailVerified=Email verified
 gssDelegationCredential=GSS Delegation Credential
 
-loginTotpStep1=Install <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> or Google Authenticator on your mobile. Both applications are available in <a href="https://play.google.com">Google Play</a> and Apple App Store.
-loginTotpStep2=Open the application and scan the barcode or enter the key
+loginTotpStep1=Install one of the following applications on your mobile
+loginTotpStep2=Open the application and scan the barcode
 loginTotpStep3=Enter the one-time code provided by the application and click Submit to finish the setup
+loginTotpManualStep2=Open the application and enter the key
+loginTotpManualStep3=Use the following configuration values if the application allows setting them
+loginTotpUnableToScan=Unable to scan?
+loginTotpScanBarcode=Scan barcode?
 loginTotpOneTime=One-time code
+loginTotpType=Type
+loginTotpAlgorithm=Algorithm
+loginTotpDigits=Digits
+loginTotpInterval=Interval
+
+loginTotp.totp=Time-based
+loginTotp.hotp=Counter-based
+
 
 oauthGrantRequest=Do you grant these access privileges?
 inResource=in
diff --git a/themes/src/main/resources/theme/keycloak/account/resources/css/account.css b/themes/src/main/resources/theme/keycloak/account/resources/css/account.css
index 3014bca..3878e43 100644
--- a/themes/src/main/resources/theme/keycloak/account/resources/css/account.css
+++ b/themes/src/main/resources/theme/keycloak/account/resources/css/account.css
@@ -267,3 +267,11 @@ hr + .form-horizontal {
 .kc-dropdown:hover ul{
     display:block;
 }
+
+
+#kc-totp-secret-key {
+    border: 1px solid #eee;
+    font-size: 16px;
+    padding: 10px;
+    margin: 50px 0;
+}
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/keycloak/login/resources/css/login.css b/themes/src/main/resources/theme/keycloak/login/resources/css/login.css
index 55deb32..3c1a31a 100644
--- a/themes/src/main/resources/theme/keycloak/login/resources/css/login.css
+++ b/themes/src/main/resources/theme/keycloak/login/resources/css/login.css
@@ -156,6 +156,13 @@ ol#kc-totp-settings li:first-of-type {
     max-height:150px;
 }
 
+#kc-totp-secret-key {
+    background-color: #fff;
+    color: #333333;
+    font-size: 16px;
+    padding: 10px;
+}
+
 /* OAuth */
 
 #kc-oauth h3 {