Details
diff --git a/forms/src/main/java/org/keycloak/forms/UrlBean.java b/forms/src/main/java/org/keycloak/forms/UrlBean.java
index fdb11fe..cb82aaa 100644
--- a/forms/src/main/java/org/keycloak/forms/UrlBean.java
+++ b/forms/src/main/java/org/keycloak/forms/UrlBean.java
@@ -120,6 +120,10 @@ public class UrlBean {
return Urls.accountTotpPage(baseURI, realm.getId()).toString();
}
+ public String getTotpRemoveUrl() {
+ return Urls.accountTotpRemove(baseURI, realm.getId()).toString();
+ }
+
public String getEmailVerificationUrl() {
return Urls.accountEmailVerification(baseURI, realm.getId()).toString();
}
diff --git a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
index 1cd60fa..27c2df1 100644
--- a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
+++ b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
@@ -81,6 +81,10 @@ public class FormServiceImpl implements FormService {
Map<String, Object> attributes = new HashMap<String, Object>();
+ if (dataBean.getError() != null){
+ attributes.put("message", new ErrorBean(dataBean.getError(), dataBean.getErrorType()));
+ }
+
RealmBean realm = new RealmBean(dataBean.getRealm());
attributes.put("template", new TemplateBean(realm, dataBean.getContextPath()));
@@ -145,10 +149,6 @@ public class FormServiceImpl implements FormService {
private class CommandPassword implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
- if (dataBean.getError() != null){
- attributes.put("message", new ErrorBean(dataBean.getError(), dataBean.getErrorType()));
- }
-
RealmBean realm = new RealmBean(dataBean.getRealm());
attributes.put("realm", realm);
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/header.css b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/header.css
index c3b1bd7..04e3340 100644
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/header.css
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/header.css
@@ -60,6 +60,7 @@ body {
}
.header.rcue .navbar {
margin-bottom: 0;
+ z-index:auto;
}
.header.rcue .navbar.primary {
font-size: 13px;
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl
index e456284..253ab75 100755
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl
@@ -25,7 +25,7 @@
<!-- <p>Forgot <a href="#">Username</a> or <a href="#">Password</a>?</p> -->
</div>
- <input type="submit" value="Log In" />
+ <input class="btn-primary" type="submit" value="Log In" />
</form>
<#elseif section = "info">
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl
index d8cca1f..ac7e496 100644
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl
@@ -35,7 +35,8 @@
</div>
<#if error?has_content>
- <div class="feedback error bottom-left show">
+ <div class="
+ error bottom-left show">
<p>
<strong id="loginError">${rb.getString(error.summary)}</strong>
</p>
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl
index 3cc7975..7cf9f71 100644
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl
@@ -3,7 +3,7 @@
<html>
<head>
<meta charset="utf-8">
- <title>Edit Account</title>
+ <title>Edit Account - <#nested "title"></title>
<link rel="icon" href="img/favicon.ico">
<!-- Frameworks -->
@@ -34,10 +34,14 @@
</head>
<body class="admin-console user ${bodyClass}">
- <#if error?has_content>
- <!--div class="feedback success show"><p><strong>Success!</strong> Your changes have been saved.</p></div-->
+ <#if message?has_content>
<div class="feedback-aligner">
- <div class="alert alert-danger">${rb.getString(error.summary)}</div>
+ <#if message.success>
+ <div class="feedback success show"><p><strong>${rb.getString('successHeader')}</strong> ${rb.getString(message.summary)}</p></div>
+ </#if>
+ <#if message.error>
+ <div class="feedback error show"><p><strong>${rb.getString('errorHeader')}</strong> ${rb.getString(message.summary)}</p></div>
+ </#if>
</div>
</#if>
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/totp.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/totp.ftl
index 63de6ef..5f4d6dc 100755
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/totp.ftl
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/totp.ftl
@@ -1,7 +1,9 @@
<#import "template-main.ftl" as layout>
<@layout.mainLayout active='totp' bodyClass='totp'; section>
- <#if section = "header">
+ <#if section = "title">
+ Google Authenticator
+ <#elseif section = "header">
<#if totp.enabled>
<h2>Authenticators</h2>
@@ -12,7 +14,6 @@
<#elseif section = "content">
<#if totp.enabled>
- <#-- TODO this is only mock page -->
<form>
<fieldset>
<p class="info">You have the following authenticators set up:</p>
@@ -21,11 +22,16 @@
<tbody>
<tr>
<td class="provider"><span class="social googleplus">Google</span></td>
- <td class="soft">Connected as john@google.com</td>
- <td class="action"><a href="user-totp-setup.html" class="button">Remove Google</a></td>
+ <td class="action">
+ <a href="${url.totpRemoveUrl}" class="button">Remove Google</a>
+ </td>
</tr>
</tbody>
</table>
+ <p class="info">
+ If the totp authentication is required by the realm and you remove your configured authenticator,
+ you will have to reconfigure it immediately or on the next login.
+ </p>
</fieldset>
</form>
diff --git a/forms/src/main/resources/org/keycloak/forms/messages.properties b/forms/src/main/resources/org/keycloak/forms/messages.properties
index 92722f9..67b737f 100644
--- a/forms/src/main/resources/org/keycloak/forms/messages.properties
+++ b/forms/src/main/resources/org/keycloak/forms/messages.properties
@@ -37,6 +37,9 @@ invalidPasswordExisting=Invalid existing password
invalidPasswordConfirm=Password confirmation doesn't match
invalidTotp=Invalid authenticator code
+successTotp=Google authenticator configured.
+successTotpRemoved=Google authenticator removed.
+
usernameExists=Username already exists
error=A system error has occured, contact admin
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index b78d9ed..2874971 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -164,6 +164,15 @@ public class AccountService {
return accessCodeEntry;
}
+ @Path("totp-remove")
+ @GET
+ public Response processTotpRemove() {
+ UserModel user = getUserFromAuthManager();
+ user.setTotp(false);
+ return Flows.forms(realm, request, uriInfo).setError("successTotpRemoved").setErrorType(FormFlows.ErrorType.SUCCESS)
+ .setUser(user).forwardToTotp();
+ }
+
@Path("totp")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@@ -206,7 +215,8 @@ public class AccountService {
if (accessCodeEntry != null) {
return redirectOauth(user, accessCodeEntry);
} else {
- return Flows.forms(realm, request, uriInfo).setUser(user).forwardToTotp();
+ return Flows.forms(realm, request, uriInfo).setError("successTotp").setErrorType(FormFlows.ErrorType.SUCCESS)
+ .setUser(user).forwardToTotp();
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
index 1da6a61..b6a86a2 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
@@ -55,6 +55,10 @@ public class Urls {
return accountBase(baseUri).path(AccountService.class, "totpPage").build(realmId);
}
+ public static URI accountTotpRemove(URI baseUri, String realmId) {
+ return accountBase(baseUri).path(AccountService.class, "processTotpRemove").build(realmId);
+ }
+
public static URI accountEmailVerification(URI baseUri, String realmId) {
return accountBase(baseUri).path(AccountService.class, "emailVerification").build(realmId);
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
index 916513d..bb70d97 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
@@ -30,6 +30,8 @@ import org.keycloak.services.managers.RealmManager;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AccountTotpPage;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginConfigTotpPage;
@@ -54,9 +56,6 @@ public class RequiredActionTotpSetupTest {
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
appRealm.addRequiredCredential(CredentialRepresentation.TOTP);
appRealm.setResetPasswordAllowed(true);
-
- UserModel user = appRealm.getUser("test-user@localhost");
- user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
}
});
@@ -77,6 +76,12 @@ public class RequiredActionTotpSetupTest {
protected LoginConfigTotpPage totpPage;
@WebResource
+ protected AccountTotpPage accountTotpPage;
+
+ @WebResource
+ protected OAuthClient oauth;
+
+ @WebResource
protected RegisterPage registerPage;
protected TimeBasedOTP totp = new TimeBasedOTP();
@@ -101,9 +106,69 @@ public class RequiredActionTotpSetupTest {
totpPage.assertCurrent();
- totpPage.configure(totp.generate(totpPage.getTotpSecret()));
+ String totpSecret = totpPage.getTotpSecret();
+
+ totpPage.configure(totp.generate(totpSecret));
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ oauth.openLogout();
+
+ loginPage.open();
+ loginPage.loginTotp("test-user@localhost", "password", totp.generate(totpSecret));
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
+ @Test
+ public void setupTotpRegisteredAfterTotpRemoval() {
+ // Register new user
+ loginPage.open();
+ loginPage.clickRegister();
+ registerPage.register("firstName2", "lastName2", "email2", "setupTotp2", "password2", "password2");
+
+ // Configure totp
+ totpPage.assertCurrent();
+
+ String totpCode = totpPage.getTotpSecret();
+ totpPage.configure(totp.generate(totpCode));
+
+ // After totp config, user should be on the app page
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ // Logout
+ oauth.openLogout();
+
+ // Try to login after logout
+ loginPage.open();
+ loginPage.login("setupTotp2", "password2");
+
+ // Totp is already configured, thus one-time password is needed, login page should be loaded
+ Assert.assertTrue(loginPage.isCurrent());
+ Assert.assertFalse(totpPage.isCurrent());
+
+ // Login with one-time password
+ loginPage.loginTotp("setupTotp2", "password2", totp.generate(totpCode));
+
+ // Open account page
+ accountTotpPage.open();
+ accountTotpPage.assertCurrent();
+
+ // Remove google authentificator
+ accountTotpPage.removeTotp();
+
+ // Logout
+ oauth.openLogout();
+
+ // Try to login
+ loginPage.open();
+ loginPage.login("setupTotp2", "password2");
+
+ // Since the authentificator was removed, it has to be set up again
+ totpPage.assertCurrent();
+ totpPage.configure(totp.generate(totpPage.getTotpSecret()));
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ }
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java
index 32cfb80..11cc502 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java
@@ -41,6 +41,9 @@ public class AccountTotpPage extends Page {
@FindBy(css = "button[type=\"submit\"]")
private WebElement submitButton;
+ @FindBy(linkText = "Remove Google")
+ private WebElement removeLink;
+
public void configure(String totp) {
totpInput.sendKeys(totp);
submitButton.click();
@@ -51,11 +54,15 @@ public class AccountTotpPage extends Page {
}
public boolean isCurrent() {
- return driver.getPageSource().contains("Google Authenticator Setup");
+ return driver.getTitle().contains("Edit Account - Google Authenticator");
}
public void open() {
driver.navigate().to(PATH);
}
+ public void removeTotp() {
+ removeLink.click();
+ }
+
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
index 1d9c949..d099690 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
@@ -41,6 +41,9 @@ public class LoginPage extends Page {
@FindBy(id = "password")
private WebElement passwordInput;
+ @FindBy(id = "totp")
+ private WebElement totp;
+
@FindBy(css = "input[type=\"submit\"]")
private WebElement submitButton;
@@ -63,6 +66,19 @@ public class LoginPage extends Page {
submitButton.click();
}
+ public void loginTotp(String username, String password, String code) {
+ usernameInput.clear();
+ usernameInput.sendKeys(username);
+
+ passwordInput.clear();
+ passwordInput.sendKeys(password);
+
+ totp.clear();
+ totp.sendKeys(code);
+
+ submitButton.click();
+ }
+
public String getError() {
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/Page.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/Page.java
index cb56000..d38e2ac 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/Page.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/Page.java
@@ -35,7 +35,7 @@ public abstract class Page {
public void assertCurrent() {
String name = getClass().getSimpleName();
- Assert.assertTrue("Exptected " + name + " but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")",
+ Assert.assertTrue("Expected " + name + " but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")",
isCurrent());
}