[KEYCLOAK-12426] Add username to the login form + ability to reset login

This commit is contained in:
Martin Bartos RH 2020-01-15 12:41:42 +01:00 committed by Marek Posolda
parent 85dc1b3653
commit d3f6937a23
14 changed files with 121 additions and 50 deletions

View file

@ -191,12 +191,7 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> inputData, boolean clearUser) {
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
if (password == null || password.isEmpty()) {
context.getEvent().user(user);
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = challenge(context, getDefaultChallengeMessage(context));
context.forceChallenge(challengeResponse);
context.clearUser();
return false;
return badPasswordHandler(context, user, clearUser,true);
}
if (isTemporarilyDisabledByBruteForce(context, user)) return false;
@ -204,17 +199,26 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
if (password != null && !password.isEmpty() && context.getSession().userCredentialManager().isValid(context.getRealm(), user, UserCredentialModel.password(password))) {
return true;
} else {
return badPasswordHandler(context, user, clearUser,false);
}
}
// Set up AuthenticationFlowContext error.
private boolean badPasswordHandler(AuthenticationFlowContext context, UserModel user, boolean clearUser,boolean isEmptyPassword) {
context.getEvent().user(user);
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = challenge(context, getDefaultChallengeMessage(context));
if(isEmptyPassword) {
context.forceChallenge(challengeResponse);
}else{
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse);
}
if (clearUser) {
context.clearUser();
}
return false;
}
}
protected boolean isTemporarilyDisabledByBruteForce(AuthenticationFlowContext context, UserModel user) {
if (context.getRealm().isBruteForceProtected()) {

View file

@ -49,7 +49,11 @@ public class AuthenticationContextBean {
public boolean showUsername() {
return context != null && context.getUser() != null && context.getAuthenticationSession() != null;
return context != null && context.getUser() != null && context.getAuthenticationSession() != null && page!=LoginFormsPages.ERROR;
}
public boolean showResetCredentials() {
return showUsername() && page == LoginFormsPages.LOGIN_RESET_PASSWORD;
}

View file

@ -42,7 +42,7 @@ public abstract class LanguageComboboxAwarePage extends AbstractPage {
@FindBy(id = "try-another-way")
private WebElement tryAnotherWayLink;
@FindBy(id = "attempted-username")
@FindBy(id = "kc-attempted-username")
private WebElement attemptedUsernameLabel;
// TODO: This won't be a link, but some kind of an icon once we do better design
@ -82,7 +82,7 @@ public abstract class LanguageComboboxAwarePage extends AbstractPage {
public static void assertAttemptedUsernameAvailability(WebDriver driver, boolean expectedAvailability) {
try {
driver.findElement(By.id("attempted-username"));
driver.findElement(By.id("kc-attempted-username"));
Assert.assertTrue(expectedAvailability);
} catch (NoSuchElementException nse) {
Assert.assertFalse(expectedAvailability);

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.pages;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@ -73,7 +74,12 @@ public class LoginConfigTotpPage extends AbstractPage {
}
public boolean isCurrent() {
return PageUtils.getPageTitle(driver).equals("Mobile Authenticator Setup");
try {
driver.findElement(By.id("totp"));
return true;
} catch (Throwable t) {
return false;
}
}
public void open() {

View file

@ -40,6 +40,7 @@ public class LoginPasswordResetPage extends LanguageComboboxAwarePage {
private WebElement backToLogin;
public void changePassword(String username) {
usernameInput.clear();
usernameInput.sendKeys(username);
submitButton.click();

View file

@ -58,15 +58,13 @@ public class LoginTotpPage extends LanguageComboboxAwarePage {
}
public boolean isCurrent() {
if (driver.getTitle().startsWith("Log in to ")) {
try {
driver.findElement(By.id("otp"));
return true;
} catch (Throwable t) {
}
}
return false;
}
}
@Override
public void open() {

View file

@ -57,11 +57,6 @@ public class PasswordPage extends LanguageComboboxAwarePage {
}
public boolean isCurrent(String realm) {
// Check the title
if (!DroneUtils.getCurrentDriver().getTitle().equals("Log in to " + realm) && !DroneUtils.getCurrentDriver().getTitle().equals("Anmeldung bei " + realm)) {
return false;
}
// Check there is NO username field
try {
driver.findElement(By.id("username"));
@ -72,6 +67,7 @@ public class PasswordPage extends LanguageComboboxAwarePage {
// Check there is password field
try {
driver.findElement(By.id("kc-attempted-username"));
driver.findElement(By.id("password"));
} catch (NoSuchElementException nfe) {
return false;

View file

@ -3,7 +3,6 @@ package org.keycloak.testsuite.pages;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Assert;
import org.keycloak.testsuite.util.DroneUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
@ -35,11 +34,7 @@ public class SelectAuthenticatorPage extends LanguageComboboxAwarePage {
.stream()
.filter(webElement -> webElement.getAttribute("selected") != null)
.findFirst()
.orElseThrow(() -> {
return new AssertionError("Selected login method not found");
})
.orElseThrow(() -> new AssertionError("Selected login method not found"))
.getText();
}

View file

@ -9,10 +9,13 @@
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<#if auth?has_content && auth.showUsername()>
<input type="text" id="username" name="username" class="${properties.kcInputClass!}" autofocus value="${auth.attemptedUsername}"/>
<#else>
<input type="text" id="username" name="username" class="${properties.kcInputClass!}" autofocus/>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">

View file

@ -83,6 +83,8 @@ offlineAccessScopeConsentText=Offline Access
samlRoleListScopeConsentText=My Roles
rolesScopeConsentText=User roles
restartLoginTooltip=Restart login
loginTotpIntro=You need to set up a One Time Password generator to access this account
loginTotpStep1=Install one of the following applications on your mobile
loginTotpStep2=Open the application and scan the barcode

View file

@ -1,6 +1,6 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayInfo=true; section>
<#if section = "header">
<#if section = "header" || section = "show-username">
<script type="text/javascript">
// Fill up the two hidden and submit the form
function fillAndSubmit() {

View file

@ -52,11 +52,30 @@
</div>
</div>
</#if>
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<h1 id="kc-page-title"><#nested "header"></h1>
<#else>
<#nested "show-username">
</#if>
</header>
<div id="kc-content">
<div id="kc-content-wrapper">
<#if auth?has_content && auth.showUsername() && !auth.showResetCredentials()>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-username">
<label id="kc-attempted-username">${auth.attemptedUsername}</label>
<a id="reset-login" href="${url.loginRestartFlowUrl}">
<div class="kc-login-tooltip">
<i class="${properties.kcResetFlowIcon!}"></i>
<span class="kc-tooltip-text">${msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
</div>
<hr/>
</#if>
<#-- App-initiated actions should not see warning messages about the need to complete the action -->
<#-- during login. -->
<#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
@ -69,16 +88,6 @@
</div>
</#if>
<#if auth?has_content && auth.showUsername() >
<div class="${properties.kcFormGroupClass!}">
<label id="attempted-username">${auth.attemptedUsername}</label>
<a href="${url.loginRestartFlowUrl}" id="reset-login">Reset Login</a>
</div>
<hr />
</#if>
<#nested "form">
<#if auth?has_content && auth.showTryAnotherWayLink() >

View file

@ -116,6 +116,17 @@ div.kc-logo-text span {
width: 100%;
}
#kc-attempted-username{
font-size: 20px;
font-family:inherit;
font-weight: normal;
padding-right:10px;
}
#kc-username{
text-align: center;
}
/* #kc-content-wrapper {
overflow-y: hidden;
} */
@ -216,6 +227,47 @@ ul#kc-totp-supported-apps {
margin-top: 0;
}
.kc-login-tooltip{
position:relative;
display: inline-block;
}
.kc-login-tooltip .kc-tooltip-text{
top:-3px;
left:160%;
background-color: black;
visibility: hidden;
color: #fff;
min-width:130px;
text-align: center;
border-radius: 2px;
box-shadow:0 1px 8px rgba(0,0,0,0.6);
padding: 5px;
position: absolute;
opacity:0;
transition:opacity 0.5s;
}
/* Show tooltip */
.kc-login-tooltip:hover .kc-tooltip-text {
visibility: visible;
opacity:0.7;
}
/* Arrow for tooltip */
.kc-login-tooltip .kc-tooltip-text::after {
content: " ";
position: absolute;
top: 15px;
right: 100%;
margin-top: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent black transparent transparent;
}
.zocial,
a.zocial {
width: 100%;

View file

@ -35,6 +35,7 @@ kcFeedbackWarningIcon=pficon pficon-warning-triangle-o
kcFeedbackSuccessIcon=pficon pficon-ok
kcFeedbackInfoIcon=pficon pficon-info
kcResetFlowIcon=pficon pficon-arrow fa-2x
kcFormClass=form-horizontal
kcFormGroupClass=form-group