Merge pull request #67 from vrockai/KEYCLOAK-84
KEYCLOAK-84 form for managing totp authentificators
This commit is contained in:
commit
5728a00cd4
14 changed files with 142 additions and 21 deletions
|
@ -120,6 +120,10 @@ public class UrlBean {
|
||||||
return Urls.accountTotpPage(baseURI, realm.getId()).toString();
|
return Urls.accountTotpPage(baseURI, realm.getId()).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTotpRemoveUrl() {
|
||||||
|
return Urls.accountTotpRemove(baseURI, realm.getId()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
public String getEmailVerificationUrl() {
|
public String getEmailVerificationUrl() {
|
||||||
return Urls.accountEmailVerification(baseURI, realm.getId()).toString();
|
return Urls.accountEmailVerification(baseURI, realm.getId()).toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,10 @@ public class FormServiceImpl implements FormService {
|
||||||
|
|
||||||
Map<String, Object> attributes = new HashMap<String, Object>();
|
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());
|
RealmBean realm = new RealmBean(dataBean.getRealm());
|
||||||
attributes.put("template", new TemplateBean(realm, dataBean.getContextPath()));
|
attributes.put("template", new TemplateBean(realm, dataBean.getContextPath()));
|
||||||
|
|
||||||
|
@ -145,10 +149,6 @@ public class FormServiceImpl implements FormService {
|
||||||
|
|
||||||
private class CommandPassword implements Command {
|
private class CommandPassword implements Command {
|
||||||
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
|
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());
|
RealmBean realm = new RealmBean(dataBean.getRealm());
|
||||||
|
|
||||||
attributes.put("realm", realm);
|
attributes.put("realm", realm);
|
||||||
|
|
|
@ -60,6 +60,7 @@ body {
|
||||||
}
|
}
|
||||||
.header.rcue .navbar {
|
.header.rcue .navbar {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
z-index:auto;
|
||||||
}
|
}
|
||||||
.header.rcue .navbar.primary {
|
.header.rcue .navbar.primary {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
<!-- <p>Forgot <a href="#">Username</a> or <a href="#">Password</a>?</p> -->
|
<!-- <p>Forgot <a href="#">Username</a> or <a href="#">Password</a>?</p> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="submit" value="Log In" />
|
<input class="btn-primary" type="submit" value="Log In" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<#elseif section = "info">
|
<#elseif section = "info">
|
||||||
|
|
|
@ -35,7 +35,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<#if error?has_content>
|
<#if error?has_content>
|
||||||
<div class="feedback error bottom-left show">
|
<div class="
|
||||||
|
error bottom-left show">
|
||||||
<p>
|
<p>
|
||||||
<strong id="loginError">${rb.getString(error.summary)}</strong>
|
<strong id="loginError">${rb.getString(error.summary)}</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Edit Account</title>
|
<title>Edit Account - <#nested "title"></title>
|
||||||
<link rel="icon" href="img/favicon.ico">
|
<link rel="icon" href="img/favicon.ico">
|
||||||
|
|
||||||
<!-- Frameworks -->
|
<!-- Frameworks -->
|
||||||
|
@ -34,10 +34,14 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="admin-console user ${bodyClass}">
|
<body class="admin-console user ${bodyClass}">
|
||||||
|
|
||||||
<#if error?has_content>
|
<#if message?has_content>
|
||||||
<!--div class="feedback success show"><p><strong>Success!</strong> Your changes have been saved.</p></div-->
|
|
||||||
<div class="feedback-aligner">
|
<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>
|
</div>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<#import "template-main.ftl" as layout>
|
<#import "template-main.ftl" as layout>
|
||||||
<@layout.mainLayout active='totp' bodyClass='totp'; section>
|
<@layout.mainLayout active='totp' bodyClass='totp'; section>
|
||||||
|
|
||||||
<#if section = "header">
|
<#if section = "title">
|
||||||
|
Google Authenticator
|
||||||
|
<#elseif section = "header">
|
||||||
|
|
||||||
<#if totp.enabled>
|
<#if totp.enabled>
|
||||||
<h2>Authenticators</h2>
|
<h2>Authenticators</h2>
|
||||||
|
@ -12,7 +14,6 @@
|
||||||
<#elseif section = "content">
|
<#elseif section = "content">
|
||||||
|
|
||||||
<#if totp.enabled>
|
<#if totp.enabled>
|
||||||
<#-- TODO this is only mock page -->
|
|
||||||
<form>
|
<form>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<p class="info">You have the following authenticators set up:</p>
|
<p class="info">You have the following authenticators set up:</p>
|
||||||
|
@ -21,11 +22,16 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="provider"><span class="social googleplus">Google</span></td>
|
<td class="provider"><span class="social googleplus">Google</span></td>
|
||||||
<td class="soft">Connected as john@google.com</td>
|
<td class="action">
|
||||||
<td class="action"><a href="user-totp-setup.html" class="button">Remove Google</a></td>
|
<a href="${url.totpRemoveUrl}" class="button">Remove Google</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,9 @@ invalidPasswordExisting=Invalid existing password
|
||||||
invalidPasswordConfirm=Password confirmation doesn't match
|
invalidPasswordConfirm=Password confirmation doesn't match
|
||||||
invalidTotp=Invalid authenticator code
|
invalidTotp=Invalid authenticator code
|
||||||
|
|
||||||
|
successTotp=Google authenticator configured.
|
||||||
|
successTotpRemoved=Google authenticator removed.
|
||||||
|
|
||||||
usernameExists=Username already exists
|
usernameExists=Username already exists
|
||||||
|
|
||||||
error=A system error has occured, contact admin
|
error=A system error has occured, contact admin
|
||||||
|
|
|
@ -164,6 +164,15 @@ public class AccountService {
|
||||||
return accessCodeEntry;
|
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")
|
@Path("totp")
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
@ -206,7 +215,8 @@ public class AccountService {
|
||||||
if (accessCodeEntry != null) {
|
if (accessCodeEntry != null) {
|
||||||
return redirectOauth(user, accessCodeEntry);
|
return redirectOauth(user, accessCodeEntry);
|
||||||
} else {
|
} 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,10 @@ public class Urls {
|
||||||
return accountBase(baseUri).path(AccountService.class, "totpPage").build(realmId);
|
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) {
|
public static URI accountEmailVerification(URI baseUri, String realmId) {
|
||||||
return accountBase(baseUri).path(AccountService.class, "emailVerification").build(realmId);
|
return accountBase(baseUri).path(AccountService.class, "emailVerification").build(realmId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserModel.RequiredAction;
|
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;
|
||||||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||||
import org.keycloak.testsuite.pages.LoginConfigTotpPage;
|
import org.keycloak.testsuite.pages.LoginConfigTotpPage;
|
||||||
|
@ -54,9 +56,6 @@ public class RequiredActionTotpSetupTest {
|
||||||
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
|
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
|
||||||
appRealm.addRequiredCredential(CredentialRepresentation.TOTP);
|
appRealm.addRequiredCredential(CredentialRepresentation.TOTP);
|
||||||
appRealm.setResetPasswordAllowed(true);
|
appRealm.setResetPasswordAllowed(true);
|
||||||
|
|
||||||
UserModel user = appRealm.getUser("test-user@localhost");
|
|
||||||
user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -76,6 +75,12 @@ public class RequiredActionTotpSetupTest {
|
||||||
@WebResource
|
@WebResource
|
||||||
protected LoginConfigTotpPage totpPage;
|
protected LoginConfigTotpPage totpPage;
|
||||||
|
|
||||||
|
@WebResource
|
||||||
|
protected AccountTotpPage accountTotpPage;
|
||||||
|
|
||||||
|
@WebResource
|
||||||
|
protected OAuthClient oauth;
|
||||||
|
|
||||||
@WebResource
|
@WebResource
|
||||||
protected RegisterPage registerPage;
|
protected RegisterPage registerPage;
|
||||||
|
|
||||||
|
@ -101,9 +106,69 @@ public class RequiredActionTotpSetupTest {
|
||||||
|
|
||||||
totpPage.assertCurrent();
|
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());
|
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());
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,9 @@ public class AccountTotpPage extends Page {
|
||||||
@FindBy(css = "button[type=\"submit\"]")
|
@FindBy(css = "button[type=\"submit\"]")
|
||||||
private WebElement submitButton;
|
private WebElement submitButton;
|
||||||
|
|
||||||
|
@FindBy(linkText = "Remove Google")
|
||||||
|
private WebElement removeLink;
|
||||||
|
|
||||||
public void configure(String totp) {
|
public void configure(String totp) {
|
||||||
totpInput.sendKeys(totp);
|
totpInput.sendKeys(totp);
|
||||||
submitButton.click();
|
submitButton.click();
|
||||||
|
@ -51,11 +54,15 @@ public class AccountTotpPage extends Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCurrent() {
|
public boolean isCurrent() {
|
||||||
return driver.getPageSource().contains("Google Authenticator Setup");
|
return driver.getTitle().contains("Edit Account - Google Authenticator");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void open() {
|
public void open() {
|
||||||
driver.navigate().to(PATH);
|
driver.navigate().to(PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeTotp() {
|
||||||
|
removeLink.click();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,9 @@ public class LoginPage extends Page {
|
||||||
@FindBy(id = "password")
|
@FindBy(id = "password")
|
||||||
private WebElement passwordInput;
|
private WebElement passwordInput;
|
||||||
|
|
||||||
|
@FindBy(id = "totp")
|
||||||
|
private WebElement totp;
|
||||||
|
|
||||||
@FindBy(css = "input[type=\"submit\"]")
|
@FindBy(css = "input[type=\"submit\"]")
|
||||||
private WebElement submitButton;
|
private WebElement submitButton;
|
||||||
|
|
||||||
|
@ -63,6 +66,19 @@ public class LoginPage extends Page {
|
||||||
submitButton.click();
|
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() {
|
public String getError() {
|
||||||
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
|
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ public abstract class Page {
|
||||||
|
|
||||||
public void assertCurrent() {
|
public void assertCurrent() {
|
||||||
String name = getClass().getSimpleName();
|
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());
|
isCurrent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue