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 otpPolicyDigits;
|
||||||
protected Integer otpPolicyLookAheadWindow;
|
protected Integer otpPolicyLookAheadWindow;
|
||||||
protected Integer otpPolicyPeriod;
|
protected Integer otpPolicyPeriod;
|
||||||
|
protected List<String> otpSupportedApplications;
|
||||||
|
|
||||||
protected List<UserRepresentation> users;
|
protected List<UserRepresentation> users;
|
||||||
protected List<UserRepresentation> federatedUsers;
|
protected List<UserRepresentation> federatedUsers;
|
||||||
|
@ -854,6 +855,14 @@ public class RealmRepresentation {
|
||||||
this.otpPolicyPeriod = otpPolicyPeriod;
|
this.otpPolicyPeriod = otpPolicyPeriod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getOtpSupportedApplications() {
|
||||||
|
return otpSupportedApplications;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpSupportedApplications(List<String> otpSupportedApplications) {
|
||||||
|
this.otpSupportedApplications = otpSupportedApplications;
|
||||||
|
}
|
||||||
|
|
||||||
public String getBrowserFlow() {
|
public String getBrowserFlow() {
|
||||||
return browserFlow;
|
return browserFlow;
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,4 +66,6 @@ public interface AccountProvider extends Provider {
|
||||||
AccountProvider setStateChecker(String stateChecker);
|
AccountProvider setStateChecker(String stateChecker);
|
||||||
|
|
||||||
AccountProvider setFeatures(boolean social, boolean events, boolean passwordUpdateSupported);
|
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.setOtpPolicyInitialCounter(otpPolicy.getInitialCounter());
|
||||||
rep.setOtpPolicyType(otpPolicy.getType());
|
rep.setOtpPolicyType(otpPolicy.getType());
|
||||||
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
|
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
|
||||||
|
rep.setOtpSupportedApplications(otpPolicy.getSupportedApplications());
|
||||||
if (realm.getBrowserFlow() != null) rep.setBrowserFlow(realm.getBrowserFlow().getAlias());
|
if (realm.getBrowserFlow() != null) rep.setBrowserFlow(realm.getBrowserFlow().getAlias());
|
||||||
if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
|
if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
|
||||||
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
|
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
|
||||||
|
|
|
@ -25,6 +25,8 @@ import java.io.Serializable;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
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 Map<String, String> algToKeyUriAlg = new HashMap<>();
|
||||||
|
|
||||||
|
private static final OtpApp[] allApplications = new OtpApp[] { new FreeOTP(), new GoogleAuthenticator() };
|
||||||
|
|
||||||
static {
|
static {
|
||||||
algToKeyUriAlg.put(HmacOTP.HMAC_SHA1, "SHA1");
|
algToKeyUriAlg.put(HmacOTP.HMAC_SHA1, "SHA1");
|
||||||
algToKeyUriAlg.put(HmacOTP.HMAC_SHA256, "SHA256");
|
algToKeyUriAlg.put(HmacOTP.HMAC_SHA256, "SHA256");
|
||||||
|
@ -151,4 +155,60 @@ public class OTPPolicy implements Serializable {
|
||||||
throw new RuntimeException(e);
|
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
|
@Override
|
||||||
public void requiredActionChallenge(RequiredActionContext context) {
|
public void requiredActionChallenge(RequiredActionContext context) {
|
||||||
Response challenge = context.form()
|
Response challenge = context.form()
|
||||||
|
.setAttribute("mode", getMode(context))
|
||||||
.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
|
.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
|
||||||
context.challenge(challenge);
|
context.challenge(challenge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getMode(RequiredActionContext context) {
|
||||||
|
return context.getUriInfo().getQueryParameters().getFirst("mode");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void processAction(RequiredActionContext context) {
|
public void processAction(RequiredActionContext context) {
|
||||||
EventBuilder event = context.getEvent();
|
EventBuilder event = context.getEvent();
|
||||||
|
@ -60,12 +65,14 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
|
||||||
|
|
||||||
if (Validation.isBlank(totp)) {
|
if (Validation.isBlank(totp)) {
|
||||||
Response challenge = context.form()
|
Response challenge = context.form()
|
||||||
|
.setAttribute("mode", getMode(context))
|
||||||
.setError(Messages.MISSING_TOTP)
|
.setError(Messages.MISSING_TOTP)
|
||||||
.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
|
.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
|
||||||
context.challenge(challenge);
|
context.challenge(challenge);
|
||||||
return;
|
return;
|
||||||
} else if (!CredentialValidation.validOTP(context.getRealm(), totp, totpSecret)) {
|
} else if (!CredentialValidation.validOTP(context.getRealm(), totp, totpSecret)) {
|
||||||
Response challenge = context.form()
|
Response challenge = context.form()
|
||||||
|
.setAttribute("mode", getMode(context))
|
||||||
.setError(Messages.INVALID_TOTP)
|
.setError(Messages.INVALID_TOTP)
|
||||||
.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
|
.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
|
||||||
context.challenge(challenge);
|
context.challenge(challenge);
|
||||||
|
|
|
@ -86,6 +86,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
|
||||||
protected KeycloakSession session;
|
protected KeycloakSession session;
|
||||||
protected FreeMarkerUtil freeMarker;
|
protected FreeMarkerUtil freeMarker;
|
||||||
protected HttpHeaders headers;
|
protected HttpHeaders headers;
|
||||||
|
protected Map<String, Object> attributes;
|
||||||
|
|
||||||
protected UriInfo uriInfo;
|
protected UriInfo uriInfo;
|
||||||
|
|
||||||
|
@ -110,7 +111,11 @@ public class FreeMarkerAccountProvider implements AccountProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response createResponse(AccountPages page) {
|
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;
|
Theme theme;
|
||||||
try {
|
try {
|
||||||
|
@ -156,7 +161,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
|
||||||
|
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case TOTP:
|
case TOTP:
|
||||||
attributes.put("totp", new TotpBean(session, realm, user));
|
attributes.put("totp", new TotpBean(session, realm, user, uriInfo.getRequestUriBuilder()));
|
||||||
break;
|
break;
|
||||||
case FEDERATED_IDENTITY:
|
case FEDERATED_IDENTITY:
|
||||||
attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker));
|
attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker));
|
||||||
|
@ -361,6 +366,15 @@ public class FreeMarkerAccountProvider implements AccountProvider {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountProvider setAttribute(String key, String value) {
|
||||||
|
if (attributes == null) {
|
||||||
|
attributes = new HashMap<>();
|
||||||
|
}
|
||||||
|
attributes.put(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,25 +18,32 @@
|
||||||
package org.keycloak.forms.account.freemarker.model;
|
package org.keycloak.forms.account.freemarker.model;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.HmacOTP;
|
import org.keycloak.models.utils.HmacOTP;
|
||||||
import org.keycloak.utils.TotpUtils;
|
import org.keycloak.utils.TotpUtils;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class TotpBean {
|
public class TotpBean {
|
||||||
|
|
||||||
|
private final RealmModel realm;
|
||||||
private final String totpSecret;
|
private final String totpSecret;
|
||||||
private final String totpSecretEncoded;
|
private final String totpSecretEncoded;
|
||||||
private final String totpSecretQrCode;
|
private final String totpSecretQrCode;
|
||||||
private final boolean enabled;
|
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.enabled = session.userCredentialManager().isConfiguredFor(realm, user, realm.getOTPPolicy().getType());
|
||||||
|
|
||||||
|
this.realm = realm;
|
||||||
this.totpSecret = HmacOTP.generateSecret(20);
|
this.totpSecret = HmacOTP.generateSecret(20);
|
||||||
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
|
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
|
||||||
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
|
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
|
||||||
|
@ -58,5 +65,17 @@ public class TotpBean {
|
||||||
return totpSecretQrCode;
|
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) {
|
switch (page) {
|
||||||
case LOGIN_CONFIG_TOTP:
|
case LOGIN_CONFIG_TOTP:
|
||||||
attributes.put("totp", new TotpBean(session, realm, user));
|
attributes.put("totp", new TotpBean(session, realm, user, uriInfo.getRequestUriBuilder()));
|
||||||
break;
|
break;
|
||||||
case LOGIN_UPDATE_PROFILE:
|
case LOGIN_UPDATE_PROFILE:
|
||||||
UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
|
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.credential.CredentialModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.HmacOTP;
|
import org.keycloak.models.utils.HmacOTP;
|
||||||
import org.keycloak.utils.TotpUtils;
|
import org.keycloak.utils.TotpUtils;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class TotpBean {
|
public class TotpBean {
|
||||||
|
|
||||||
|
private final RealmModel realm;
|
||||||
private final String totpSecret;
|
private final String totpSecret;
|
||||||
private final String totpSecretEncoded;
|
private final String totpSecretEncoded;
|
||||||
private final String totpSecretQrCode;
|
private final String totpSecretQrCode;
|
||||||
private final boolean enabled;
|
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.enabled = session.userCredentialManager().isConfiguredFor(realm, user, CredentialModel.OTP);
|
||||||
this.totpSecret = HmacOTP.generateSecret(20);
|
this.totpSecret = HmacOTP.generateSecret(20);
|
||||||
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
|
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
|
||||||
|
@ -56,5 +63,18 @@ public class TotpBean {
|
||||||
return totpSecretQrCode;
|
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;
|
package org.keycloak.services.resources.account;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.authentication.RequiredActionContext;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.common.util.UriUtils;
|
import org.keycloak.common.util.UriUtils;
|
||||||
|
@ -230,6 +231,7 @@ public class AccountFormService extends AbstractSecuredLocalService {
|
||||||
@Path("totp")
|
@Path("totp")
|
||||||
@GET
|
@GET
|
||||||
public Response totpPage() {
|
public Response totpPage() {
|
||||||
|
account.setAttribute("mode", uriInfo.getQueryParameters().getFirst("mode"));
|
||||||
return forwardToPage("totp", AccountPages.TOTP);
|
return forwardToPage("totp", AccountPages.TOTP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,6 +444,8 @@ public class AccountFormService extends AbstractSecuredLocalService {
|
||||||
|
|
||||||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||||
|
|
||||||
|
account.setAttribute("mode", uriInfo.getQueryParameters().getFirst("mode"));
|
||||||
|
|
||||||
String action = formData.getFirst("submitAction");
|
String action = formData.getFirst("submitAction");
|
||||||
if (action != null && action.equals("Cancel")) {
|
if (action != null && action.equals("Cancel")) {
|
||||||
setReferrerOnPage();
|
setReferrerOnPage();
|
||||||
|
|
|
@ -39,6 +39,12 @@ public class AccountTotpPage extends AbstractAccountPage {
|
||||||
@FindBy(id = "remove-mobile")
|
@FindBy(id = "remove-mobile")
|
||||||
private WebElement removeLink;
|
private WebElement removeLink;
|
||||||
|
|
||||||
|
@FindBy(id = "mode-barcode")
|
||||||
|
private WebElement barcodeLink;
|
||||||
|
|
||||||
|
@FindBy(id = "mode-manual")
|
||||||
|
private WebElement manualLink;
|
||||||
|
|
||||||
private String getPath() {
|
private String getPath() {
|
||||||
return AccountFormService.totpUrl(UriBuilder.fromUri(getAuthServerRoot())).build("test").toString();
|
return AccountFormService.totpUrl(UriBuilder.fromUri(getAuthServerRoot())).build("test").toString();
|
||||||
}
|
}
|
||||||
|
@ -64,4 +70,12 @@ public class AccountTotpPage extends AbstractAccountPage {
|
||||||
removeLink.click();
|
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\"]")
|
@FindBy(css = "input[type=\"submit\"]")
|
||||||
private WebElement submitButton;
|
private WebElement submitButton;
|
||||||
|
|
||||||
|
@FindBy(id = "mode-barcode")
|
||||||
|
private WebElement barcodeLink;
|
||||||
|
|
||||||
|
@FindBy(id = "mode-manual")
|
||||||
|
private WebElement manualLink;
|
||||||
|
|
||||||
public void configure(String totp) {
|
public void configure(String totp) {
|
||||||
totpInput.sendKeys(totp);
|
totpInput.sendKeys(totp);
|
||||||
submitButton.click();
|
submitButton.click();
|
||||||
|
@ -50,4 +56,12 @@ public class LoginConfigTotpPage extends AbstractPage {
|
||||||
throw new UnsupportedOperationException();
|
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.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.hasItems;
|
import static org.hamcrest.Matchers.hasItems;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @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"));
|
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
|
// Error with false code
|
||||||
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret() + "123"));
|
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret() + "123"));
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
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.pages.RegisterPage;
|
||||||
import org.keycloak.testsuite.util.RealmBuilder;
|
import org.keycloak.testsuite.util.RealmBuilder;
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
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>
|
* @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();
|
String userId = events.expectRegister("setupTotp", "email@mail.com").assertEvent().getUserId();
|
||||||
|
|
||||||
Assert.assertTrue(totpPage.isCurrent());
|
assertTrue(totpPage.isCurrent());
|
||||||
|
|
||||||
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));
|
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));
|
||||||
|
|
||||||
String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent()
|
String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent()
|
||||||
.getDetails().get(Details.CODE_ID);
|
.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();
|
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
|
@Test
|
||||||
public void setupTotpExisting() {
|
public void setupTotpExisting() {
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
|
@ -149,7 +242,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
|
||||||
String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
|
String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
|
||||||
.getDetails().get(Details.CODE_ID);
|
.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();
|
EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent();
|
||||||
|
|
||||||
|
@ -162,7 +255,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
|
||||||
String src = driver.getPageSource();
|
String src = driver.getPageSource();
|
||||||
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
||||||
|
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
events.expectLogin().assertEvent();
|
events.expectLogin().assertEvent();
|
||||||
}
|
}
|
||||||
|
@ -185,7 +278,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
|
||||||
totpPage.configure(totp.generateTOTP(totpCode));
|
totpPage.configure(totp.generateTOTP(totpCode));
|
||||||
|
|
||||||
// After totp config, user should be on the app page
|
// 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();
|
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
|
// Totp is already configured, thus one-time password is needed, login page should be loaded
|
||||||
String uri = driver.getCurrentUrl();
|
String uri = driver.getCurrentUrl();
|
||||||
String src = driver.getPageSource();
|
String src = driver.getPageSource();
|
||||||
Assert.assertTrue(loginPage.isCurrent());
|
assertTrue(loginPage.isCurrent());
|
||||||
Assert.assertFalse(totpPage.isCurrent());
|
Assert.assertFalse(totpPage.isCurrent());
|
||||||
|
|
||||||
// Login with one-time password
|
// 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()
|
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent()
|
||||||
.getDetails().get(Details.CODE_ID);
|
.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();
|
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()
|
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
|
||||||
.getDetails().get(Details.CODE_ID);
|
.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();
|
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
|
||||||
|
|
||||||
|
@ -278,10 +371,10 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
|
||||||
loginPage.login("test-user@localhost", "password");
|
loginPage.login("test-user@localhost", "password");
|
||||||
String src = driver.getPageSource();
|
String src = driver.getPageSource();
|
||||||
String token = timeBased.generateTOTP(totpSecret);
|
String token = timeBased.generateTOTP(totpSecret);
|
||||||
Assert.assertEquals(8, token.length());
|
assertEquals(8, token.length());
|
||||||
loginTotpPage.login(token);
|
loginTotpPage.login(token);
|
||||||
|
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
events.expectLogin().assertEvent();
|
events.expectLogin().assertEvent();
|
||||||
|
|
||||||
|
@ -318,7 +411,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
|
||||||
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
|
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
|
||||||
.getDetails().get(Details.CODE_ID);
|
.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();
|
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
|
||||||
|
|
||||||
|
@ -331,7 +424,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
|
||||||
String token = otpgen.generateHOTP(totpSecret, 1);
|
String token = otpgen.generateHOTP(totpSecret, 1);
|
||||||
loginTotpPage.login(token);
|
loginTotpPage.login(token);
|
||||||
|
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
events.expectLogin().assertEvent();
|
events.expectLogin().assertEvent();
|
||||||
|
|
||||||
|
@ -356,7 +449,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
|
||||||
loginTotpPage.assertCurrent();
|
loginTotpPage.assertCurrent();
|
||||||
loginTotpPage.login(token);
|
loginTotpPage.login(token);
|
||||||
|
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
events.expectLogin().assertEvent();
|
events.expectLogin().assertEvent();
|
||||||
|
|
||||||
|
|
|
@ -98,10 +98,23 @@ revoke=Revoke Grant
|
||||||
|
|
||||||
configureAuthenticators=Configured Authenticators
|
configureAuthenticators=Configured Authenticators
|
||||||
mobile=Mobile
|
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.
|
totpStep1=Install one of the following applications on your mobile
|
||||||
totpStep2=Open the application and scan the barcode or enter the key.
|
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.
|
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.
|
missingUsernameMessage=Please specify username.
|
||||||
missingFirstNameMessage=Please specify first name.
|
missingFirstNameMessage=Please specify first name.
|
||||||
invalidEmailMessage=Invalid email address.
|
invalidEmailMessage=Invalid email address.
|
||||||
|
|
|
@ -30,13 +30,37 @@
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
<p>${msg("totpStep1")?no_esc}</p>
|
<p>${msg("totpStep1")}</p>
|
||||||
</li>
|
|
||||||
<li>
|
<ul>
|
||||||
<p>${msg("totpStep2")}</p>
|
<#list totp.policy.supportedApplications as app>
|
||||||
<p><img src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"></p>
|
<li>${app}</li>
|
||||||
<p><span class="code">${totp.totpSecretEncoded}</span></p>
|
</#list>
|
||||||
|
</ul>
|
||||||
</li>
|
</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>
|
<li>
|
||||||
<p>${msg("totpStep3")}</p>
|
<p>${msg("totpStep3")}</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -891,6 +891,8 @@ initial-counter=Initial Counter
|
||||||
otp.initial-counter.tooltip=What should the initial counter value be?
|
otp.initial-counter.tooltip=What should the initial counter value be?
|
||||||
otp-token-period=OTP Token Period
|
otp-token-period=OTP Token Period
|
||||||
otp-token-period.tooltip=How many seconds should an OTP token be valid? Defaults to 30 seconds.
|
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
|
table-of-password-policies=Table of Password Policies
|
||||||
add-policy.placeholder=Add policy...
|
add-policy.placeholder=Add policy...
|
||||||
policy-type=Policy Type
|
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.realm = angular.copy(realm);
|
||||||
$scope.serverInfo = serverInfo;
|
$scope.serverInfo = serverInfo;
|
||||||
$scope.registrationAllowed = $scope.realm.registrationAllowed;
|
$scope.registrationAllowed = $scope.realm.registrationAllowed;
|
||||||
|
@ -359,16 +359,7 @@ function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $l
|
||||||
$scope.changed = false;
|
$scope.changed = false;
|
||||||
console.log('oldCopy.realm - ' + oldCopy.realm);
|
console.log('oldCopy.realm - ' + oldCopy.realm);
|
||||||
Realm.update({ id : oldCopy.realm}, realmCopy, function () {
|
Realm.update({ id : oldCopy.realm}, realmCopy, function () {
|
||||||
var data = Realm.query(function () {
|
$route.reload();
|
||||||
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);
|
|
||||||
Notifications.success("Your changes have been saved to the realm.");
|
Notifications.success("Your changes have been saved to the realm.");
|
||||||
$scope.registrationAllowed = $scope.realm.registrationAllowed;
|
$scope.registrationAllowed = $scope.realm.registrationAllowed;
|
||||||
});
|
});
|
||||||
|
@ -380,17 +371,16 @@ function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $l
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.cancel = function() {
|
$scope.cancel = function() {
|
||||||
//$location.url("/realms");
|
$route.reload();
|
||||||
window.history.back();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.controller('DefenseHeadersCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
|
module.controller('DefenseHeadersCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
|
||||||
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/defense/headers");
|
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
|
// KEYCLOAK-5474: Make sure duplicateEmailsAllowed is disabled if loginWithEmailAllowed
|
||||||
$scope.$watch('realm.loginWithEmailAllowed', function() {
|
$scope.$watch('realm.loginWithEmailAllowed', function() {
|
||||||
if ($scope.realm.loginWithEmailAllowed) {
|
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 ];
|
$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) {
|
module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
|
||||||
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");
|
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");
|
||||||
|
|
||||||
$scope.supportedLocalesOptions = {
|
$scope.supportedLocalesOptions = {
|
||||||
'multiple' : true,
|
'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.flows = [];
|
||||||
$scope.clientFlows = [];
|
$scope.clientFlows = [];
|
||||||
for (var i=0 ; i<flows.length ; i++) {
|
for (var i=0 ; i<flows.length ; i++) {
|
||||||
|
@ -1988,7 +1978,7 @@ module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm
|
||||||
|
|
||||||
$scope.profileInfo = serverInfo.profileInfo;
|
$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>
|
<kc-tooltip>{{:: 'otp-token-period.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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="form-group" data-ng-show="access.manageRealm">
|
||||||
<div class="col-md-10 col-md-offset-2">
|
<div class="col-md-10 col-md-offset-2">
|
||||||
|
|
|
@ -5,19 +5,46 @@
|
||||||
<#elseif section = "header">
|
<#elseif section = "header">
|
||||||
${msg("loginTotpTitle")}
|
${msg("loginTotpTitle")}
|
||||||
<#elseif section = "form">
|
<#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>
|
||||||
<li>
|
|
||||||
<p>${msg("loginTotpStep2")}</p>
|
<#if mode?? && mode = "manual">
|
||||||
<img id="kc-totp-secret-qr-code" src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"><br/>
|
<li>
|
||||||
<span class="code">${totp.totpSecretEncoded}</span>
|
<p>${msg("loginTotpManualStep2")}</p>
|
||||||
</li>
|
<p><span id="kc-totp-secret-key">${totp.totpSecretEncoded}</span></p>
|
||||||
<li>
|
<p><a href="${totp.qrUrl}" id="mode-barcode">${msg("loginTotpScanBarcode")}</a></p>
|
||||||
<p>${msg("loginTotpStep3")}</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>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-totp-settings-form" method="post">
|
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-totp-settings-form" method="post">
|
||||||
<div class="${properties.kcFormGroupClass!}">
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
<div class="${properties.kcInputWrapperClass!}">
|
<div class="${properties.kcInputWrapperClass!}">
|
||||||
|
|
|
@ -68,10 +68,22 @@ country=Country
|
||||||
emailVerified=Email verified
|
emailVerified=Email verified
|
||||||
gssDelegationCredential=GSS Delegation Credential
|
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.
|
loginTotpStep1=Install one of the following applications on your mobile
|
||||||
loginTotpStep2=Open the application and scan the barcode or enter the key
|
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
|
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
|
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?
|
oauthGrantRequest=Do you grant these access privileges?
|
||||||
inResource=in
|
inResource=in
|
||||||
|
|
|
@ -267,3 +267,11 @@ hr + .form-horizontal {
|
||||||
.kc-dropdown:hover ul{
|
.kc-dropdown:hover ul{
|
||||||
display:block;
|
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;
|
max-height:150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#kc-totp-secret-key {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* OAuth */
|
/* OAuth */
|
||||||
|
|
||||||
#kc-oauth h3 {
|
#kc-oauth h3 {
|
||||||
|
|
Loading…
Reference in a new issue