keycloak-memoizeit
Changes
services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java 18(+16 -2)
services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java 14(+14 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java 14(+14 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java 35(+35 -0)
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 {