KEYCLOAK-2120 Added manual setup page for OTP
This commit is contained in:
parent
118e998570
commit
b303acaaba
23 changed files with 444 additions and 61 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
@ -361,6 +366,15 @@ public class FreeMarkerAccountProvider implements AccountProvider {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountProvider setAttribute(String key, String value) {
|
||||
if (attributes == null) {
|
||||
attributes = new HashMap<>();
|
||||
}
|
||||
attributes.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
|
||||
|
|
|
@ -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,18 +130,104 @@ 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();
|
||||
|
@ -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();
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -5,19 +5,46 @@
|
|||
<#elseif section = "header">
|
||||
${msg("loginTotpTitle")}
|
||||
<#elseif section = "form">
|
||||
<ol id="kc-totp-settings">
|
||||
<li>
|
||||
<p>${msg("loginTotpStep1")?no_esc}</p>
|
||||
|
||||
|
||||
<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("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>
|
||||
</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!}">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue