KEYCLOAK-12799 Missing Cancel button on The WebAuthn setup screen when using AIA
This commit is contained in:
parent
fae333750a
commit
a1bbab9eb2
8 changed files with 334 additions and 112 deletions
|
@ -41,6 +41,8 @@ import org.keycloak.credential.WebAuthnCredentialProviderFactory;
|
|||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.WebAuthnPolicy;
|
||||
|
@ -135,6 +137,8 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
|||
excludeCredentialIds = stringifyExcludeCredentialIds(webAuthnCredentialPubKeyIds);
|
||||
}
|
||||
|
||||
String isSetRetry = context.getHttpRequest().getDecodedFormParameters().getFirst(WebAuthnConstants.IS_SET_RETRY);
|
||||
|
||||
Response form = context.form()
|
||||
.setAttribute(WebAuthnConstants.CHALLENGE, challengeValue)
|
||||
.setAttribute(WebAuthnConstants.USER_ID, userId)
|
||||
|
@ -148,6 +152,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
|||
.setAttribute(WebAuthnConstants.USER_VERIFICATION_REQUIREMENT, userVerificationRequirement)
|
||||
.setAttribute(WebAuthnConstants.CREATE_TIMEOUT, createTimeout)
|
||||
.setAttribute(WebAuthnConstants.EXCLUDE_CREDENTIAL_IDS, excludeCredentialIds)
|
||||
.setAttribute(WebAuthnConstants.IS_SET_RETRY, isSetRetry)
|
||||
.createForm("webauthn-register.ftl");
|
||||
context.challenge(form);
|
||||
}
|
||||
|
|
|
@ -195,10 +195,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
attributes.put("statusCode", status.getStatusCode());
|
||||
}
|
||||
|
||||
if (authenticationSession != null && authenticationSession.getClientNote(Constants.KC_ACTION_EXECUTING) != null) {
|
||||
attributes.put("isAppInitiatedAction", true);
|
||||
}
|
||||
|
||||
switch (page) {
|
||||
case LOGIN_CONFIG_TOTP:
|
||||
attributes.put("totp", new TotpBean(session, realm, user, uriInfo.getRequestUriBuilder()));
|
||||
|
@ -447,6 +443,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
if (realm != null && user != null && session != null) {
|
||||
attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session));
|
||||
}
|
||||
|
||||
if (authenticationSession != null && authenticationSession.getClientNote(Constants.KC_ACTION_EXECUTING) != null) {
|
||||
attributes.put("isAppInitiatedAction", true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.keycloak.testsuite.pages.webauthn;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
|
@ -14,10 +15,22 @@ public class WebAuthnErrorPage extends LanguageComboboxAwarePage {
|
|||
@FindBy(id = "kc-try-again")
|
||||
private WebElement tryAgainButton;
|
||||
|
||||
// Available only with AIA
|
||||
@FindBy(id = "cancelWebAuthnAIA")
|
||||
private WebElement cancelRegistrationAIA;
|
||||
|
||||
public void clickTryAgain() {
|
||||
tryAgainButton.click();
|
||||
}
|
||||
|
||||
public void clickCancelRegistrationAIA() {
|
||||
try {
|
||||
cancelRegistrationAIA.click();
|
||||
} catch (NoSuchElementException e) {
|
||||
Assert.fail("It only works with AIA");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCurrent() {
|
||||
try {
|
||||
|
|
|
@ -20,6 +20,9 @@ package org.keycloak.testsuite.pages.webauthn;
|
|||
import org.junit.Assert;
|
||||
import org.keycloak.testsuite.pages.AbstractPage;
|
||||
import org.openqa.selenium.Alert;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
import org.openqa.selenium.support.ui.ExpectedConditions;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
|
||||
|
@ -31,6 +34,23 @@ import org.openqa.selenium.support.ui.WebDriverWait;
|
|||
*/
|
||||
public class WebAuthnRegisterPage extends AbstractPage {
|
||||
|
||||
// Available only with AIA
|
||||
@FindBy(id = "registerWebAuthnAIA")
|
||||
private WebElement registerAIAButton;
|
||||
|
||||
// Available only with AIA
|
||||
@FindBy(id = "cancelWebAuthnAIA")
|
||||
private WebElement cancelAIAButton;
|
||||
|
||||
public void confirmAIA() {
|
||||
Assert.assertTrue("It only works with AIA", isAIA());
|
||||
registerAIAButton.click();
|
||||
}
|
||||
|
||||
public void cancelAIA() {
|
||||
Assert.assertTrue("It only works with AIA", isAIA());
|
||||
cancelAIAButton.click();
|
||||
}
|
||||
|
||||
public void registerWebAuthnCredential(String authenticatorLabel) {
|
||||
// label edit after registering authenicator by .create()
|
||||
|
@ -43,8 +63,20 @@ public class WebAuthnRegisterPage extends AbstractPage {
|
|||
promptDialog.accept();
|
||||
}
|
||||
|
||||
private boolean isAIA() {
|
||||
try {
|
||||
registerAIAButton.getText();
|
||||
cancelAIAButton.getText();
|
||||
return true;
|
||||
} catch (NoSuchElementException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCurrent() {
|
||||
if (isAIA()) {
|
||||
return true;
|
||||
}
|
||||
// Cant verify the page in case that prompt is shown. Prompt is shown immediately when WebAuthnRegisterPage is displayed
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright 2019 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.webauthn;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.testsuite.WebAuthnAssume;
|
||||
import org.keycloak.testsuite.actions.AbstractAppInitiatedActionTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
||||
import org.keycloak.testsuite.pages.PasswordPage;
|
||||
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.keycloak.common.Profile.Feature.WEB_AUTHN;
|
||||
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.ALTERNATIVE;
|
||||
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
|
||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
@EnableFeature(value = WEB_AUTHN, skipRestart = true, onlyForProduct = true)
|
||||
@AuthServerContainerExclude(REMOTE)
|
||||
public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTest {
|
||||
|
||||
@Page
|
||||
LoginUsernameOnlyPage usernamePage;
|
||||
|
||||
@Page
|
||||
PasswordPage passwordPage;
|
||||
|
||||
@Page
|
||||
WebAuthnRegisterPage registerPage;
|
||||
|
||||
public AppInitiatedActionWebAuthnTest() {
|
||||
super(WebAuthnRegisterFactory.PROVIDER_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isImportAfterEachMethod() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
RequiredActionProviderRepresentation action = new RequiredActionProviderRepresentation();
|
||||
action.setAlias(WebAuthnRegisterFactory.PROVIDER_ID);
|
||||
action.setProviderId(WebAuthnRegisterFactory.PROVIDER_ID);
|
||||
action.setEnabled(true);
|
||||
action.setDefaultAction(true);
|
||||
action.setPriority(10);
|
||||
|
||||
List<RequiredActionProviderRepresentation> actions = new ArrayList<>();
|
||||
actions.add(action);
|
||||
testRealm.setRequiredActions(actions);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUpWebAuthnFlow() {
|
||||
final String newFlowAlias = "browserWebAuthnAIA";
|
||||
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
|
||||
testingClient.server("test").run(session -> {
|
||||
FlowUtil.inCurrentRealm(session)
|
||||
.selectFlow(newFlowAlias)
|
||||
.inForms(forms -> forms
|
||||
.clear()
|
||||
.addAuthenticatorExecution(REQUIRED, UsernameFormFactory.PROVIDER_ID)
|
||||
.addSubFlowExecution(REQUIRED, subFlow -> subFlow
|
||||
.addAuthenticatorExecution(ALTERNATIVE, PasswordFormFactory.PROVIDER_ID)
|
||||
.addAuthenticatorExecution(ALTERNATIVE, WebAuthnAuthenticatorFactory.PROVIDER_ID)))
|
||||
.defineAsBrowserFlow();
|
||||
});
|
||||
}
|
||||
|
||||
@Before
|
||||
public void verifyEnvironment() {
|
||||
WebAuthnAssume.assumeChrome();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelSetupWebAuthn() {
|
||||
loginUser();
|
||||
|
||||
doAIA();
|
||||
|
||||
registerPage.assertCurrent();
|
||||
registerPage.cancelAIA();
|
||||
|
||||
assertKcActionStatus("cancelled");
|
||||
}
|
||||
|
||||
private void loginUser() {
|
||||
usernamePage.open();
|
||||
usernamePage.assertCurrent();
|
||||
usernamePage.login("test-user@localhost");
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.login("password");
|
||||
|
||||
events.expectLogin()
|
||||
.detail(Details.USERNAME, "test-user@localhost")
|
||||
.assertEvent();
|
||||
}
|
||||
}
|
|
@ -37,10 +37,19 @@
|
|||
</table>
|
||||
</#if>
|
||||
|
||||
<div id="kc-error-message">
|
||||
<input tabindex="4" onclick="refreshPage()" type="button"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
||||
name="try-again" id="kc-try-again" value="${kcSanitize(msg("doTryAgain"))?no_esc}"/>
|
||||
</div>
|
||||
name="try-again" id="kc-try-again" value="${kcSanitize(msg("doTryAgain"))?no_esc}"
|
||||
/>
|
||||
|
||||
<#if isAppInitiatedAction??>
|
||||
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form" method="post">
|
||||
<button type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
||||
id="cancelWebAuthnAIA" name="cancel-aia" value="true"/>${msg("doCancel")}
|
||||
</button>
|
||||
</form>
|
||||
</#if>
|
||||
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
|
@ -16,9 +16,12 @@
|
|||
<input type="hidden" id="error" name="error"/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script type="text/javascript" src="${url.resourcesPath}/node_modules/jquery/dist/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
function registerSecurityKey() {
|
||||
// mandatory parameters
|
||||
let challenge = "${challenge}";
|
||||
let userid = "${userid}";
|
||||
|
@ -39,7 +42,7 @@
|
|||
displayName: username
|
||||
},
|
||||
pubKeyCredParams: pubKeyCredParams,
|
||||
}
|
||||
};
|
||||
|
||||
// optional parameters
|
||||
let rpId = "${rpId}";
|
||||
|
@ -105,6 +108,7 @@
|
|||
$("#register").submit();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function getPubKeyCredParams(signatureAlgorithms) {
|
||||
let pubKeyCredParams = [];
|
||||
|
@ -115,7 +119,10 @@
|
|||
let signatureAlgorithmsList = signatureAlgorithms.split(',');
|
||||
|
||||
for (let i = 0; i < signatureAlgorithmsList.length; i++) {
|
||||
pubKeyCredParams.push({type: "public-key", alg: signatureAlgorithmsList[i]});
|
||||
pubKeyCredParams.push({
|
||||
type: "public-key",
|
||||
alg: signatureAlgorithmsList[i]
|
||||
});
|
||||
}
|
||||
return pubKeyCredParams;
|
||||
}
|
||||
|
@ -127,12 +134,33 @@
|
|||
let excludeCredentialIdsList = excludeCredentialIds.split(',');
|
||||
|
||||
for (let i = 0; i < excludeCredentialIdsList.length; i++) {
|
||||
excludeCredentials.push({type: "public-key", id: base64url.decode(excludeCredentialIdsList[i], { loose: true })});
|
||||
excludeCredentials.push({
|
||||
type: "public-key",
|
||||
id: base64url.decode(excludeCredentialIdsList[i],
|
||||
{loose: true})
|
||||
});
|
||||
}
|
||||
return excludeCredentials;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
|
||||
<input type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
||||
id="registerWebAuthnAIA" value="${msg("doRegister")}" onclick="registerSecurityKey()"
|
||||
/>
|
||||
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form"
|
||||
method="post">
|
||||
<button type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
||||
id="cancelWebAuthnAIA" name="cancel-aia" value="true"/>${msg("doCancel")}
|
||||
</button>
|
||||
</form>
|
||||
<#else>
|
||||
<script>
|
||||
registerSecurityKey();
|
||||
</script>
|
||||
</#if>
|
||||
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
|
@ -127,6 +127,10 @@ div.kc-logo-text span {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
#kc-webauthn-settings-form{
|
||||
padding-top:8px;
|
||||
}
|
||||
|
||||
/* #kc-content-wrapper {
|
||||
overflow-y: hidden;
|
||||
} */
|
||||
|
|
Loading…
Reference in a new issue