added more style fixes for the login.v2 (#32708)

* added more style fixes for the login.v2

related: #32522
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed grant screen

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* test fixes

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fix for code.ftl

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* test fixes

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

---------

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-09-11 20:52:49 +02:00 committed by GitHub
parent 125124c2d9
commit 9aad6f650d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 438 additions and 158 deletions

View file

@ -12,7 +12,7 @@ public class DeleteAccountActionConfirmPage extends RequiredActions {
@FindBy(css = "button[name='cancel-aia']")
WebElement cancelActionButton;
@FindBy(css = "input[type='submit']")
@FindBy(css = "button[type='submit']")
WebElement confirmActionButton;
@Override

View file

@ -36,10 +36,10 @@ public class LoginActions extends LoginBase {
.path("login-actions");
}
@FindBy(css = "input[type='submit']")
@FindBy(css = "button[type='submit']")
private WebElement submitButton;
@FindBy(css = "button[type='submit']")
@FindBy(css = "button[name='cancel-aia']")
private WebElement cancelButton;
public void submit() {

View file

@ -34,10 +34,10 @@ import static org.keycloak.testsuite.util.UIUtils.clickLink;
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class OAuthGrant extends RequiredActions {
@FindBy(css = "input[name=\"accept\"]")
@FindBy(css = "button[name=\"accept\"]")
private WebElement acceptButton;
@FindBy(css = "input[name=\"cancel\"]")
@FindBy(css = "button[name=\"cancel\"]")
private WebElement cancelButton;
@FindBy(xpath = "//div[@id='kc-oauth']/ul/li/span")

View file

@ -37,7 +37,7 @@ public class OneTimeCode extends Authenticate {
@FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']")
private WebElement loginErrorMessage;
@FindBy(id = "input-error-otp-code")
@FindBy(id = "input-error-otp")
private WebElement totpInputCodeError;
public String getOtpLabel() {

View file

@ -40,7 +40,7 @@ public class LoginPasswordUpdatePage {
@FindBy(id = "password-confirm")
private WebElement passwordConfirmInput;
@FindBy(css = "input[type=\"submit\"]")
@FindBy(css = "button[type=\"submit\"]")
private WebElement submitButton;
@FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']")

View file

@ -32,7 +32,7 @@ public class LoginPasswordResetPage extends LanguageComboboxAwarePage {
@FindBy(id = "input-error-username")
private WebElement usernameError;
@FindBy(css = "input[type=\"submit\"]")
@FindBy(css = "button[type=\"submit\"]")
private WebElement submitButton;
@FindBy(className = "pf-v5-c-success")

View file

@ -32,7 +32,7 @@ public class LoginPasswordUpdatePage extends LogoutSessionsPage {
@FindBy(id = "password-confirm")
private WebElement passwordConfirmInput;
@FindBy(css = "input[type=\"submit\"]")
@FindBy(css = "button[type=\"submit\"]")
private WebElement submitButton;
@FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']")
@ -40,7 +40,7 @@ public class LoginPasswordUpdatePage extends LogoutSessionsPage {
@FindBy(className = "kc-feedback-text")
private WebElement feedbackMessage;
@FindBy(name = "cancel-aia")
private WebElement cancelAIAButton;
@ -50,7 +50,7 @@ public class LoginPasswordUpdatePage extends LogoutSessionsPage {
submitButton.click();
}
public void cancel() {
cancelAIAButton.click();
}

View file

@ -22,6 +22,7 @@ import java.util.stream.Collectors;
import org.junit.Assert;
import org.keycloak.common.util.Retry;
import org.keycloak.testsuite.util.UIUtils;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
@ -38,13 +39,13 @@ public class LoginTotpPage extends LanguageComboboxAwarePage {
@FindBy(id = "password-token")
private WebElement passwordToken;
@FindBy(css = "input[type=\"submit\"]")
@FindBy(css = "button[type=\"submit\"]")
private WebElement submitButton;
@FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']")
private WebElement loginErrorMessage;
@FindBy(id = "input-error-otp-code")
@FindBy(id = "input-error-otp")
private WebElement totpInputCodeError;
public void login(String totp) {
@ -88,7 +89,7 @@ public class LoginTotpPage extends LanguageComboboxAwarePage {
// If false, we don't expect that credentials combobox is available. If true, we expect that it is available on the page
public void assertOtpCredentialSelectorAvailability(boolean expectedAvailability) {
try {
driver.findElement(By.className("pf-c-tile"));
driver.findElement(By.className("pf-v5-c-tile"));
Assert.assertTrue(expectedAvailability);
} catch (NoSuchElementException nse) {
Assert.assertFalse(expectedAvailability);
@ -113,21 +114,19 @@ public class LoginTotpPage extends LanguageComboboxAwarePage {
}
private By getXPathForLookupAllCards() {
return By.xpath("//span[contains(@class, 'pf-c-tile__title')]");
return By.xpath("//span[contains(@class, 'pf-v5-c-tile__title')]");
}
private By getCssSelectorForLookupActiveCard() {
return By.cssSelector(".pf-c-tile__input:checked + .pf-c-tile .pf-c-tile__title");
return By.cssSelector(".pf-m-selected");
}
private By getXPathForLookupCardWithName(String credentialName) {
return By.xpath("//label[contains(@class, 'pf-c-tile')][normalize-space() = '"+ credentialName +"']");
return By.xpath("//div[contains(@class, 'pf-v5-c-tile')][normalize-space() = '"+ credentialName +"']");
}
public void selectOtpCredential(String credentialName) {
waitForElement(getCssSelectorForLookupActiveCard());
WebElement webElement = driver.findElement(
getXPathForLookupCardWithName(credentialName));
UIUtils.clickLink(webElement);
@ -143,4 +142,4 @@ public class LoginTotpPage extends LanguageComboboxAwarePage {
}, 10, 10);
}
}
}

View file

@ -41,9 +41,9 @@ public class OAuthGrantPage extends LanguageComboboxAwarePage {
public static final String OFFLINE_ACCESS_CONSENT_TEXT = "Offline Access";
public static final String ROLES_CONSENT_TEXT = "User roles";
@FindBy(css = "input[name=\"accept\"]")
@FindBy(css = "button[name=\"accept\"]")
private WebElement acceptButton;
@FindBy(css = "input[name=\"cancel\"]")
@FindBy(css = "button[name=\"cancel\"]")
private WebElement cancelButton;

View file

@ -1,13 +1,13 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -18,11 +18,13 @@ package org.keycloak.testsuite.util.saml;
import org.keycloak.testsuite.util.SamlClient.Step;
import org.keycloak.testsuite.util.SamlClientBuilder;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import org.apache.http.NameValuePair;
@ -37,12 +39,12 @@ import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
/**
*
* @author hmlnarik
*/
public class RequiredConsentBuilder implements Step {
@ -88,18 +90,18 @@ public class RequiredConsentBuilder implements Step {
for (Element form : theLoginPage.getElementsByTag("form")) {
String method = form.attr("method");
String action = form.attr("action");
boolean isPost = method != null && "post".equalsIgnoreCase(method);
boolean isPost = "post".equalsIgnoreCase(method);
Element submitButton;
if (approveConsent) {
submitButton = form.getElementById("kc-login");
} else {
submitButton = form.getElementById("kc-cancel");
}
parameters.add(new BasicNameValuePair(submitButton.attr("name"), submitButton.attr("value")));
for (Element input : form.getElementsByTag("input")) {
if (Objects.equals(input.id(), "kc-login")) {
if (approveConsent)
parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value")));
} else if (Objects.equals(input.id(), "kc-cancel")) {
if (!approveConsent)
parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value")));
} else {
parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
}
parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
}
if (isPost) {

View file

@ -181,6 +181,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
Assert.assertTrue(oneTimeCodePage.isOtpLabelPresent());
loginTotpPage.assertCurrent();
loginTotpPage.assertOtpCredentialSelectorAvailability(true);
loginTotpPage.selectOtpCredential("first");
// Check that selected credential is "first"
Assert.assertEquals("first", loginTotpPage.getSelectedOtpCredential());
@ -207,6 +208,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
Assert.assertTrue(oneTimeCodePage.isOtpLabelPresent());
loginTotpPage.assertCurrent();
loginTotpPage.assertOtpCredentialSelectorAvailability(true);
loginTotpPage.selectOtpCredential(orderedCredentials.get(0));
// Check that preferred credential is selected
Assert.assertEquals(orderedCredentials.get(0), loginTotpPage.getSelectedOtpCredential());
@ -252,7 +254,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
// A conditional flow without conditional authenticator should automatically be disabled
@Test
public void testFlowDisabledWhenConditionalAuthenticatorIsMissing() {
try {
configureBrowserFlowWithConditionalSubFlowHavingConditionalAuthenticator("browser - non missing conditional authenticator", true);
@ -288,7 +290,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
// A conditional flow with disabled conditional authenticator should automatically be disabled
@Test
public void testFlowDisabledWhenConditionalAuthenticatorIsDisabled() {
try {
configureBrowserFlowWithConditionalSubFlowHavingDisabledConditionalAuthenticator("browser - disabled conditional authenticator");
@ -321,7 +323,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
// Configure a conditional authenticator in a non-conditional sub-flow
// In such case, the flow is evaluated and the conditional authenticator is considered as disabled
@Test
public void testConditionalAuthenticatorInNonConditionalFlow() {
try {
configureBrowserFlowWithConditionalAuthenticatorInNonConditionalFlow();
@ -364,7 +366,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
// user-with-two-configured-otp has the "user" role and should be asked for an OTP code
// user-with-one-configured-otp does not have the role. He should not be asked for an OTP code
@Test
public void testConditionalRoleAuthenticator() {
String requiredRole = "user";
// A browser flow is configured with an OTPForm for users having the role "user"
@ -392,7 +394,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
// user-with-two-configured-otp has the "composite-realm-role-1" role and should be asked for an OTP code
// user-with-one-configured-otp does not have the role. He should not be asked for an OTP code
@Test
public void testConditionalRoleAuthenticatorWithRealmRoleIncludedInCompositeRealmRole() {
// Create composite-realm-role-1
@ -439,7 +441,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
// user-with-two-configured-otp has the "composite-client-role-1" role and should be asked for an OTP code
// user-with-one-configured-otp does not have the role. He should not be asked for an OTP code
@Test
public void testConditionalRoleAuthenticatorWithClientRoleIncludedInCompositeClientRole() {
String clientName = "test-app";
@ -521,7 +523,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
// Configure a conditional authenticator with a condition which change while the flow evaluation
// In such case, all the required authenticator inside the subflow should be evaluated even if the condition has changed
@Test
public void testConditionalAuthenticatorWithConditionalSubFlowWithChangingConditionWhileFlowEvaluation() {
try {
configureBrowserFlowWithConditionalSubFlowWithChangingConditionWhileFlowEvaluation();
@ -595,7 +597,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
}
@Test
public void testSwitchExecutionNotAllowedWithRequiredPasswordAndAlternativeOTP() {
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithRequiredPasswordFormAndAlternativeOTP(newFlowAlias);
@ -631,7 +633,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
@Test
public void testSocialProvidersPresentOnLoginUsernameOnlyPageIfConfigured() {
String testRealm = "test";
// Test setup - Configure the testing Keycloak instance with UsernameForm & PasswordForm (both REQUIRED) and OTPFormAuthenticator (ALTERNATIVE)
@ -688,7 +690,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
}
@Test
public void testConditionalFlowWithConditionalAuthenticatorEvaluatingToFalseActsAsDisabled(){
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithConditionalFlowWithOTP(newFlowAlias);
@ -707,7 +709,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
}
@Test
public void testConditionalFlowWithConditionalAuthenticatorEvaluatingToTrueActsAsRequired(){
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithConditionalFlowWithOTP(newFlowAlias);
@ -769,7 +771,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
* In this test the user is expected to have to log in with OTP
*/
@Test
public void testConditionalFlowWithMultipleConditionalAuthenticatorsWithUserWithRoleAndOTP() {
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithConditionalFlowWithMultipleConditionalAuthenticators(newFlowAlias);
@ -800,7 +802,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
* In this test, the user is expected to have to login with username and password only, as the conditional branch evaluates to false, and is therefore DISABLED
*/
@Test
public void testConditionalFlowWithMultipleConditionalAuthenticatorsWithUserWithRoleButNotOTP() {
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithConditionalFlowWithMultipleConditionalAuthenticators(newFlowAlias);
@ -866,7 +868,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
* and will instead raise an credential setup required error.
*/
@Test
public void testLoginWithWithNoOTPCredentialAndNoRequiredActionProviderRegistered(){
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithRequiredOTP(newFlowAlias);
@ -893,7 +895,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
* and will instead raise an credential setup required error.
*/
@Test
public void testLoginWithWithNoOTPCredentialAndRequiredActionProviderDisabled(){
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithRequiredOTP(newFlowAlias);
@ -918,7 +920,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
* has its requiredActionProvider enabled, than it will login and show the otpSetup page.
*/
@Test
public void testLoginWithWithNoOTPCredential(){
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithRequiredOTP(newFlowAlias);;
@ -961,7 +963,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
* and will instead raise an credential setup required error.
*/
@Test
public void testLoginWithWithNoWebAuthnCredentialAndRequiredActionProviderDisabled(){
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithRequiredWebAuthn(newFlowAlias);
@ -984,7 +986,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
* has its requiredActionProvider enabled, then it will login and show the WebAuthn registration page.
*/
@Test
public void testLoginWithWithNoWebAuthnCredential(){
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithRequiredWebAuthn(newFlowAlias);
@ -1027,7 +1029,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
* then the selection mechanism will see that there's no viable alternative, and move on to the next execution (in this case the flow)
*/
@Test
public void testLoginWithWithNoOTPCredentialAndAlternativeActionProvider(){
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithAlternativeOTPAndPassword(newFlowAlias);
@ -1073,7 +1075,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
* This test checks the error messages, when the credentials are invalid and UsernameForm and PasswordForm are separated.
*/
@Test
public void testLoginMultiFactorWithWrongCredentialsMessage() {
UserRepresentation user = testRealm().users().search("test-user@localhost").get(0);
Assert.assertNotNull(user);
@ -1155,7 +1157,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
* then it will not try to create the required action, and will instead move to the next alternative
*/
@Test
public void testLoginWithWithNoWebAuthnCredentialAndAlternativeActionProvider(){
String newFlowAlias = "browser - copy 1";
configureBrowserFlowWithAlternativeWebAuthnAndPassword(newFlowAlias);
@ -1182,7 +1184,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
* After login with password and fulfill the conditional subflow2, the subflow1 should be considered successful as well and the OTP authentication should not be needed
*/
@Test
public void testLoginWithAlternativeOTPAndConditionalPassword(){
String newFlowAlias = "browser - copy 2";
configureBrowserFlowWithAlternativeOTPAndConditionalPassword(newFlowAlias);

View file

@ -1370,7 +1370,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
newPassword.sendKeys("resetPassword");
final WebElement confirmPassword = driver.findElement(By.id("password-confirm"));
confirmPassword.sendKeys("resetPassword");
final WebElement submit = driver.findElement(By.cssSelector("input[type=\"submit\"]"));
final WebElement submit = driver.findElement(By.cssSelector("button[type=\"submit\"]"));
submit.click();
}

View file

@ -3,12 +3,17 @@
<div class="${properties.kcFormActionGroupClass}">
<#nested>
</div>
</div>
</#macro>
<#macro button id name label class=["kcButtonPrimaryClass"]>
<#macro button label id="" name="" class=["kcButtonPrimaryClass"]>
<button class="<#list class as c>${properties[c]} </#list>" name="${name}" id="${id}" type="submit">${msg(label)}</button>
</#macro>
<#macro buttonLink href label id="" class=["kcButtonSecondaryClass"]>
<a id="${id}" href="${href}" class="<#list class as c>${properties[c]} </#list>">${kcSanitize(msg(label))?no_esc}</a>
</#macro>
<#macro loginButton>
<@buttons.actionGroup>
<@buttons.button id="kc-login" name="login" label="doLogIn" class=["kcButtonPrimaryClass", "kcButtonBlockClass"] />

View file

@ -0,0 +1,20 @@
<#import "template.ftl" as layout>
<#import "field.ftl" as field>
<@layout.registrationLayout; section>
<#if section = "header">
<#if code.success>
${msg("codeSuccessTitle")}
<#else>
${kcSanitize(msg("codeErrorTitle", code.error))}
</#if>
<#elseif section = "form">
<div id="kc-code">
<#if code.success>
<p>${msg("copyCodeInstruction")}</p>
<@field.input name="code" label="" value=code.code />
<#else>
<p id="error">${kcSanitize(code.error)}</p>
</#if>
</div>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,40 @@
<#import "template.ftl" as layout>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout; section>
<!-- template: delete-account-confirm.ftl -->
<#if section = "header">
${msg("deleteAccountConfirm")}
<#elseif section = "form">
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-deleteaccount-form" method="post">
<div class="${properties.kcAlertClass!} pf-m-warning">
<div class="${properties.kcAlertIconClass!}">
<i class="${properties.kcFeedbackWarningIcon!}" aria-hidden="true"></i>
</div>
<span class="${properties.kcAlertTitleClass!}">
${msg("irreversibleAction")}
</span>
</div>
<p>${msg("deletingImplies")}</p>
<ul class="pf-v5-c-list" role="list">
<li>${msg("loggingOutImmediately")}</li>
<li>${msg("errasingData")}</li>
</ul>
<p class="delete-account-text">${msg("finalDeletionConfirmation")}</p>
<@buttons.actionGroup>
<@buttons.button label="doConfirmDelete" class=["kcButtonPrimaryClass"]/>
<#if triggered_from_aia>
<@buttons.button name="cancel-aia" label="doCancel" class=["kcButtonSecondaryClass"]/>
</#if>
</@buttons.actionGroup>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,21 @@
<#import "template.ftl" as layout>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout displayMessage=false; section>
<!-- template: delete-credential.ftl -->
<#if section = "header">
${msg("deleteCredentialTitle", credentialLabel)}
<#elseif section = "form">
<div id="kc-delete-text" class="${properties.kcContentWrapperClass!}">
${msg("deleteCredentialMessage", credentialLabel)}
</div>
<form class="${properties.kcFormClass!}" action="${url.loginAction}" method="POST">
<@buttons.actionGroup>
<@buttons.button name="accept" id="kc-accept" label="doConfirmDelete" class=["kcButtonPrimaryClass"]/>
<@buttons.button name="cancel-aia" id="kc-decline" label="doDecline" class=["kcButtonSecondaryClass"]/>
</@buttons.actionGroup>
</form
</#if>
</@layout.registrationLayout>

View file

@ -41,19 +41,19 @@
</#if>
</#macro>
<#macro input name label value="" required=false>
<#assign error=kcSanitize(messagesPerField.get(name))?no_esc>
<#macro input name label value="" required=false autocomplete="off" fieldName=name>
<#assign error=kcSanitize(messagesPerField.get(fieldName))?no_esc>
<@field.group name=name label=label error=error required=required>
<span class="${properties.kcInputClass} <#if error?has_content>${properties.kcError}</#if>">
<input id="${name}" name="${name}" value="${value}" type="text" autocomplete="off"
<input id="${name}" name="${name}" value="${value}" type="text" autocomplete="${autocomplete}"
aria-invalid="<#if error?has_content>true</#if>"/>
<@errorIcon error=error/>
</span>
</@field.group>
</#macro>
<#macro password name label value="" required=false forgotPassword=false>
<#assign error=kcSanitize(messagesPerField.get(name))?no_esc>
<#macro password name label value="" required=false forgotPassword=false fieldName=name>
<#assign error=kcSanitize(messagesPerField.get(fieldName))?no_esc>
<@field.group name=name label=label error=error required=required>
<div class="${properties.kcInputGroup}">
<div class="${properties.kcInputGroupItemClass} ${properties.kcFill}">

View file

@ -0,0 +1,59 @@
<#import "template.ftl" as layout>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout bodyClass="oauth"; section>
<#if section = "header">
<#if client.attributes.logoUri??>
<img src="${client.attributes.logoUri}"/>
</#if>
<p>
<#if client.name?has_content>
${msg("oauthGrantTitle",advancedMsg(client.name))}
<#else>
${msg("oauthGrantTitle",client.clientId)}
</#if>
</p>
<#elseif section = "form">
<div id="kc-oauth" class="content-area">
<h3>${msg("oauthGrantRequest")}</h3>
<ul class="${properties.kcListClass!}">
<#if oauth.clientScopesRequested??>
<#list oauth.clientScopesRequested as clientScope>
<li>
<span><#if !clientScope.dynamicScopeParameter??>
${advancedMsg(clientScope.consentScreenText)}
<#else>
${advancedMsg(clientScope.consentScreenText)}: <b>${clientScope.dynamicScopeParameter}</b>
</#if>
</span>
</li>
</#list>
</#if>
</ul>
<#if client.attributes.policyUri?? || client.attributes.tosUri??>
<h3>
<#if client.name?has_content>
${msg("oauthGrantInformation",advancedMsg(client.name))}
<#else>
${msg("oauthGrantInformation",client.clientId)}
</#if>
<#if client.attributes.tosUri??>
${msg("oauthGrantReview")}
<a href="${client.attributes.tosUri}" target="_blank">${msg("oauthGrantTos")}</a>
</#if>
<#if client.attributes.policyUri??>
${msg("oauthGrantReview")}
<a href="${client.attributes.policyUri}" target="_blank">${msg("oauthGrantPolicy")}</a>
</#if>
</h3>
</#if>
<form class="${properties.kcFormClass} ${properties.kcMarginTopClass!}" action="${url.oauthAction}" method="POST">
<input type="hidden" name="code" value="${oauth.code}">
<@buttons.actionGroup>
<@buttons.button id="kc-login" name="accept" label="doYes"/>
<@buttons.button id="kc-cancel" name="cancel" label="doNo" class=["kcButtonSecondaryClass"]/>
</@buttons.actionGroup>
</form>
</div>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,45 @@
<#import "template.ftl" as layout>
<#import "field.ftl" as field>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('totp'); section>
<!-- template: login-otp.ftl -->
<#if section="header">
${msg("doLogIn")}
<#elseif section="form">
<form id="kc-otp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<input id="selectedCredentialId" type="hidden" name="selectedCredentialId" value="${otpLogin.selectedCredentialId!''}">
<#if otpLogin.userOtpCredentials?size gt 1>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcInputWrapperClass!}">
<#list otpLogin.userOtpCredentials as otpCredential>
<div id="kc-otp-credential-${otpCredential?index}" class="${properties.kcLoginOTPListClass!}"
onclick="toggleOTP(${otpCredential?index}, '${otpCredential.id}')">
<span class="${properties.kcLoginOTPListItemHeaderClass!}">
<span class="${properties.kcLoginOTPListItemIconBodyClass!}">
<i class="${properties.kcLoginOTPListItemIconClass!}" aria-hidden="true"></i>
</span>
<span class="${properties.kcLoginOTPListItemTitleClass!}">${otpCredential.userLabel}</span>
</span>
</div>
</#list>
</div>
</div>
</#if>
<@field.input name="otp" label=msg("loginOtpOneTime") autocomplete="one-time-code" fieldName="totp" />
<@buttons.loginButton />
</form>
<script>
function toggleOTP(index, value) {
// select the clicked OTP credential
document.getElementById("selectedCredentialId").value = value;
// remove selected class from all OTP credentials
Array.from(document.getElementsByClassName("${properties.kcLoginOTPListSelectedClass!}")).map(i => i.classList.remove("${properties.kcLoginOTPListSelectedClass!}"));
// add selected class to the clicked OTP credential
document.getElementById("kc-otp-credential-" + index).classList.add("${properties.kcLoginOTPListSelectedClass!}");
}
</script>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,27 @@
<#import "template.ftl" as layout>
<#import "field.ftl" as field>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('username'); section>
<#if section = "header">
${msg("emailForgotTitle")}
<#elseif section = "form">
<form id="kc-reset-password-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<#assign label>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</#assign>
<@field.input name="username" label=label value=auth.attemptedUsername!''/>
<@buttons.actionGroup>
<@buttons.button id="kc-form-buttons" label="doSubmit" class=["kcButtonPrimaryClass", "kcButtonBlockClass"]/>
<@buttons.buttonLink href=url.loginUrl label="backToLogin" class=["kcButtonSecondaryClass", "kcButtonBlockClass"]/>
</@buttons.actionGroup>
</form>
<#elseif section = "info" >
<#if realm.duplicateEmailsAllowed>
${msg("emailInstructionUsername")}
<#else>
${msg("emailInstruction")}
</#if>
</#if>
</@layout.registrationLayout>

View file

@ -1,79 +1,28 @@
<#import "template.ftl" as layout>
<#import "password-commons.ftl" as passwordCommons>
<#import "field.ftl" as field>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('password','password-confirm'); section>
<!-- template: login-update-password.ftl -->
<#if section = "header">
${msg("updatePasswordTitle")}
<#elseif section = "form">
<form id="kc-passwd-update-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post" novalidate="novalidate">
<div class="${properties.kcFormGroupClass!}">
<label for="password-new" class="${properties.kcLabelClass!}">
<span class="pf-v5-c-form__label-text">
${msg("passwordNew")}
</span>
</label>
<div class="${properties.kcInputGroup!}">
<span class="${properties.kcInputClass!}" dir="ltr">
<input type="password" id="password-new" name="password-new" autofocus autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>"
/>
</span>
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password-new" data-password-toggle
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div>
<#if messagesPerField.existsError('password')>
<span id="input-error-password" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password'))?no_esc}
</span>
</#if>
</div>
<div class="${properties.kcFormGroupClass!}">
<label for="password-confirm" class="${properties.kcLabelClass!}">
<span class="pf-v5-c-form__label-text">
${msg("passwordConfirm")}
</span>
</label>
<div class="${properties.kcInputGroup!}">
<span class="${properties.kcInputClass!}" dir="ltr">
<input type="password" id="password-confirm" name="password-confirm"
autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>"
/>
</span>
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password-confirm" data-password-toggle
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div>
<#if messagesPerField.existsError('password-confirm')>
<span id="input-error-password-confirm" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}
</span>
</#if>
</div>
<@field.password name="password-new" label=msg("passwordNew") fieldName="password" />
<@field.password name="password-confirm" label=msg("passwordConfirm") />
<div class="${properties.kcFormGroupClass!}">
<@passwordCommons.logoutOtherSessions/>
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!} pf-m-action">
<div class="pf-v5-c-form__actions">
<#if isAppInitiatedAction??>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" />${msg("doCancel")}</button>
<#else>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
</#if>
</div>
</div>
<@buttons.actionGroup>
<#if isAppInitiatedAction??>
<@buttons.button label="doSubmit" class=["kcButtonPrimaryClass"]/>
<@buttons.button label="doCancel" name="cancel-aia" class=["kcButtonSecondaryClass"]/>
<#else>
<@buttons.button label="doSubmit" class=["kcButtonPrimaryClass", "kcButtonBlockClass"]/>
</#if>
</@buttons.actionGroup>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -17,7 +17,7 @@
<#assign label>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</#assign>
<@field.input name="username" label=label value="${(login.username!'')}" />
<@field.input name="username" label=label value=login.username!'' />
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">

View file

@ -15,7 +15,7 @@
<#assign label>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</#assign>
<@field.input name="username" label=label value="${(login.username!'')}" />
<@field.input name="username" label=label value=login.username!'' />
</#if>
<@field.password name="password" label=msg("password") forgotPassword=realm.resetPasswordAllowed/>
@ -42,7 +42,7 @@
</div>
</#if>
<#elseif section = "socialProviders" >
<#if realm.password && social?? && social.providers?has_content>
<#if realm.password && social.providers?? && social.providers?has_content>
<div class="pf-v5-c-login__main-footer-band">
<p class="pf-v5-c-login__main-footer-band-item">
${msg("identity-provider-login-label")}

View file

@ -173,7 +173,7 @@
<#if message.type = 'error'><span class="${properties.kcFeedbackErrorIcon!}"></span></#if>
<#if message.type = 'info'><span class="${properties.kcFeedbackInfoIcon!}"></span></#if>
</div>
<span class="${properties.kcAlertTitleClass!} kc-feedback-text">${kcSanitize(message.summary)?no_esc}</span>
<span class="${properties.kcAlertTitleClass!} kc-feedback-text">${kcSanitize(message.summary)?no_esc}</span>
</div>
</#if>
@ -181,11 +181,11 @@
<#if auth?has_content && auth.showTryAnotherWayLink()>
<form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post" novalidate="novalidate">
<div class="${properties.kcFormGroupClass!}">
<input type="hidden" name="tryAnotherWay" value="on"/>
<a href="#" id="try-another-way"
onclick="document.forms['kc-select-try-another-way-form'].submit();return false;">${msg("doTryAnotherWay")}</a>
</div>
<input type="hidden" name="tryAnotherWay" value="on"/>
<a id="try-another-way" href="javascript:document.forms['kc-select-try-another-way-form'].submit()"
class="${properties.kcButtonSecondaryClass} ${properties.kcButtonBlockClass} ${properties.kcMarginTopClass}">
${kcSanitize(msg("doTryAnotherWay"))?no_esc}
</a>
</form>
</#if>

View file

@ -0,0 +1,21 @@
<#import "template.ftl" as layout>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout displayMessage=false; section>
<!-- template: terms.ftl -->
<#if section = "header">
${msg("termsTitle")}
<#elseif section = "form">
<div class="${properties.kcContentWrapperClass}">
${kcSanitize(msg("termsText"))?no_esc}
</div>
<form class="${properties.kcFormClass!}" action="${url.loginAction}" method="POST">
<@buttons.actionGroup>
<@buttons.button name="accept" id="kc-accept" label="doAccept" class=["kcButtonPrimaryClass"]/>
<@buttons.button name="cancel" id="kc-decline" label="doDecline" class=["kcButtonSecondaryClass"]/>
</@buttons.actionGroup>
</form>
<div class="clearfix"></div>
</#if>
</@layout.registrationLayout>

View file

@ -44,6 +44,7 @@ kcListClass=pf-v5-c-list
kcButtonClass=pf-v5-c-button
kcButtonPrimaryClass=pf-v5-c-button pf-m-primary
kcButtonSecondaryClass=pf-v5-c-button pf-m-secondary
kcButtonBlockClass=pf-m-block
kcButtonLinkClass=pf-v5-c-button pf-m-link
kcCommonLogoIdP=pf-v5-c-login__main-footer-links-item
@ -79,3 +80,11 @@ kcLoginMainBody=pf-v5-c-login__main-body
kcContentWrapperClass=pf-v5-u-mb-md-on-md
kcWebAuthnDefaultIcon=pf-v5-c-icon pf-m-lg
kcMarginTopClass=pf-v5-u-mt-md-on-md
kcLoginOTPListClass=pf-v5-c-tile
kcLoginOTPListItemHeaderClass=pf-v5-c-tile__header pf-m-stacked
kcLoginOTPListItemIconBodyClass=pf-v5-c-tile__icon
kcLoginOTPListItemIconClass=fa fa-mobile
kcLoginOTPListItemTitleClass=pf-v5-c-tile__title
kcLoginOTPListSelectedClass=pf-m-selected

View file

@ -127,28 +127,48 @@
</#macro>
<#macro selectTag attribute>
<select id="${attribute.name}" name="${attribute.name}" class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
<#if attribute.readOnly>disabled</#if>
<#if attribute.annotations.inputType=='multiselect'>multiple</#if>
<#if attribute.annotations.inputTypeSize??>size="${attribute.annotations.inputTypeSize}"</#if>
>
<#if attribute.annotations.inputType=='select'>
<option value=""></option>
</#if>
<div class="${properties.kcInputClass!}">
<select id="${attribute.name}" name="${attribute.name}"
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
<#if attribute.readOnly>disabled</#if>
<#if attribute.annotations.inputType=='multiselect'>multiple</#if>
<#if attribute.annotations.inputTypeSize??>size="${attribute.annotations.inputTypeSize}"</#if>
>
<#if attribute.annotations.inputType=='select'>
<option value=""></option>
</#if>
<#if attribute.annotations.inputOptionsFromValidation?? && attribute.validators[attribute.annotations.inputOptionsFromValidation]?? && attribute.validators[attribute.annotations.inputOptionsFromValidation].options??>
<#assign options=attribute.validators[attribute.annotations.inputOptionsFromValidation].options>
<#elseif attribute.validators.options?? && attribute.validators.options.options??>
<#assign options=attribute.validators.options.options>
<#else>
<#assign options=[]>
</#if>
<#if attribute.annotations.inputOptionsFromValidation?? && attribute.validators[attribute.annotations.inputOptionsFromValidation]?? && attribute.validators[attribute.annotations.inputOptionsFromValidation].options??>
<#assign options=attribute.validators[attribute.annotations.inputOptionsFromValidation].options>
<#elseif attribute.validators.options?? && attribute.validators.options.options??>
<#assign options=attribute.validators.options.options>
<#else>
<#assign options=[]>
</#if>
<#list options as option>
<option value="${option}" <#if attribute.values?seq_contains(option)>selected</#if>><@selectOptionLabelText attribute=attribute option=option/></option>
</#list>
</select>
<#list options as option>
<option value="${option}" <#if attribute.values?seq_contains(option)>selected</#if>><@selectOptionLabelText attribute=attribute option=option/></option>
</#list>
</select>
<span class="${properties.kcFormControlUtilClass}">
<span class="${properties.kcFormControlToggleIcon!}">
<svg
class="pf-v5-svg"
viewBox="0 0 320 512"
fill="currentColor"
aria-hidden="true"
role="img"
width="1em"
height="1em"
>
<path
d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"
>
</path>
</svg>
</span>
</span>
</div>
</#macro>
<#macro inputTagSelects attribute>

View file

@ -0,0 +1,61 @@
<#import "template.ftl" as layout>
<#import "password-commons.ftl" as passwordCommons>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout; section>
<#if section = "title">
title
<#elseif section = "header">
<span class="${properties.kcWebAuthnKeyIcon!}"></span>
${kcSanitize(msg("webauthn-registration-title"))?no_esc}
<#elseif section = "form">
<form id="register" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
<input type="hidden" id="attestationObject" name="attestationObject"/>
<input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId"/>
<input type="hidden" id="authenticatorLabel" name="authenticatorLabel"/>
<input type="hidden" id="transports" name="transports"/>
<input type="hidden" id="error" name="error"/>
<@passwordCommons.logoutOtherSessions/>
</div>
</form>
<script type="module">
import { registerByWebAuthn } from "${url.resourcesPath}/js/webauthnRegister.js";
const registerButton = document.getElementById('registerWebAuthn');
registerButton.addEventListener("click", function() {
const input = {
challenge : '${challenge}',
userid : '${userid}',
username : '${username}',
signatureAlgorithms : [<#list signatureAlgorithms as sigAlg>${sigAlg?c},</#list>],
rpEntityName : '${rpEntityName}',
rpId : '${rpId}',
attestationConveyancePreference : '${attestationConveyancePreference}',
authenticatorAttachment : '${authenticatorAttachment}',
requireResidentKey : '${requireResidentKey}',
userVerificationRequirement : '${userVerificationRequirement}',
createTimeout : ${createTimeout},
excludeCredentialIds : '${excludeCredentialIds}',
initLabel : "${msg("webauthn-registration-init-label")?no_esc}",
initLabelPrompt : "${msg("webauthn-registration-init-label-prompt")?no_esc}",
errmsg : "${msg("webauthn-unsupported-browser-text")?no_esc}"
};
registerByWebAuthn(input);
});
</script>
<div class="pf-v5-u-py-lg ${properties.kcFormClass!}">
<@buttons.actionGroup>
<@buttons.button id="registerWebAuthn" label="doRegisterSecurityKey" />
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
<form action="${url.loginAction}" id="kc-webauthn-settings-form" method="post">
<@buttons.button id="cancelWebAuthnAIA" name="cancel-aia" label="doCancel" class=["kcButtonSecondaryClass"]/>
</form>
</#if>
</@buttons.actionGroup>
</div>
</#if>
</@layout.registrationLayout>