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.crypto.Algorithm;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; 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.KeycloakSession;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.WebAuthnPolicy; import org.keycloak.models.WebAuthnPolicy;
@ -135,6 +137,8 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
excludeCredentialIds = stringifyExcludeCredentialIds(webAuthnCredentialPubKeyIds); excludeCredentialIds = stringifyExcludeCredentialIds(webAuthnCredentialPubKeyIds);
} }
String isSetRetry = context.getHttpRequest().getDecodedFormParameters().getFirst(WebAuthnConstants.IS_SET_RETRY);
Response form = context.form() Response form = context.form()
.setAttribute(WebAuthnConstants.CHALLENGE, challengeValue) .setAttribute(WebAuthnConstants.CHALLENGE, challengeValue)
.setAttribute(WebAuthnConstants.USER_ID, userId) .setAttribute(WebAuthnConstants.USER_ID, userId)
@ -148,6 +152,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
.setAttribute(WebAuthnConstants.USER_VERIFICATION_REQUIREMENT, userVerificationRequirement) .setAttribute(WebAuthnConstants.USER_VERIFICATION_REQUIREMENT, userVerificationRequirement)
.setAttribute(WebAuthnConstants.CREATE_TIMEOUT, createTimeout) .setAttribute(WebAuthnConstants.CREATE_TIMEOUT, createTimeout)
.setAttribute(WebAuthnConstants.EXCLUDE_CREDENTIAL_IDS, excludeCredentialIds) .setAttribute(WebAuthnConstants.EXCLUDE_CREDENTIAL_IDS, excludeCredentialIds)
.setAttribute(WebAuthnConstants.IS_SET_RETRY, isSetRetry)
.createForm("webauthn-register.ftl"); .createForm("webauthn-register.ftl");
context.challenge(form); context.challenge(form);
} }

View file

@ -195,10 +195,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("statusCode", status.getStatusCode()); attributes.put("statusCode", status.getStatusCode());
} }
if (authenticationSession != null && authenticationSession.getClientNote(Constants.KC_ACTION_EXECUTING) != null) {
attributes.put("isAppInitiatedAction", true);
}
switch (page) { switch (page) {
case LOGIN_CONFIG_TOTP: case LOGIN_CONFIG_TOTP:
attributes.put("totp", new TotpBean(session, realm, user, uriInfo.getRequestUriBuilder())); 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) { if (realm != null && user != null && session != null) {
attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session)); 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; package org.keycloak.testsuite.pages.webauthn;
import org.junit.Assert;
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage; import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
import org.openqa.selenium.By; import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.NoSuchElementException;
@ -14,10 +15,22 @@ public class WebAuthnErrorPage extends LanguageComboboxAwarePage {
@FindBy(id = "kc-try-again") @FindBy(id = "kc-try-again")
private WebElement tryAgainButton; private WebElement tryAgainButton;
// Available only with AIA
@FindBy(id = "cancelWebAuthnAIA")
private WebElement cancelRegistrationAIA;
public void clickTryAgain() { public void clickTryAgain() {
tryAgainButton.click(); tryAgainButton.click();
} }
public void clickCancelRegistrationAIA() {
try {
cancelRegistrationAIA.click();
} catch (NoSuchElementException e) {
Assert.fail("It only works with AIA");
}
}
@Override @Override
public boolean isCurrent() { public boolean isCurrent() {
try { try {

View file

@ -20,6 +20,9 @@ package org.keycloak.testsuite.pages.webauthn;
import org.junit.Assert; import org.junit.Assert;
import org.keycloak.testsuite.pages.AbstractPage; import org.keycloak.testsuite.pages.AbstractPage;
import org.openqa.selenium.Alert; 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.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait; import org.openqa.selenium.support.ui.WebDriverWait;
@ -31,6 +34,23 @@ import org.openqa.selenium.support.ui.WebDriverWait;
*/ */
public class WebAuthnRegisterPage extends AbstractPage { 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) { public void registerWebAuthnCredential(String authenticatorLabel) {
// label edit after registering authenicator by .create() // label edit after registering authenicator by .create()
@ -43,8 +63,20 @@ public class WebAuthnRegisterPage extends AbstractPage {
promptDialog.accept(); promptDialog.accept();
} }
private boolean isAIA() {
try {
registerAIAButton.getText();
cancelAIAButton.getText();
return true;
} catch (NoSuchElementException e) {
return false;
}
}
public boolean isCurrent() { 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 // Cant verify the page in case that prompt is shown. Prompt is shown immediately when WebAuthnRegisterPage is displayed
throw new UnsupportedOperationException(); 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> </table>
</#if> </#if>
<div id="kc-error-message">
<input tabindex="4" onclick="refreshPage()" type="button" <input tabindex="4" onclick="refreshPage()" type="button"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
name="try-again" id="kc-try-again" value="${kcSanitize(msg("doTryAgain"))?no_esc}"/> name="try-again" id="kc-try-again" value="${kcSanitize(msg("doTryAgain"))?no_esc}"
</div> />
<#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> </#if>
</@layout.registrationLayout> </@layout.registrationLayout>

View file

@ -16,9 +16,12 @@
<input type="hidden" id="error" name="error"/> <input type="hidden" id="error" name="error"/>
</div> </div>
</form> </form>
<script type="text/javascript" src="${url.resourcesPath}/node_modules/jquery/dist/jquery.min.js"></script> <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" src="${url.resourcesPath}/js/base64url.js"></script>
<script type="text/javascript"> <script type="text/javascript">
function registerSecurityKey() {
// mandatory parameters // mandatory parameters
let challenge = "${challenge}"; let challenge = "${challenge}";
let userid = "${userid}"; let userid = "${userid}";
@ -39,7 +42,7 @@
displayName: username displayName: username
}, },
pubKeyCredParams: pubKeyCredParams, pubKeyCredParams: pubKeyCredParams,
} };
// optional parameters // optional parameters
let rpId = "${rpId}"; let rpId = "${rpId}";
@ -105,6 +108,7 @@
$("#register").submit(); $("#register").submit();
}); });
}
function getPubKeyCredParams(signatureAlgorithms) { function getPubKeyCredParams(signatureAlgorithms) {
let pubKeyCredParams = []; let pubKeyCredParams = [];
@ -115,7 +119,10 @@
let signatureAlgorithmsList = signatureAlgorithms.split(','); let signatureAlgorithmsList = signatureAlgorithms.split(',');
for (let i = 0; i < signatureAlgorithmsList.length; i++) { 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; return pubKeyCredParams;
} }
@ -127,12 +134,33 @@
let excludeCredentialIdsList = excludeCredentialIds.split(','); let excludeCredentialIdsList = excludeCredentialIds.split(',');
for (let i = 0; i < excludeCredentialIdsList.length; i++) { 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; return excludeCredentials;
} }
</script> </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> </#if>
</@layout.registrationLayout> </@layout.registrationLayout>

View file

@ -127,6 +127,10 @@ div.kc-logo-text span {
text-align: center; text-align: center;
} }
#kc-webauthn-settings-form{
padding-top:8px;
}
/* #kc-content-wrapper { /* #kc-content-wrapper {
overflow-y: hidden; overflow-y: hidden;
} */ } */