KEYCLOAK-12799 Missing Cancel button on The WebAuthn setup screen when using AIA

This commit is contained in:
mabartos 2020-03-03 17:02:05 +01:00 committed by Marek Posolda
parent fae333750a
commit a1bbab9eb2
8 changed files with 334 additions and 112 deletions

View file

@ -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);
}

View file

@ -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);
}
}
/**

View file

@ -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 {

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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;
} */