KEYCLOAK-12469 KEYCLOAK-12185 Implement nice design to the screen wit… (#6690)
* KEYCLOAK-12469 KEYCLOAK-12185 Add CredentialTypeMetadata. Implement the screen with authentication mechanisms and implement Account REST Credentials API by use the credential type metadata
This commit is contained in:
parent
6ac5a2a17e
commit
d8e450719b
24 changed files with 918 additions and 235 deletions
|
@ -1,16 +1,24 @@
|
|||
package org.keycloak.authentication;
|
||||
|
||||
import org.keycloak.credential.CredentialProvider;
|
||||
import org.keycloak.credential.CredentialTypeMetadata;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
public class AuthenticationSelectionOption {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final AuthenticationExecutionModel authExec;
|
||||
private final CredentialTypeMetadata credentialTypeMetadata;
|
||||
|
||||
public AuthenticationSelectionOption(KeycloakSession session, AuthenticationExecutionModel authExec) {
|
||||
this.session = session;
|
||||
this.authExec = authExec;
|
||||
Authenticator authenticator = session.getProvider(Authenticator.class, authExec.getAuthenticator());
|
||||
if (authenticator instanceof CredentialValidator) {
|
||||
CredentialProvider credentialProvider = ((CredentialValidator) authenticator).getCredentialProvider(session);
|
||||
credentialTypeMetadata = credentialProvider.getCredentialTypeMetadata();
|
||||
} else {
|
||||
credentialTypeMetadata = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -22,15 +30,18 @@ public class AuthenticationSelectionOption {
|
|||
return authExec.getId();
|
||||
}
|
||||
|
||||
public String getAuthExecName() {
|
||||
return authExec.getAuthenticator();
|
||||
public String getDisplayName() {
|
||||
return credentialTypeMetadata == null ? authExec.getAuthenticator() + "-display-name" : credentialTypeMetadata.getDisplayName();
|
||||
}
|
||||
|
||||
public String getAuthExecDisplayName() {
|
||||
// TODO: Retrieve the displayName for the authenticator from the AuthenticationFactory
|
||||
// TODO: Retrieve icon CSS style
|
||||
// TODO: Should be addressed as part of https://issues.redhat.com/browse/KEYCLOAK-12185
|
||||
return getAuthExecName();
|
||||
public String getHelpText() {
|
||||
return credentialTypeMetadata == null ? authExec.getAuthenticator() + "-help-text" : credentialTypeMetadata.getHelpText();
|
||||
}
|
||||
|
||||
public String getIconCssClass() {
|
||||
// For now, we won't allow to retrieve "iconCssClass" from the AuthenticatorFactory. We will see in the future if we need
|
||||
// this capability for authenticator factories, which authenticators don't implement credentialProvider
|
||||
return credentialTypeMetadata == null ? CredentialTypeMetadata.DEFAULT_ICON_CSS_CLASS : credentialTypeMetadata.getIconCssClass();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -68,4 +68,5 @@ public interface ConfigurableAuthenticatorFactory extends ConfiguredProvider {
|
|||
* @return
|
||||
*/
|
||||
boolean isUserSetupAllowed();
|
||||
|
||||
}
|
||||
|
|
|
@ -49,4 +49,6 @@ public interface CredentialProvider<T extends CredentialModel> extends Provider
|
|||
}
|
||||
return getCredentialFromModel(models.get(0));
|
||||
}
|
||||
|
||||
CredentialTypeMetadata getCredentialTypeMetadata();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,282 @@
|
|||
/*
|
||||
* 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.credential;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class CredentialTypeMetadata implements Comparable<CredentialTypeMetadata> {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(CredentialTypeMetadata.class);
|
||||
|
||||
public static final String DEFAULT_ICON_CSS_CLASS = "kcAuthenticatorDefaultClass";
|
||||
|
||||
private String type;
|
||||
|
||||
private String displayName;
|
||||
|
||||
private String helpText;
|
||||
|
||||
private String iconCssClass = DEFAULT_ICON_CSS_CLASS;
|
||||
|
||||
private String createAction;
|
||||
|
||||
private String updateAction;
|
||||
|
||||
private Boolean removeable;
|
||||
|
||||
private Category category;
|
||||
|
||||
|
||||
public enum Category {
|
||||
PASSWORD("password", 1),
|
||||
TWO_FACTOR("two-factor", 2),
|
||||
PASSWORDLESS("passwordless", 3);
|
||||
|
||||
private String categoryName;
|
||||
private int order;
|
||||
|
||||
Category(String categoryName, int order) {
|
||||
this.categoryName = categoryName;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return categoryName;
|
||||
}
|
||||
|
||||
public int compareWith(Category that) {
|
||||
return order - that.order;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private CredentialTypeMetadata() {
|
||||
}
|
||||
|
||||
|
||||
// GETTERS
|
||||
|
||||
/**
|
||||
* @return credential type like for example "password", "otp" or "webauthn"
|
||||
*/
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the label, which will be shown to the end user on various screens, like login screen with available authentication mechanisms.
|
||||
* This label will reference this particular authenticator type.
|
||||
* It should be clear to end users. For example, implementations can return "Authenticator Application" for OTP or "Security Key" for WebAuthn.
|
||||
*
|
||||
* Alternatively, this method can return a message key, so that it is possible to localize it for various languages.
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the text, which will be shown to the user on various screens, like login screen with available authentication mechanisms.
|
||||
* This text will reference this particular authenticator type.
|
||||
* For example for OTP, the returned text could be "Enter a verification code from authenticator application" .
|
||||
*
|
||||
* Alternatively, this method can return a message key, so that it is possible to localize it for various languages.
|
||||
*/
|
||||
public String getHelpText() {
|
||||
return helpText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the icon CSS, which can be used to display icon, which represents this particular authenticator.
|
||||
*
|
||||
* The icon will be displayed on various places. For example the "Select authenticator" screen during login, where user can select from
|
||||
* various authentication mechanisms for two-factor or passwordless authentication.
|
||||
*
|
||||
* The returned value can be either:
|
||||
* - Key of the property, which will reference the actual CSS in the themes.properties file. For example if you return "kcAuthenticatorWebAuthnClass"
|
||||
* from this method, then your themes.properties should have the property like for example "kcAuthenticatorWebAuthnClass=fa fa-key list-view-pf-icon-lg" .
|
||||
* This would mean that "fa fa-key list-view-pf-icon-lg" will be the actual CSS used.
|
||||
* - the icon CSS class directly. For example you can return "fa fa-key list-view-pf-icon-lg" directly for the above example with WebAuthn.
|
||||
* This alternative is fine just if your authenticator can use same CSS class for all the themes.
|
||||
*
|
||||
* If you don't expect your authenticator to need icon (for example it will never be shown in the "select authenticator" screen), then
|
||||
* it is fine to keep the default value.
|
||||
*/
|
||||
public String getIconCssClass() {
|
||||
return iconCssClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the providerID of the required action, which can be used by the user to create new credential of our type. Null if there is no
|
||||
* action for creating credential. For example we're creating credential in case of "otp" type, but we're updating credential
|
||||
* in case of type "password"
|
||||
*/
|
||||
public String getCreateAction() {
|
||||
return createAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the providerID of the required action, which can be used by the user to update credential of our type. Null if there is no
|
||||
* action for updating credential. For example we're creating credential in case of "otp" type, but we're updating credential
|
||||
* in case of type "password"
|
||||
*/
|
||||
public String getUpdateAction() {
|
||||
return updateAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if user can remove some previously registered credentials of this type.
|
||||
*/
|
||||
public boolean isRemoveable() {
|
||||
return removeable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Category of this credential
|
||||
*/
|
||||
public Category getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public static CredentialTypeMetadataBuilder builder() {
|
||||
return new CredentialTypeMetadataBuilder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(CredentialTypeMetadata other) {
|
||||
int categoryCompare = category == null ? (other.category == null ? 0 : 1) : (other.category == null ? -1 : category.compareWith(other.category));
|
||||
if (categoryCompare != 0) return categoryCompare;
|
||||
|
||||
int typeCompare = type == null ? (other.type == null ? 0 : 1) : (other.type == null ? -1 : type.compareTo(other.type));
|
||||
return typeCompare;
|
||||
|
||||
}
|
||||
|
||||
// BUILDER
|
||||
|
||||
public static class CredentialTypeMetadataBuilder {
|
||||
|
||||
private CredentialTypeMetadata instance = new CredentialTypeMetadata();
|
||||
|
||||
public CredentialTypeMetadataBuilder type(String type) {
|
||||
instance.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialTypeMetadataBuilder displayName(String displayName) {
|
||||
instance.displayName = displayName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialTypeMetadataBuilder helpText(String helpText) {
|
||||
instance.helpText = helpText;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialTypeMetadataBuilder iconCssClass(String iconCssClass) {
|
||||
instance.iconCssClass = iconCssClass;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialTypeMetadataBuilder createAction(String createAction) {
|
||||
instance.createAction = createAction;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialTypeMetadataBuilder updateAction(String updateAction) {
|
||||
instance.updateAction = updateAction;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialTypeMetadataBuilder removeable(boolean removeable) {
|
||||
instance.removeable = removeable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialTypeMetadataBuilder category(Category category) {
|
||||
instance.category = category;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will validate metadata and return them
|
||||
*
|
||||
* @return metadata
|
||||
*/
|
||||
public CredentialTypeMetadata build(KeycloakSession session) {
|
||||
assertNotNull(instance.type, "type");
|
||||
assertNotNull(instance.displayName, "displayName");
|
||||
assertNotNull(instance.helpText, "helpText");
|
||||
assertNotNull(instance.iconCssClass, "iconCssClass");
|
||||
assertNotNull(instance.removeable, "removeable");
|
||||
assertNotNull(instance.category, "category");
|
||||
|
||||
if (!verifyRequiredAction(session, instance.createAction)) {
|
||||
instance.createAction = null;
|
||||
}
|
||||
if (!verifyRequiredAction(session, instance.updateAction)) {
|
||||
instance.updateAction = null;
|
||||
}
|
||||
// Assume credential can't have both createAction and updateAction.
|
||||
if (instance.createAction != null && instance.updateAction != null) {
|
||||
throw new IllegalStateException("Both createAction and updateAction are not null when building CredentialTypeMetadata for the credential type '" + instance.type);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void assertNotNull(Object input, String fieldName) {
|
||||
if (input == null) {
|
||||
throw new IllegalStateException("Field '" + fieldName + "' is null when building CredentialTypeMetadata for the credential type '" + instance.type);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if required action of specified providerId is registered in the realm and enabled
|
||||
private boolean verifyRequiredAction(KeycloakSession session, String requiredActionProviderId) {
|
||||
if (requiredActionProviderId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
if (realm == null) {
|
||||
logger.warnf("Realm was not set in context when trying to get credential metadata of provider '%s'", instance.type);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (RequiredActionProviderModel requiredActionProvider : realm.getRequiredActionProviders()) {
|
||||
if (requiredActionProviderId.equals(requiredActionProvider.getProviderId()) && requiredActionProvider.isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -22,6 +22,7 @@ import org.keycloak.authentication.AuthenticatorFactory;
|
|||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -37,7 +38,7 @@ public class WebAuthnAuthenticatorFactory implements AuthenticatorFactory {
|
|||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return "auth";
|
||||
return WebAuthnCredentialModel.TYPE_TWOFACTOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.authentication.authenticators.browser;
|
|||
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -28,6 +29,11 @@ public class WebAuthnPasswordlessAuthenticatorFactory extends WebAuthnAuthentica
|
|||
|
||||
public static final String PROVIDER_ID = "webauthn-authenticator-passwordless";
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return WebAuthnCredentialModel.TYPE_PASSWORDLESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "WebAuthn Passwordless Authenticator";
|
||||
|
|
|
@ -27,6 +27,7 @@ import javax.ws.rs.core.Response;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.WebAuthnConstants;
|
||||
import org.keycloak.authentication.CredentialRegistrator;
|
||||
import org.keycloak.authentication.InitiatedActionSupport;
|
||||
import org.keycloak.authentication.RequiredActionContext;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
|
@ -82,6 +83,11 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
|||
this.certPathtrustValidator = certPathtrustValidator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InitiatedActionSupport initiatedActionSupport() {
|
||||
return InitiatedActionSupport.SUPPORTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requiredActionChallenge(RequiredActionContext context) {
|
||||
UserModel userModel = context.getUser();
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.credential;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
import org.keycloak.models.credential.dto.OTPCredentialData;
|
||||
import org.keycloak.models.credential.dto.OTPSecretData;
|
||||
|
@ -134,4 +135,17 @@ public class OTPCredentialProvider implements CredentialProvider<OTPCredentialMo
|
|||
public String getType() {
|
||||
return OTPCredentialModel.TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialTypeMetadata getCredentialTypeMetadata() {
|
||||
return CredentialTypeMetadata.builder()
|
||||
.type(getType())
|
||||
.category(CredentialTypeMetadata.Category.TWO_FACTOR)
|
||||
.displayName("otp-display-name")
|
||||
.helpText("otp-help-text")
|
||||
.iconCssClass("kcAuthenticatorOTPClass")
|
||||
.createAction(UserModel.RequiredAction.CONFIGURE_TOTP.toString())
|
||||
.removeable(true)
|
||||
.build(session);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.keycloak.credential;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.credential.hash.PasswordHashProvider;
|
||||
import org.keycloak.models.ModelException;
|
||||
|
@ -293,4 +294,17 @@ public class PasswordCredentialProvider implements CredentialProvider<PasswordCr
|
|||
public String getType() {
|
||||
return PasswordCredentialModel.TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialTypeMetadata getCredentialTypeMetadata() {
|
||||
return CredentialTypeMetadata.builder()
|
||||
.type(getType())
|
||||
.category(CredentialTypeMetadata.Category.PASSWORD)
|
||||
.displayName("password")
|
||||
.helpText("password-help-text")
|
||||
.iconCssClass("kcAuthenticatorPasswordClass")
|
||||
.updateAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString())
|
||||
.removeable(false)
|
||||
.build(session);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import java.util.List;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -228,4 +229,21 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialTypeMetadata getCredentialTypeMetadata() {
|
||||
return CredentialTypeMetadata.builder()
|
||||
.type(getType())
|
||||
.category(CredentialTypeMetadata.Category.TWO_FACTOR)
|
||||
.displayName("webauthn-display-name")
|
||||
.helpText("webauthn-help-text")
|
||||
.iconCssClass("kcAuthenticatorWebAuthnClass")
|
||||
.createAction(WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.removeable(true)
|
||||
.build(session);
|
||||
}
|
||||
|
||||
protected KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.keycloak.credential;
|
||||
|
||||
import com.webauthn4j.converter.util.CborConverter;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
|
||||
|
@ -37,4 +38,17 @@ public class WebAuthnPasswordlessCredentialProvider extends WebAuthnCredentialPr
|
|||
public String getType() {
|
||||
return WebAuthnCredentialModel.TYPE_PASSWORDLESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialTypeMetadata getCredentialTypeMetadata() {
|
||||
return CredentialTypeMetadata.builder()
|
||||
.type(getType())
|
||||
.category(CredentialTypeMetadata.Category.PASSWORDLESS)
|
||||
.displayName("webauthn-passwordless-display-name")
|
||||
.helpText("webauthn-passwordless-help-text")
|
||||
.iconCssClass("kcAuthenticatorWebAuthnPasswordlessClass")
|
||||
.createAction(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)
|
||||
.removeable(true)
|
||||
.build(getKeycloakSession());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package org.keycloak.services.resources.account;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.authentication.CredentialRegistrator;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.credential.CredentialProvider;
|
||||
import org.keycloak.credential.CredentialTypeMetadata;
|
||||
import org.keycloak.credential.PasswordCredentialProvider;
|
||||
import org.keycloak.credential.PasswordCredentialProviderFactory;
|
||||
import org.keycloak.credential.UserCredentialStoreManager;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.*;
|
||||
|
@ -25,13 +28,25 @@ import javax.ws.rs.PUT;
|
|||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.DISABLED;
|
||||
|
||||
public class AccountCredentialResource {
|
||||
|
||||
public static final String TYPE = "type";
|
||||
public static final String ENABLED_ONLY = "enabled-only";
|
||||
public static final String USER_CREDENTIALS = "user-credentials";
|
||||
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final EventBuilder event;
|
||||
private final UserModel user;
|
||||
|
@ -46,42 +61,38 @@ public class AccountCredentialResource {
|
|||
realm = session.getContext().getRealm();
|
||||
}
|
||||
|
||||
// TODO: This is kept here for now and commented. The endpoints will be added by team cheetah during work on account console.
|
||||
// This is here just to show what logic will need to be called in the new endpoints. We may need to remove it and/or change it
|
||||
// @GET
|
||||
// @NoCache
|
||||
// @Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
// public List<CredentialRepresentation> credentials(){
|
||||
// auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||
// List<CredentialModel> models = session.userCredentialManager().getStoredCredentials(realm, user);
|
||||
// models.forEach(c -> c.setSecretData(null));
|
||||
// return models.stream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toList());
|
||||
// }
|
||||
|
||||
private static class CredentialContainer {
|
||||
// ** These first three attributes can be ordinary UI text or a key into
|
||||
public static class CredentialContainer {
|
||||
// ** category, displayName and helptext attributes can be ordinary UI text or a key into
|
||||
// a localized message bundle. Typically, it will be a key, but
|
||||
// the UI will work just fine if you don't care about localization
|
||||
// and you want to just send UI text.
|
||||
//
|
||||
// Also, the ${} shown in Apicurio is not needed.
|
||||
private String type;
|
||||
private String category; // **
|
||||
private String type; // **
|
||||
private String displayName;
|
||||
private String helptext; // **
|
||||
private boolean enabled;
|
||||
private String iconCssClass;
|
||||
private String createAction;
|
||||
private String updateAction;
|
||||
private boolean removeable;
|
||||
private List<CredentialModel> userCredentials;
|
||||
private List<CredentialRepresentation> userCredentials;
|
||||
private CredentialTypeMetadata metadata;
|
||||
|
||||
public CredentialContainer(String category, String type, String helptext, boolean enabled, String createAction, String updateAction, boolean removeable,List<CredentialModel> userCredentials) {
|
||||
this.category = category;
|
||||
this.type = type;
|
||||
this.helptext = helptext;
|
||||
this.enabled = enabled;
|
||||
this.createAction = createAction;
|
||||
this.updateAction = updateAction;
|
||||
this.removeable = removeable;
|
||||
public CredentialContainer() {
|
||||
}
|
||||
|
||||
public CredentialContainer(CredentialTypeMetadata metadata, List<CredentialRepresentation> userCredentials) {
|
||||
this.metadata = metadata;
|
||||
this.type = metadata.getType();
|
||||
this.category = metadata.getCategory().toString();
|
||||
this.displayName = metadata.getDisplayName();
|
||||
this.helptext = metadata.getHelpText();
|
||||
this.iconCssClass = metadata.getIconCssClass();
|
||||
this.createAction = metadata.getCreateAction();
|
||||
this.updateAction = metadata.getUpdateAction();
|
||||
this.removeable = metadata.isRemoveable();
|
||||
this.userCredentials = userCredentials;
|
||||
}
|
||||
|
||||
|
@ -93,12 +104,16 @@ public class AccountCredentialResource {
|
|||
return type;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getHelptext() {
|
||||
return helptext;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
public String getIconCssClass() {
|
||||
return iconCssClass;
|
||||
}
|
||||
|
||||
public String getCreateAction() {
|
||||
|
@ -113,107 +128,127 @@ public class AccountCredentialResource {
|
|||
return removeable;
|
||||
}
|
||||
|
||||
public List<CredentialModel> getUserCredentials() {
|
||||
public List<CredentialRepresentation> getUserCredentials() {
|
||||
return userCredentials;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public CredentialTypeMetadata getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the list of credentials available to the current logged in user. It will return only credentials of enabled types,
|
||||
* which user can use to authenticate in some authentication flow.
|
||||
*
|
||||
* @param type Allows to filter just single credential type, which will be specified as this parameter. If null, it will return all credential types
|
||||
* @param userCredentials specifies if user credentials should be returned. If true, they will be returned in the "userCredentials" attribute of
|
||||
* particular credential. Defaults to true.
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
public List<CredentialContainer> dummyCredentialTypes(){
|
||||
public List<CredentialContainer> credentialTypes(@QueryParam(TYPE) String type,
|
||||
@QueryParam(USER_CREDENTIALS) Boolean userCredentials) {
|
||||
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||
List<CredentialModel> models = session.userCredentialManager().getStoredCredentials(realm, user);
|
||||
|
||||
List<CredentialModel> passwordUserCreds = new java.util.ArrayList<>();
|
||||
passwordUserCreds.add(models.get(0));
|
||||
boolean filterUserCredentials = userCredentials != null && !userCredentials;
|
||||
|
||||
List<CredentialModel> otpUserCreds = new java.util.ArrayList<>();
|
||||
if (models.size() > 1) otpUserCreds.add(models.get(1));
|
||||
if (models.size() > 2) otpUserCreds.add(models.get(2));
|
||||
List<CredentialContainer> credentialTypes = new LinkedList<>();
|
||||
List<CredentialProvider> credentialProviders = UserCredentialStoreManager.getCredentialProviders(session, realm, CredentialProvider.class);
|
||||
Set<String> enabledCredentialTypes = getEnabledCredentialTypes(credentialProviders);
|
||||
|
||||
List<CredentialModel> webauthnUserCreds = new java.util.ArrayList<>();
|
||||
CredentialModel webauthnCred = new CredentialModel();
|
||||
webauthnCred.setId("bogus-id");
|
||||
webauthnCred.setUserLabel("yubikey key");
|
||||
webauthnCred.setCreatedDate(1579122652382L);
|
||||
webauthnUserCreds.add(webauthnCred);
|
||||
List<CredentialModel> models = filterUserCredentials ? null : session.userCredentialManager().getStoredCredentials(realm, user);
|
||||
|
||||
List<CredentialModel> webauthnStrongUserCreds = new java.util.ArrayList<>();
|
||||
CredentialModel webauthnStrongCred = new CredentialModel();
|
||||
webauthnStrongCred.setId("bogus-id-for-webauthnStrong");
|
||||
webauthnStrongCred.setUserLabel("My very strong key with required PIN");
|
||||
webauthnStrongCred.setCreatedDate(1579122652382L);
|
||||
webauthnUserCreds.add(webauthnStrongCred);
|
||||
// Don't return secrets from REST endpoint
|
||||
if (models != null) {
|
||||
for (CredentialModel credential : models) {
|
||||
credential.setSecretData(null);
|
||||
}
|
||||
}
|
||||
|
||||
CredentialContainer password = new CredentialContainer(
|
||||
"password",
|
||||
"password",
|
||||
"passwordHelptext",
|
||||
true,
|
||||
null, // no create action
|
||||
"UPDATE_PASSWORD",
|
||||
false,
|
||||
passwordUserCreds
|
||||
);
|
||||
CredentialContainer otp = new CredentialContainer(
|
||||
"two-factor",
|
||||
"otp",
|
||||
"otpHelptext",
|
||||
true,
|
||||
"CONFIGURE_TOTP",
|
||||
null, // no update action
|
||||
true,
|
||||
otpUserCreds
|
||||
);
|
||||
CredentialContainer webAuthn = new CredentialContainer(
|
||||
"two-factor",
|
||||
"webauthn",
|
||||
"webauthnHelptext",
|
||||
true,
|
||||
"CONFIGURE_WEBAUTHN",
|
||||
null, // no update action
|
||||
true,
|
||||
webauthnUserCreds
|
||||
);
|
||||
CredentialContainer passwordless = new CredentialContainer(
|
||||
"passwordless",
|
||||
"webauthn-passwordless",
|
||||
"webauthn-passwordlessHelptext",
|
||||
true,
|
||||
"CONFIGURE_WEBAUTHN_STRONG",
|
||||
null, // no update action
|
||||
true,
|
||||
webauthnStrongUserCreds
|
||||
);
|
||||
for (CredentialProvider credentialProvider : credentialProviders) {
|
||||
String credentialProviderType = credentialProvider.getType();
|
||||
|
||||
List<CredentialContainer> dummyCreds = new java.util.ArrayList<>();
|
||||
dummyCreds.add(password);
|
||||
dummyCreds.add(otp);
|
||||
dummyCreds.add(webAuthn);
|
||||
dummyCreds.add(passwordless);
|
||||
// Filter just by single type
|
||||
if (type != null && !type.equals(credentialProviderType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return dummyCreds;
|
||||
boolean enabled = enabledCredentialTypes.contains(credentialProviderType);
|
||||
|
||||
// Filter disabled credential types
|
||||
if (!enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CredentialTypeMetadata metadata = credentialProvider.getCredentialTypeMetadata();
|
||||
|
||||
List<CredentialRepresentation> userCredentialModels = filterUserCredentials ? null : models.stream()
|
||||
.filter(credentialModel -> credentialProvider.getType().equals(credentialModel.getType()))
|
||||
.map(ModelToRepresentation::toRepresentation)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
CredentialContainer credType = new CredentialContainer(metadata, userCredentialModels);
|
||||
credentialTypes.add(credType);
|
||||
}
|
||||
|
||||
credentialTypes.sort(Comparator.comparing(CredentialContainer::getMetadata));
|
||||
|
||||
return credentialTypes;
|
||||
}
|
||||
//
|
||||
//
|
||||
// @GET
|
||||
// @Path("registrators")
|
||||
// @NoCache
|
||||
// @Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
// public List<String> getCredentialRegistrators(){
|
||||
// auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||
//
|
||||
// return session.getContext().getRealm().getRequiredActionProviders().stream()
|
||||
// .map(RequiredActionProviderModel::getProviderId)
|
||||
// .filter(providerId -> session.getProvider(RequiredActionProvider.class, providerId) instanceof CredentialRegistrator)
|
||||
// .collect(Collectors.toList());
|
||||
// }
|
||||
//
|
||||
|
||||
// Going through all authentication flows and their authentication executions to see if there is any authenticator of the corresponding
|
||||
// credential type.
|
||||
private Set<String> getEnabledCredentialTypes(List<CredentialProvider> credentialProviders) {
|
||||
Set<String> enabledCredentialTypes = new HashSet<>();
|
||||
|
||||
for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) {
|
||||
// Ignore DISABLED executions and flows
|
||||
if (isFlowEffectivelyDisabled(flow)) continue;
|
||||
|
||||
for (AuthenticationExecutionModel execution : realm.getAuthenticationExecutions(flow.getId())) {
|
||||
if (execution.getAuthenticator() != null && DISABLED != execution.getRequirement()) {
|
||||
AuthenticatorFactory authenticatorFactory = (AuthenticatorFactory) session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, execution.getAuthenticator());
|
||||
if (authenticatorFactory != null && authenticatorFactory.getReferenceCategory() != null) {
|
||||
enabledCredentialTypes.add(authenticatorFactory.getReferenceCategory());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> credentialTypes = credentialProviders.stream()
|
||||
.map(CredentialProvider::getType)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
enabledCredentialTypes.retainAll(credentialTypes);
|
||||
|
||||
return enabledCredentialTypes;
|
||||
}
|
||||
|
||||
// Returns true if flow is effectively disabled - either it's execution or some parent execution is disabled
|
||||
private boolean isFlowEffectivelyDisabled(AuthenticationFlowModel flow) {
|
||||
while (!flow.isTopLevel()) {
|
||||
AuthenticationExecutionModel flowExecution = realm.getAuthenticationExecutionByFlowId(flow.getId());
|
||||
if (flowExecution == null) return false; // Can happen under some corner cases
|
||||
if (DISABLED == flowExecution.getRequirement()) return true;
|
||||
if (flowExecution.getParentFlow() == null) return false;
|
||||
|
||||
// Check parent flow
|
||||
flow = realm.getAuthenticationFlowById(flowExecution.getParentFlow());
|
||||
if (flow == null) return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a credential for a user
|
||||
* Remove a credential of current user
|
||||
*
|
||||
* @param credentialId ID of the credential, which will be removed
|
||||
*/
|
||||
@Path("{credentialId}")
|
||||
@DELETE
|
||||
|
@ -222,18 +257,23 @@ public class AccountCredentialResource {
|
|||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||
session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
|
||||
}
|
||||
//
|
||||
// /**
|
||||
// * Update a credential label for a user
|
||||
// */
|
||||
// @PUT
|
||||
// @Consumes(javax.ws.rs.core.MediaType.TEXT_PLAIN)
|
||||
// @Path("{credentialId}/label")
|
||||
// public void setLabel(final @PathParam("credentialId") String credentialId, String userLabel) {
|
||||
// auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||
// session.userCredentialManager().updateCredentialLabel(realm, user, credentialId, userLabel);
|
||||
// }
|
||||
//
|
||||
|
||||
|
||||
/**
|
||||
* Update a user label of specified credential of current user
|
||||
*
|
||||
* @param credentialId ID of the credential, which will be updated
|
||||
* @param userLabel new user label
|
||||
*/
|
||||
@PUT
|
||||
@Consumes(javax.ws.rs.core.MediaType.TEXT_PLAIN)
|
||||
@Path("{credentialId}/label")
|
||||
public void setLabel(final @PathParam("credentialId") String credentialId, String userLabel) {
|
||||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||
session.userCredentialManager().updateCredentialLabel(realm, user, credentialId, userLabel);
|
||||
}
|
||||
|
||||
// TODO: This is kept here for now and commented.
|
||||
// /**
|
||||
// * Move a credential to a position behind another credential
|
||||
// * @param credentialId The credential to move
|
||||
|
|
|
@ -45,7 +45,6 @@ public abstract class LanguageComboboxAwarePage extends AbstractPage {
|
|||
@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
|
||||
@FindBy(id = "reset-login")
|
||||
private WebElement resetLoginLink;
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import org.openqa.selenium.By;
|
|||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
import org.openqa.selenium.support.ui.Select;
|
||||
|
||||
/**
|
||||
* Login page with the list of authentication mechanisms, which are available to the user (Password, OTP, WebAuthn...)
|
||||
|
@ -17,30 +16,64 @@ import org.openqa.selenium.support.ui.Select;
|
|||
*/
|
||||
public class SelectAuthenticatorPage extends LanguageComboboxAwarePage {
|
||||
|
||||
// Corresponds to the PasswordForm
|
||||
public static final String PASSWORD = "Password";
|
||||
|
||||
// Corresponds to the OTPFormAuthenticator
|
||||
public static final String AUTHENTICATOR_APPLICATION = "Authenticator Application";
|
||||
|
||||
|
||||
@FindBy(id = "authenticators-choice")
|
||||
private WebElement authenticatorsSelect;
|
||||
|
||||
|
||||
/**
|
||||
* Return list of names like for example [ "Password", "Authenticator Application", "Security Key" ]
|
||||
*/
|
||||
public List<String> getAvailableLoginMethods() {
|
||||
return new Select(authenticatorsSelect).getOptions()
|
||||
.stream()
|
||||
.map(WebElement::getText)
|
||||
List<WebElement> rows = getLoginMethodsRows();
|
||||
|
||||
return rows.stream()
|
||||
.map(this::getLoginMethodNameFromRow)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
public String getSelectedLoginMethod() {
|
||||
return new Select(authenticatorsSelect).getOptions()
|
||||
.stream()
|
||||
.filter(webElement -> webElement.getAttribute("selected") != null)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("Selected login method not found"))
|
||||
.getText();
|
||||
/**
|
||||
*
|
||||
* Selects the chosen login method (For example "Password") by click on it.
|
||||
*
|
||||
* @param loginMethodName name as displayed. For example "Password" or "Authenticator Application"
|
||||
*
|
||||
*/
|
||||
public void selectLoginMethod(String loginMethodName) {
|
||||
getLoginMethodRowByName(loginMethodName).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return help text corresponding to the named login method
|
||||
*
|
||||
* @param loginMethodName name as displayed. For example "Password" or "Authenticator Application"
|
||||
* @return
|
||||
*/
|
||||
public String getLoginMethodHelpText(String loginMethodName) {
|
||||
return getLoginMethodRowByName(loginMethodName).findElement(By.className("list-group-item-text")).getText();
|
||||
}
|
||||
|
||||
|
||||
public void selectLoginMethod(String loginMethod) {
|
||||
new Select(authenticatorsSelect).selectByVisibleText(loginMethod);
|
||||
private List<WebElement> getLoginMethodsRows() {
|
||||
return driver.findElements(By.className("list-view-pf-main-info"));
|
||||
}
|
||||
|
||||
private String getLoginMethodNameFromRow(WebElement loginMethodRow) {
|
||||
return loginMethodRow.findElement(By.className("list-group-item-heading")).getText();
|
||||
}
|
||||
|
||||
private WebElement getLoginMethodRowByName(String loginMethodName) {
|
||||
return getLoginMethodsRows().stream()
|
||||
.filter(loginMethodRow -> loginMethodName.equals(getLoginMethodNameFromRow(loginMethodRow)))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("Login method '" + loginMethodName + "' not found in the available authentication mechanisms"));
|
||||
}
|
||||
|
||||
|
||||
|
@ -53,7 +86,7 @@ public class SelectAuthenticatorPage extends LanguageComboboxAwarePage {
|
|||
|
||||
// Check the authenticators-choice available
|
||||
try {
|
||||
driver.findElement(By.id("authenticators-choice"));
|
||||
driver.findElement(By.id("kc-select-credential-form"));
|
||||
} catch (NoSuchElementException nfe) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -20,17 +20,37 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
|||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnRegister;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.credential.CredentialTypeMetadata;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||
import org.keycloak.representations.account.ClientRepresentation;
|
||||
import org.keycloak.representations.account.ConsentRepresentation;
|
||||
import org.keycloak.representations.account.ConsentScopeRepresentation;
|
||||
import org.keycloak.representations.account.SessionRepresentation;
|
||||
import org.keycloak.representations.account.UserRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.account.AccountCredentialResource;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.admin.authentication.AbstractAuthenticationTest;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.TokenUtil;
|
||||
|
||||
|
@ -41,11 +61,14 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.junit.Assert.*;
|
||||
import org.keycloak.services.resources.account.AccountCredentialResource.PasswordUpdate;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -277,6 +300,198 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
assertEquals(expectedStatus, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialsGet() throws IOException {
|
||||
configureBrowserFlowWithWebAuthnAuthenticator("browser-webauthn");
|
||||
|
||||
List<AccountCredentialResource.CredentialContainer> credentials = getCredentials();
|
||||
|
||||
Assert.assertEquals(4, credentials.size());
|
||||
|
||||
AccountCredentialResource.CredentialContainer password = credentials.get(0);
|
||||
assertCredentialContainerExpected(password, PasswordCredentialModel.TYPE, CredentialTypeMetadata.Category.PASSWORD.toString(),
|
||||
"password", "password-help-text", "kcAuthenticatorPasswordClass",
|
||||
null, UserModel.RequiredAction.UPDATE_PASSWORD.toString(), false, 1);
|
||||
|
||||
CredentialRepresentation password1 = password.getUserCredentials().get(0);
|
||||
Assert.assertNull(password1.getSecretData());
|
||||
Assert.assertNotNull(password1.getCredentialData());
|
||||
|
||||
AccountCredentialResource.CredentialContainer otp = credentials.get(1);
|
||||
assertCredentialContainerExpected(otp, OTPCredentialModel.TYPE, CredentialTypeMetadata.Category.TWO_FACTOR.toString(),
|
||||
"otp-display-name", "otp-help-text", "kcAuthenticatorOTPClass",
|
||||
UserModel.RequiredAction.CONFIGURE_TOTP.toString(), null, true, 0);
|
||||
|
||||
// WebAuthn credentials will be returned, but createAction will be still null because requiredAction "webauthn register" not yet registered
|
||||
AccountCredentialResource.CredentialContainer webauthn = credentials.get(2);
|
||||
assertCredentialContainerExpected(webauthn, WebAuthnCredentialModel.TYPE_TWOFACTOR, CredentialTypeMetadata.Category.TWO_FACTOR.toString(),
|
||||
"webauthn-display-name", "webauthn-help-text", "kcAuthenticatorWebAuthnClass",
|
||||
null, null, true, 0);
|
||||
|
||||
AccountCredentialResource.CredentialContainer webauthnPasswordless = credentials.get(3);
|
||||
assertCredentialContainerExpected(webauthnPasswordless, WebAuthnCredentialModel.TYPE_PASSWORDLESS, CredentialTypeMetadata.Category.PASSWORDLESS.toString(),
|
||||
"webauthn-passwordless-display-name", "webauthn-passwordless-help-text", "kcAuthenticatorWebAuthnPasswordlessClass",
|
||||
null, null, true, 0);
|
||||
|
||||
// Register requiredActions for WebAuthn
|
||||
RequiredActionProviderSimpleRepresentation requiredAction = new RequiredActionProviderSimpleRepresentation();
|
||||
requiredAction.setId("12345");
|
||||
requiredAction.setName(WebAuthnRegisterFactory.PROVIDER_ID);
|
||||
requiredAction.setProviderId(WebAuthnRegisterFactory.PROVIDER_ID);
|
||||
testRealm().flows().registerRequiredAction(requiredAction);
|
||||
|
||||
requiredAction = new RequiredActionProviderSimpleRepresentation();
|
||||
requiredAction.setId("6789");
|
||||
requiredAction.setName(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
|
||||
requiredAction.setProviderId(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
|
||||
testRealm().flows().registerRequiredAction(requiredAction);
|
||||
|
||||
// requiredActions should be available
|
||||
credentials = getCredentials();
|
||||
Assert.assertEquals(WebAuthnRegisterFactory.PROVIDER_ID, credentials.get(2).getCreateAction());
|
||||
Assert.assertEquals(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID, credentials.get(3).getCreateAction());
|
||||
|
||||
// disable WebAuthn passwordless required action. It won't be returned then
|
||||
RequiredActionProviderRepresentation requiredActionRep = testRealm().flows().getRequiredAction(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
|
||||
requiredActionRep.setEnabled(false);
|
||||
testRealm().flows().updateRequiredAction(WebAuthnRegisterFactory.PROVIDER_ID, requiredActionRep);
|
||||
|
||||
credentials = getCredentials();
|
||||
Assert.assertNull(credentials.get(2).getCreateAction());
|
||||
|
||||
// Test that WebAuthn won't be returned when removed from the authentication flow
|
||||
removeWebAuthnFlow("browser-webauthn");
|
||||
|
||||
credentials = getCredentials();
|
||||
|
||||
Assert.assertEquals(2, credentials.size());
|
||||
Assert.assertEquals(PasswordCredentialModel.TYPE, credentials.get(0).getType());
|
||||
Assert.assertNotNull(OTPCredentialModel.TYPE, credentials.get(1).getType());
|
||||
|
||||
// Test password-only
|
||||
credentials = SimpleHttp.doGet(getAccountUrl("credentials?" + AccountCredentialResource.TYPE + "=password"), httpClient)
|
||||
.auth(tokenUtil.getToken()).asJson(new TypeReference<List<AccountCredentialResource.CredentialContainer>>() {});
|
||||
Assert.assertEquals(1, credentials.size());
|
||||
password = credentials.get(0);
|
||||
Assert.assertEquals(PasswordCredentialModel.TYPE, password.getType());
|
||||
Assert.assertEquals(1, password.getUserCredentials().size());
|
||||
|
||||
// Test password-only and user-credentials
|
||||
credentials = SimpleHttp.doGet(getAccountUrl("credentials?" + AccountCredentialResource.TYPE + "=password&" +
|
||||
AccountCredentialResource.USER_CREDENTIALS + "=false"), httpClient)
|
||||
.auth(tokenUtil.getToken()).asJson(new TypeReference<List<AccountCredentialResource.CredentialContainer>>() {});
|
||||
Assert.assertEquals(1, credentials.size());
|
||||
password = credentials.get(0);
|
||||
Assert.assertEquals(PasswordCredentialModel.TYPE, password.getType());
|
||||
Assert.assertNull(password.getUserCredentials());
|
||||
}
|
||||
|
||||
// Send REST request to get all credential containers and credentials of current user
|
||||
private List<AccountCredentialResource.CredentialContainer> getCredentials() throws IOException {
|
||||
return SimpleHttp.doGet(getAccountUrl("credentials"), httpClient)
|
||||
.auth(tokenUtil.getToken()).asJson(new TypeReference<List<AccountCredentialResource.CredentialContainer>>() {});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialsGetDisabledOtp() throws IOException {
|
||||
// Disable OTP in all built-in flows
|
||||
|
||||
// Disable parent subflow - that should treat OTP execution as disabled too
|
||||
AuthenticationExecutionModel.Requirement currentBrowserReq = setExecutionRequirement(DefaultAuthenticationFlows.BROWSER_FLOW,
|
||||
"Browser - Conditional OTP", AuthenticationExecutionModel.Requirement.DISABLED);
|
||||
|
||||
// Disable OTP directly in first-broker-login and direct-grant
|
||||
AuthenticationExecutionModel.Requirement currentFBLReq = setExecutionRequirement(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW,
|
||||
"OTP Form", AuthenticationExecutionModel.Requirement.DISABLED);
|
||||
AuthenticationExecutionModel.Requirement currentDirectGrantReq = setExecutionRequirement(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW,
|
||||
"Direct Grant - Conditional OTP", AuthenticationExecutionModel.Requirement.DISABLED);
|
||||
try {
|
||||
// Test that OTP credential is not included. Only password
|
||||
List<AccountCredentialResource.CredentialContainer> credentials = getCredentials();
|
||||
|
||||
Assert.assertEquals(1, credentials.size());
|
||||
Assert.assertEquals(PasswordCredentialModel.TYPE, credentials.get(0).getType());
|
||||
|
||||
// Enable browser subflow. OTP should be available then
|
||||
setExecutionRequirement(DefaultAuthenticationFlows.BROWSER_FLOW,
|
||||
"Browser - Conditional OTP", currentBrowserReq);
|
||||
credentials = getCredentials();
|
||||
Assert.assertEquals(2, credentials.size());
|
||||
Assert.assertEquals(OTPCredentialModel.TYPE, credentials.get(1).getType());
|
||||
|
||||
// Disable browser subflow and enable FirstBrokerLogin. OTP should be available then
|
||||
setExecutionRequirement(DefaultAuthenticationFlows.BROWSER_FLOW,
|
||||
"Browser - Conditional OTP", AuthenticationExecutionModel.Requirement.DISABLED);
|
||||
setExecutionRequirement(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW,
|
||||
"OTP Form", currentFBLReq);
|
||||
credentials = getCredentials();
|
||||
Assert.assertEquals(2, credentials.size());
|
||||
Assert.assertEquals(OTPCredentialModel.TYPE, credentials.get(1).getType());
|
||||
} finally {
|
||||
// Revert flows
|
||||
setExecutionRequirement(DefaultAuthenticationFlows.BROWSER_FLOW,
|
||||
"Browser - Conditional OTP", currentBrowserReq);
|
||||
setExecutionRequirement(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW,
|
||||
"Direct Grant - Conditional OTP", currentDirectGrantReq);
|
||||
}
|
||||
}
|
||||
|
||||
// Sets new requirement and returns current requirement
|
||||
private AuthenticationExecutionModel.Requirement setExecutionRequirement(String flowAlias, String executionDisplayName, AuthenticationExecutionModel.Requirement newRequirement) {
|
||||
List<AuthenticationExecutionInfoRepresentation> executionInfos = testRealm().flows().getExecutions(flowAlias);
|
||||
for (AuthenticationExecutionInfoRepresentation exInfo : executionInfos) {
|
||||
if (executionDisplayName.equals(exInfo.getDisplayName())) {
|
||||
AuthenticationExecutionModel.Requirement currentRequirement = AuthenticationExecutionModel.Requirement.valueOf(exInfo.getRequirement());
|
||||
exInfo.setRequirement(newRequirement.toString());
|
||||
testRealm().flows().updateExecutions(flowAlias, exInfo);
|
||||
return currentRequirement;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Not found execution '" + executionDisplayName + "' in flow '" + flowAlias + "'.");
|
||||
}
|
||||
|
||||
private void configureBrowserFlowWithWebAuthnAuthenticator(String newFlowAlias) {
|
||||
HashMap<String, String> params = new HashMap<>();
|
||||
params.put("newName", newFlowAlias);
|
||||
Response response = testRealm().flows().copy("browser", params);
|
||||
response.close();
|
||||
String flowId = AbstractAuthenticationTest.findFlowByAlias(newFlowAlias, testRealm().flows().getFlows()).getId();
|
||||
|
||||
AuthenticationExecutionRepresentation execution = new AuthenticationExecutionRepresentation();
|
||||
execution.setParentFlow(flowId);
|
||||
execution.setAuthenticator(WebAuthnAuthenticatorFactory.PROVIDER_ID);
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.toString());
|
||||
response = testRealm().flows().addExecution(execution);
|
||||
response.close();
|
||||
|
||||
execution = new AuthenticationExecutionRepresentation();
|
||||
execution.setParentFlow(flowId);
|
||||
execution.setAuthenticator( WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID);
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE.toString());
|
||||
response = testRealm().flows().addExecution(execution);
|
||||
response.close();
|
||||
}
|
||||
|
||||
private void removeWebAuthnFlow(String flowToDeleteAlias) {
|
||||
List<AuthenticationFlowRepresentation> flows = testRealm().flows().getFlows();
|
||||
AuthenticationFlowRepresentation flowRepresentation = AbstractAuthenticationTest.findFlowByAlias(flowToDeleteAlias, flows);
|
||||
testRealm().flows().deleteFlow(flowRepresentation.getId());
|
||||
}
|
||||
|
||||
private void assertCredentialContainerExpected(AccountCredentialResource.CredentialContainer credential, String type, String category, String displayName, String helpText, String iconCssClass,
|
||||
String createAction, String updateAction, boolean removeable, int userCredentialsCount) {
|
||||
Assert.assertEquals(type, credential.getType());
|
||||
Assert.assertEquals(category, credential.getCategory());
|
||||
Assert.assertEquals(displayName, credential.getDisplayName());
|
||||
Assert.assertEquals(helpText, credential.getHelptext());
|
||||
Assert.assertEquals(iconCssClass, credential.getIconCssClass());
|
||||
Assert.assertEquals(createAction, credential.getCreateAction());
|
||||
Assert.assertEquals(updateAction, credential.getUpdateAction());
|
||||
Assert.assertEquals(removeable, credential.isRemoveable());
|
||||
Assert.assertEquals(userCredentialsCount, credential.getUserCredentials().size());
|
||||
}
|
||||
|
||||
public void testDeleteSessions() throws IOException {
|
||||
TokenUtil viewToken = new TokenUtil("view-account-access", "password");
|
||||
oauth.doLogin("view-account-access", "password");
|
||||
|
|
|
@ -140,12 +140,9 @@ public class KcOidcFirstBrokerLoginNewAuthTest extends AbstractInitializedBaseBr
|
|||
// Just click "Try another way" to verify that both Password and OTP are available. But go back to Password then
|
||||
passwordPage.clickTryAnotherWayLink();
|
||||
selectAuthenticatorPage.assertCurrent();
|
||||
Assert.assertNames(selectAuthenticatorPage.getAvailableLoginMethods(), "Password", "OTP");
|
||||
Assert.assertNames(selectAuthenticatorPage.getAvailableLoginMethods(), SelectAuthenticatorPage.PASSWORD, SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION);
|
||||
|
||||
// TODO: This is limitation of select, that it can't select the already present value. Should be improved when we change to select cart
|
||||
selectAuthenticatorPage.selectLoginMethod("OTP");
|
||||
loginTotpPage.clickTryAnotherWayLink();
|
||||
selectAuthenticatorPage.selectLoginMethod("Password");
|
||||
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.PASSWORD);
|
||||
|
||||
// Login with password
|
||||
Assert.assertTrue(passwordPage.isCurrent("consumer"));
|
||||
|
@ -176,7 +173,7 @@ public class KcOidcFirstBrokerLoginNewAuthTest extends AbstractInitializedBaseBr
|
|||
// Click "Try another way", Select OTP and assert OTP form present
|
||||
passwordPage.clickTryAnotherWayLink();
|
||||
selectAuthenticatorPage.assertCurrent();
|
||||
selectAuthenticatorPage.selectLoginMethod("OTP");
|
||||
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION);
|
||||
|
||||
loginTotpPage.assertCurrent();
|
||||
|
||||
|
|
|
@ -49,7 +49,6 @@ import org.keycloak.testsuite.pages.PasswordPage;
|
|||
import org.keycloak.testsuite.pages.SelectAuthenticatorPage;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||
|
@ -101,14 +100,6 @@ public class MultiFactorAuthenticationTest extends AbstractTestRealmKeycloakTest
|
|||
return res;
|
||||
}
|
||||
|
||||
private void importTestRealm(Consumer<RealmRepresentation> realmUpdater) {
|
||||
RealmRepresentation realm = loadTestRealm();
|
||||
if (realmUpdater != null) {
|
||||
realmUpdater.accept(realm);
|
||||
}
|
||||
importRealm(realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
log.debug("Adding test realm for import from testrealm.json");
|
||||
|
@ -138,10 +129,14 @@ public class MultiFactorAuthenticationTest extends AbstractTestRealmKeycloakTest
|
|||
passwordPage.clickTryAnotherWayLink();
|
||||
|
||||
selectAuthenticatorPage.assertCurrent();
|
||||
Assert.assertEquals(Arrays.asList("Password", "OTP"), selectAuthenticatorPage.getAvailableLoginMethods());
|
||||
Assert.assertEquals(Arrays.asList(SelectAuthenticatorPage.PASSWORD, SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION), selectAuthenticatorPage.getAvailableLoginMethods());
|
||||
|
||||
// Assert help texts
|
||||
Assert.assertEquals("Log in by entering your password.", selectAuthenticatorPage.getLoginMethodHelpText(SelectAuthenticatorPage.PASSWORD));
|
||||
Assert.assertEquals("Enter a verification code from authenticator application.", selectAuthenticatorPage.getLoginMethodHelpText(SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION));
|
||||
|
||||
// Select OTP and see that just single OTP is available for this user
|
||||
selectAuthenticatorPage.selectLoginMethod("OTP");
|
||||
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION);
|
||||
loginTotpPage.assertCurrent();
|
||||
loginTotpPage.assertTryAnotherWayLinkAvailability(true);
|
||||
loginTotpPage.assertOtpCredentialSelectorAvailability(false);
|
||||
|
@ -159,7 +154,7 @@ public class MultiFactorAuthenticationTest extends AbstractTestRealmKeycloakTest
|
|||
loginTotpPage.clickTryAnotherWayLink();
|
||||
|
||||
selectAuthenticatorPage.assertCurrent();
|
||||
Assert.assertEquals(Arrays.asList("OTP", "Password"), selectAuthenticatorPage.getAvailableLoginMethods());
|
||||
Assert.assertEquals(Arrays.asList(SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION, SelectAuthenticatorPage.PASSWORD), selectAuthenticatorPage.getAvailableLoginMethods());
|
||||
} finally {
|
||||
BrowserFlowTest.revertFlows(testRealm(), "browser - alternative");
|
||||
}
|
||||
|
@ -218,8 +213,8 @@ public class MultiFactorAuthenticationTest extends AbstractTestRealmKeycloakTest
|
|||
// Click "Try another way" . Ability to have both password and OTP should be possible even if OTP is in different subflow
|
||||
passwordPage.clickTryAnotherWayLink();
|
||||
selectAuthenticatorPage.assertCurrent();
|
||||
Assert.assertEquals(Arrays.asList("Password", "OTP"), selectAuthenticatorPage.getAvailableLoginMethods());
|
||||
selectAuthenticatorPage.selectLoginMethod("OTP");
|
||||
Assert.assertEquals(Arrays.asList(SelectAuthenticatorPage.PASSWORD, SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION), selectAuthenticatorPage.getAvailableLoginMethods());
|
||||
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION);
|
||||
|
||||
// Should be on the OTP now. Click "Try another way" again. Should see again both Password and OTP
|
||||
loginTotpPage.assertCurrent();
|
||||
|
@ -227,9 +222,9 @@ public class MultiFactorAuthenticationTest extends AbstractTestRealmKeycloakTest
|
|||
|
||||
loginTotpPage.clickTryAnotherWayLink();
|
||||
selectAuthenticatorPage.assertCurrent();
|
||||
Assert.assertEquals(Arrays.asList("Password", "OTP"), selectAuthenticatorPage.getAvailableLoginMethods());
|
||||
Assert.assertEquals(Arrays.asList(SelectAuthenticatorPage.PASSWORD, SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION), selectAuthenticatorPage.getAvailableLoginMethods());
|
||||
|
||||
selectAuthenticatorPage.selectLoginMethod("Password");
|
||||
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.PASSWORD);
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.login("password");
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ loginTotpCounter=Counter
|
|||
loginTotp.totp=Time-based
|
||||
loginTotp.hotp=Counter-based
|
||||
|
||||
loginChooseAuthenticator=Select your authentication method
|
||||
loginChooseAuthenticator=Select login method
|
||||
|
||||
oauthGrantRequest=Do you grant these access privileges?
|
||||
inResource=in
|
||||
|
@ -334,11 +334,16 @@ saml.post-form.message=Redirecting, please wait.
|
|||
saml.post-form.js-disabled=JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.
|
||||
|
||||
#authenticators
|
||||
auth-otp-form=OTP
|
||||
auth-password-form=Password
|
||||
auth-username-form=Username
|
||||
auth-username-password-form=Username and password
|
||||
webauthn-authenticator=WebAuthn
|
||||
webauthn-authenticator-passwordless=WebAuthn Passwordless
|
||||
otp-display-name=Authenticator Application
|
||||
otp-help-text=Enter a verification code from authenticator application.
|
||||
password-help-text=Log in by entering your password.
|
||||
auth-username-form-display-name=Username
|
||||
auth-username-form-help-text=Start log in by entering your username
|
||||
auth-username-password-form-display-name=Username and password
|
||||
auth-username-password-form-help-text=Log in by entering your username and password.
|
||||
webauthn-display-name=Security Key
|
||||
webauthn-help-text=Use your security key to log in.
|
||||
webauthn-passwordless-display-name=Security Key
|
||||
webauthn-passwordless-help-text=Use your security key for passwordless log in.
|
||||
identity-provider-redirector=Connect with another Identity Provider
|
||||
|
||||
|
|
|
@ -2,34 +2,41 @@
|
|||
<@layout.registrationLayout displayInfo=true; section>
|
||||
<#if section = "header" || section = "show-username">
|
||||
<script type="text/javascript">
|
||||
// Fill up the two hidden and submit the form
|
||||
function fillAndSubmit() {
|
||||
document.getElementById('authexec-hidden-input').value = document.getElementById('authenticators-choice').value;
|
||||
function fillAndSubmit(authExecId) {
|
||||
document.getElementById('authexec-hidden-input').value = authExecId;
|
||||
document.getElementById('kc-select-credential-form').submit();
|
||||
}
|
||||
<#if auth.authenticationSelections?size gt 1>
|
||||
// We bind the action to the select
|
||||
window.addEventListener('load', function() {
|
||||
document.getElementById('authenticators-choice').addEventListener('change', fillAndSubmit);
|
||||
});
|
||||
</#if>
|
||||
</script>
|
||||
<#if section = "header">
|
||||
${msg("loginChooseAuthenticator")}
|
||||
</#if>
|
||||
<#elseif section = "form">
|
||||
|
||||
<form id="kc-select-credential-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<div class="${properties.kcLabelWrapperClass!}">
|
||||
<label for="authenticators-choice" class="${properties.kcLabelClass!}">${msg("loginCredential")}</label>
|
||||
</div>
|
||||
<div class="${properties.kcInputWrapperClass!}">
|
||||
<select id="authenticators-choice" class="form-control" size="1">
|
||||
<#list auth.authenticationSelections as authenticationSelection>
|
||||
<option value="${authenticationSelection.authExecId}" <#if authenticationSelection.authExecId == execution>selected</#if>>${msg('${authenticationSelection.authExecDisplayName}')}</option>
|
||||
</#list>
|
||||
</select>
|
||||
<input type="hidden" id="authexec-hidden-input" name="authenticationExecution" />
|
||||
</div>
|
||||
<div class="${properties.kcSelectAuthListClass!}">
|
||||
<#list auth.authenticationSelections as authenticationSelection>
|
||||
<div class="${properties.kcSelectAuthListItemClass!}">
|
||||
<div class="${properties.kcSelectAuthListItemInfoClass!}" onclick="fillAndSubmit('${authenticationSelection.authExecId}')">
|
||||
<div class="${properties.kcSelectAuthListItemLeftClass!}">
|
||||
<span class="${properties['${authenticationSelection.iconCssClass}']!authenticationSelection.iconCssClass}"></span>
|
||||
</div>
|
||||
<div class="${properties.kcSelectAuthListItemBodyClass!}">
|
||||
<div class="${properties.kcSelectAuthListItemDescriptionClass!}">
|
||||
<div class="${properties.kcSelectAuthListItemHeadingClass!}">
|
||||
${msg('${authenticationSelection.displayName}')}
|
||||
</div>
|
||||
<div class="${properties.kcSelectAuthListItemHelpTextClass!}">
|
||||
${msg('${authenticationSelection.helpText}')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</#list>
|
||||
<input type="hidden" id="authexec-hidden-input" name="authenticationExecution" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
|
||||
|
|
|
@ -56,26 +56,22 @@
|
|||
<h1 id="kc-page-title"><#nested "header"></h1>
|
||||
<#else>
|
||||
<#nested "show-username">
|
||||
<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>
|
||||
</#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??)>
|
||||
|
|
|
@ -68,12 +68,12 @@ notSetUp={0} is not set up.
|
|||
two-factor=Two-Factor Authentication
|
||||
passwordless=Passwordless
|
||||
unknown=Unknown
|
||||
otp=One Time Password
|
||||
webauthn=WebAuthn
|
||||
webauthn-passwordless=WebAuthn Passwordless
|
||||
otpHelptext=A one-time password (OTP), is a password that is valid for only one login session.
|
||||
webauthnHelptext=WebAuthn lets web applications authenticate users without storing their passwords on servers.
|
||||
webauthn-passwordlessHelptext=I need help with this help text. Any suggestions?
|
||||
otp-display-name=Authenticator Application
|
||||
otp-help-text=Enter a verification code from authenticator application.
|
||||
webauthn-display-name=Security Key
|
||||
webauthn-help-text=Use your security key to log in.
|
||||
webauthn-passwordless-display-name=Security Key
|
||||
webauthn-passwordless-help-text=Use your security key for passwordless log in.
|
||||
|
||||
# Applications page
|
||||
applicationsPageTitle=Applications
|
||||
|
|
|
@ -65,9 +65,11 @@ interface UserCredential {
|
|||
interface CredentialContainer {
|
||||
category: CredCategory;
|
||||
type: CredType;
|
||||
displayName: string;
|
||||
helptext?: string;
|
||||
createAction: string;
|
||||
updateAction: string;
|
||||
iconCssClass: string;
|
||||
removeable: boolean;
|
||||
userCredentials: UserCredential[];
|
||||
}
|
||||
|
@ -176,17 +178,17 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
|
|||
const userCredentials: UserCredential[] = credTypeMap.get(credType)!.userCredentials;
|
||||
const removeable: boolean = credTypeMap.get(credType)!.removeable;
|
||||
const updateAction: string = credTypeMap.get(credType)!.updateAction;
|
||||
const type: string = credTypeMap.get(credType)!.type;
|
||||
const displayName: string = credTypeMap.get(credType)!.displayName;
|
||||
|
||||
if (userCredentials.length === 0) {
|
||||
const localizedType = Msg.localize(type);
|
||||
const localizedDisplayName = Msg.localize(displayName);
|
||||
return (
|
||||
<DataListItem aria-labelledby='no-credentials-list-item'>
|
||||
<DataListItemRow key='no-credentials-list-item-row'>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell/>,
|
||||
<strong><Msg msgKey='notSetUp' params={[localizedType]}/></strong>,
|
||||
<strong><Msg msgKey='notSetUp' params={[localizedDisplayName]}/></strong>,
|
||||
<DataListCell/>
|
||||
]}/>
|
||||
</DataListItemRow>
|
||||
|
@ -229,7 +231,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
|
|||
if (!credContainer.createAction) return;
|
||||
|
||||
const setupAction: AIACommand = new AIACommand(credContainer.createAction, this.props.location.pathname);
|
||||
const credContainerType: string = Msg.localize(credContainer.type);
|
||||
const credContainerDisplayName: string = Msg.localize(credContainer.displayName);
|
||||
|
||||
return (
|
||||
<DataListItem aria-labelledby={'type-datalistitem-' + credContainer.type}>
|
||||
|
@ -238,7 +240,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
|
|||
dataListCells={[
|
||||
<DataListCell width={5} key={'credTypeTitle-' + credContainer.type}>
|
||||
<Title headingLevel={TitleLevel.h3} size='2xl'>
|
||||
<strong><Msg msgKey={credContainer.type}/></strong>
|
||||
<strong><Msg msgKey={credContainer.displayName}/></strong>
|
||||
</Title>
|
||||
<Msg msgKey={credContainer.helptext}/>
|
||||
</DataListCell>,
|
||||
|
@ -249,7 +251,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
|
|||
<span className="pf-c-button__icon">
|
||||
<i className="fas fa-plus-circle" aria-hidden="true"></i>
|
||||
</span>
|
||||
<Msg msgKey='setUpNew' params={[credContainerType]}/>
|
||||
<Msg msgKey='setUpNew' params={[credContainerDisplayName]}/>
|
||||
</button>
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
|
|
|
@ -493,6 +493,14 @@ a.zocial {
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
.login-pf-page .list-view-pf .list-group-item {
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.login-pf-page .list-view-pf-description {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#kc-form-login div.form-group:last-of-type,
|
||||
#kc-register-form div.form-group:last-of-type,
|
||||
#kc-update-profile-form div.form-group:last-of-type {
|
||||
|
|
|
@ -68,3 +68,20 @@ kcInputLargeClass=input-lg
|
|||
|
||||
##### css classes for form accessability
|
||||
kcSrOnlyClass=sr-only
|
||||
|
||||
##### css classes for select-authenticator form
|
||||
kcSelectAuthListClass=list-group list-view-pf
|
||||
kcSelectAuthListItemClass=list-group-item list-view-pf-stacked
|
||||
kcSelectAuthListItemInfoClass=list-view-pf-main-info
|
||||
kcSelectAuthListItemLeftClass=list-view-pf-left
|
||||
kcSelectAuthListItemBodyClass=list-view-pf-body
|
||||
kcSelectAuthListItemDescriptionClass=list-view-pf-description
|
||||
kcSelectAuthListItemHeadingClass=list-group-item-heading
|
||||
kcSelectAuthListItemHelpTextClass=list-group-item-text
|
||||
|
||||
##### css classes for the authenticators
|
||||
kcAuthenticatorDefaultClass=fa list-view-pf-icon-lg
|
||||
kcAuthenticatorPasswordClass=fa fa-unlock list-view-pf-icon-lg
|
||||
kcAuthenticatorOTPClass=fa fa-mobile list-view-pf-icon-lg
|
||||
kcAuthenticatorWebAuthnClass=fa fa-key list-view-pf-icon-lg
|
||||
kcAuthenticatorWebAuthnPasswordlessClass=fa fa-key list-view-pf-icon-lg
|
||||
|
|
Loading…
Reference in a new issue