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;
|
package org.keycloak.authentication;
|
||||||
|
|
||||||
|
import org.keycloak.credential.CredentialProvider;
|
||||||
|
import org.keycloak.credential.CredentialTypeMetadata;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
public class AuthenticationSelectionOption {
|
public class AuthenticationSelectionOption {
|
||||||
|
|
||||||
private final KeycloakSession session;
|
|
||||||
private final AuthenticationExecutionModel authExec;
|
private final AuthenticationExecutionModel authExec;
|
||||||
|
private final CredentialTypeMetadata credentialTypeMetadata;
|
||||||
|
|
||||||
public AuthenticationSelectionOption(KeycloakSession session, AuthenticationExecutionModel authExec) {
|
public AuthenticationSelectionOption(KeycloakSession session, AuthenticationExecutionModel authExec) {
|
||||||
this.session = session;
|
|
||||||
this.authExec = authExec;
|
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();
|
return authExec.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAuthExecName() {
|
public String getDisplayName() {
|
||||||
return authExec.getAuthenticator();
|
return credentialTypeMetadata == null ? authExec.getAuthenticator() + "-display-name" : credentialTypeMetadata.getDisplayName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAuthExecDisplayName() {
|
public String getHelpText() {
|
||||||
// TODO: Retrieve the displayName for the authenticator from the AuthenticationFactory
|
return credentialTypeMetadata == null ? authExec.getAuthenticator() + "-help-text" : credentialTypeMetadata.getHelpText();
|
||||||
// TODO: Retrieve icon CSS style
|
}
|
||||||
// TODO: Should be addressed as part of https://issues.redhat.com/browse/KEYCLOAK-12185
|
|
||||||
return getAuthExecName();
|
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
|
* @return
|
||||||
*/
|
*/
|
||||||
boolean isUserSetupAllowed();
|
boolean isUserSetupAllowed();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,4 +49,6 @@ public interface CredentialProvider<T extends CredentialModel> extends Provider
|
||||||
}
|
}
|
||||||
return getCredentialFromModel(models.get(0));
|
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.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -37,7 +38,7 @@ public class WebAuthnAuthenticatorFactory implements AuthenticatorFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getReferenceCategory() {
|
public String getReferenceCategory() {
|
||||||
return "auth";
|
return WebAuthnCredentialModel.TYPE_TWOFACTOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @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";
|
public static final String PROVIDER_ID = "webauthn-authenticator-passwordless";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getReferenceCategory() {
|
||||||
|
return WebAuthnCredentialModel.TYPE_PASSWORDLESS;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDisplayType() {
|
public String getDisplayType() {
|
||||||
return "WebAuthn Passwordless Authenticator";
|
return "WebAuthn Passwordless Authenticator";
|
||||||
|
|
|
@ -27,6 +27,7 @@ import javax.ws.rs.core.Response;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.WebAuthnConstants;
|
import org.keycloak.WebAuthnConstants;
|
||||||
import org.keycloak.authentication.CredentialRegistrator;
|
import org.keycloak.authentication.CredentialRegistrator;
|
||||||
|
import org.keycloak.authentication.InitiatedActionSupport;
|
||||||
import org.keycloak.authentication.RequiredActionContext;
|
import org.keycloak.authentication.RequiredActionContext;
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
@ -82,6 +83,11 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
||||||
this.certPathtrustValidator = certPathtrustValidator;
|
this.certPathtrustValidator = certPathtrustValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InitiatedActionSupport initiatedActionSupport() {
|
||||||
|
return InitiatedActionSupport.SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requiredActionChallenge(RequiredActionContext context) {
|
public void requiredActionChallenge(RequiredActionContext context) {
|
||||||
UserModel userModel = context.getUser();
|
UserModel userModel = context.getUser();
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.credential;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.models.RequiredActionProviderModel;
|
||||||
import org.keycloak.models.credential.OTPCredentialModel;
|
import org.keycloak.models.credential.OTPCredentialModel;
|
||||||
import org.keycloak.models.credential.dto.OTPCredentialData;
|
import org.keycloak.models.credential.dto.OTPCredentialData;
|
||||||
import org.keycloak.models.credential.dto.OTPSecretData;
|
import org.keycloak.models.credential.dto.OTPSecretData;
|
||||||
|
@ -134,4 +135,17 @@ public class OTPCredentialProvider implements CredentialProvider<OTPCredentialMo
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return OTPCredentialModel.TYPE;
|
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;
|
package org.keycloak.credential;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.credential.hash.PasswordHashProvider;
|
import org.keycloak.credential.hash.PasswordHashProvider;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
|
@ -293,4 +294,17 @@ public class PasswordCredentialProvider implements CredentialProvider<PasswordCr
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return PasswordCredentialModel.TYPE;
|
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 java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.KeycloakSession;
|
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;
|
package org.keycloak.credential;
|
||||||
|
|
||||||
import com.webauthn4j.converter.util.CborConverter;
|
import com.webauthn4j.converter.util.CborConverter;
|
||||||
|
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||||
|
|
||||||
|
@ -37,4 +38,17 @@ public class WebAuthnPasswordlessCredentialProvider extends WebAuthnCredentialPr
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return WebAuthnCredentialModel.TYPE_PASSWORDLESS;
|
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;
|
package org.keycloak.services.resources.account;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.keycloak.authentication.CredentialRegistrator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
import org.keycloak.credential.CredentialModel;
|
import org.keycloak.credential.CredentialModel;
|
||||||
import org.keycloak.credential.CredentialProvider;
|
import org.keycloak.credential.CredentialProvider;
|
||||||
|
import org.keycloak.credential.CredentialTypeMetadata;
|
||||||
import org.keycloak.credential.PasswordCredentialProvider;
|
import org.keycloak.credential.PasswordCredentialProvider;
|
||||||
import org.keycloak.credential.PasswordCredentialProviderFactory;
|
import org.keycloak.credential.PasswordCredentialProviderFactory;
|
||||||
|
import org.keycloak.credential.UserCredentialStoreManager;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.*;
|
||||||
|
@ -25,13 +28,25 @@ import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.DISABLED;
|
||||||
|
|
||||||
public class AccountCredentialResource {
|
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 KeycloakSession session;
|
||||||
private final EventBuilder event;
|
private final EventBuilder event;
|
||||||
private final UserModel user;
|
private final UserModel user;
|
||||||
|
@ -46,42 +61,38 @@ public class AccountCredentialResource {
|
||||||
realm = session.getContext().getRealm();
|
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
|
public static class CredentialContainer {
|
||||||
// @GET
|
// ** category, displayName and helptext attributes can be ordinary UI text or a key into
|
||||||
// @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
|
|
||||||
// a localized message bundle. Typically, it will be a key, but
|
// a localized message bundle. Typically, it will be a key, but
|
||||||
// the UI will work just fine if you don't care about localization
|
// the UI will work just fine if you don't care about localization
|
||||||
// and you want to just send UI text.
|
// and you want to just send UI text.
|
||||||
//
|
//
|
||||||
// Also, the ${} shown in Apicurio is not needed.
|
// Also, the ${} shown in Apicurio is not needed.
|
||||||
|
private String type;
|
||||||
private String category; // **
|
private String category; // **
|
||||||
private String type; // **
|
private String displayName;
|
||||||
private String helptext; // **
|
private String helptext; // **
|
||||||
private boolean enabled;
|
private String iconCssClass;
|
||||||
private String createAction;
|
private String createAction;
|
||||||
private String updateAction;
|
private String updateAction;
|
||||||
private boolean removeable;
|
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;
|
public CredentialContainer() {
|
||||||
this.type = type;
|
}
|
||||||
this.helptext = helptext;
|
|
||||||
this.enabled = enabled;
|
public CredentialContainer(CredentialTypeMetadata metadata, List<CredentialRepresentation> userCredentials) {
|
||||||
this.createAction = createAction;
|
this.metadata = metadata;
|
||||||
this.updateAction = updateAction;
|
this.type = metadata.getType();
|
||||||
this.removeable = removeable;
|
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;
|
this.userCredentials = userCredentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,12 +104,16 @@ public class AccountCredentialResource {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
public String getHelptext() {
|
public String getHelptext() {
|
||||||
return helptext;
|
return helptext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public String getIconCssClass() {
|
||||||
return enabled;
|
return iconCssClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCreateAction() {
|
public String getCreateAction() {
|
||||||
|
@ -113,107 +128,127 @@ public class AccountCredentialResource {
|
||||||
return removeable;
|
return removeable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CredentialModel> getUserCredentials() {
|
public List<CredentialRepresentation> getUserCredentials() {
|
||||||
return userCredentials;
|
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
|
@GET
|
||||||
@NoCache
|
@NoCache
|
||||||
@Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
|
@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);
|
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||||
List<CredentialModel> models = session.userCredentialManager().getStoredCredentials(realm, user);
|
|
||||||
|
boolean filterUserCredentials = userCredentials != null && !userCredentials;
|
||||||
List<CredentialModel> passwordUserCreds = new java.util.ArrayList<>();
|
|
||||||
passwordUserCreds.add(models.get(0));
|
List<CredentialContainer> credentialTypes = new LinkedList<>();
|
||||||
|
List<CredentialProvider> credentialProviders = UserCredentialStoreManager.getCredentialProviders(session, realm, CredentialProvider.class);
|
||||||
List<CredentialModel> otpUserCreds = new java.util.ArrayList<>();
|
Set<String> enabledCredentialTypes = getEnabledCredentialTypes(credentialProviders);
|
||||||
if (models.size() > 1) otpUserCreds.add(models.get(1));
|
|
||||||
if (models.size() > 2) otpUserCreds.add(models.get(2));
|
List<CredentialModel> models = filterUserCredentials ? null : session.userCredentialManager().getStoredCredentials(realm, user);
|
||||||
|
|
||||||
List<CredentialModel> webauthnUserCreds = new java.util.ArrayList<>();
|
// Don't return secrets from REST endpoint
|
||||||
CredentialModel webauthnCred = new CredentialModel();
|
if (models != null) {
|
||||||
webauthnCred.setId("bogus-id");
|
for (CredentialModel credential : models) {
|
||||||
webauthnCred.setUserLabel("yubikey key");
|
credential.setSecretData(null);
|
||||||
webauthnCred.setCreatedDate(1579122652382L);
|
}
|
||||||
webauthnUserCreds.add(webauthnCred);
|
}
|
||||||
|
|
||||||
List<CredentialModel> webauthnStrongUserCreds = new java.util.ArrayList<>();
|
for (CredentialProvider credentialProvider : credentialProviders) {
|
||||||
CredentialModel webauthnStrongCred = new CredentialModel();
|
String credentialProviderType = credentialProvider.getType();
|
||||||
webauthnStrongCred.setId("bogus-id-for-webauthnStrong");
|
|
||||||
webauthnStrongCred.setUserLabel("My very strong key with required PIN");
|
// Filter just by single type
|
||||||
webauthnStrongCred.setCreatedDate(1579122652382L);
|
if (type != null && !type.equals(credentialProviderType)) {
|
||||||
webauthnUserCreds.add(webauthnStrongCred);
|
continue;
|
||||||
|
}
|
||||||
CredentialContainer password = new CredentialContainer(
|
|
||||||
"password",
|
boolean enabled = enabledCredentialTypes.contains(credentialProviderType);
|
||||||
"password",
|
|
||||||
"passwordHelptext",
|
// Filter disabled credential types
|
||||||
true,
|
if (!enabled) {
|
||||||
null, // no create action
|
continue;
|
||||||
"UPDATE_PASSWORD",
|
}
|
||||||
false,
|
|
||||||
passwordUserCreds
|
CredentialTypeMetadata metadata = credentialProvider.getCredentialTypeMetadata();
|
||||||
);
|
|
||||||
CredentialContainer otp = new CredentialContainer(
|
List<CredentialRepresentation> userCredentialModels = filterUserCredentials ? null : models.stream()
|
||||||
"two-factor",
|
.filter(credentialModel -> credentialProvider.getType().equals(credentialModel.getType()))
|
||||||
"otp",
|
.map(ModelToRepresentation::toRepresentation)
|
||||||
"otpHelptext",
|
.collect(Collectors.toList());
|
||||||
true,
|
|
||||||
"CONFIGURE_TOTP",
|
CredentialContainer credType = new CredentialContainer(metadata, userCredentialModels);
|
||||||
null, // no update action
|
credentialTypes.add(credType);
|
||||||
true,
|
}
|
||||||
otpUserCreds
|
|
||||||
);
|
credentialTypes.sort(Comparator.comparing(CredentialContainer::getMetadata));
|
||||||
CredentialContainer webAuthn = new CredentialContainer(
|
|
||||||
"two-factor",
|
return credentialTypes;
|
||||||
"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
|
|
||||||
);
|
|
||||||
|
|
||||||
List<CredentialContainer> dummyCreds = new java.util.ArrayList<>();
|
|
||||||
dummyCreds.add(password);
|
|
||||||
dummyCreds.add(otp);
|
|
||||||
dummyCreds.add(webAuthn);
|
|
||||||
dummyCreds.add(passwordless);
|
|
||||||
|
|
||||||
return dummyCreds;
|
|
||||||
}
|
}
|
||||||
//
|
|
||||||
//
|
// Going through all authentication flows and their authentication executions to see if there is any authenticator of the corresponding
|
||||||
// @GET
|
// credential type.
|
||||||
// @Path("registrators")
|
private Set<String> getEnabledCredentialTypes(List<CredentialProvider> credentialProviders) {
|
||||||
// @NoCache
|
Set<String> enabledCredentialTypes = new HashSet<>();
|
||||||
// @Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
|
|
||||||
// public List<String> getCredentialRegistrators(){
|
for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) {
|
||||||
// auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
// Ignore DISABLED executions and flows
|
||||||
//
|
if (isFlowEffectivelyDisabled(flow)) continue;
|
||||||
// return session.getContext().getRealm().getRequiredActionProviders().stream()
|
|
||||||
// .map(RequiredActionProviderModel::getProviderId)
|
for (AuthenticationExecutionModel execution : realm.getAuthenticationExecutions(flow.getId())) {
|
||||||
// .filter(providerId -> session.getProvider(RequiredActionProvider.class, providerId) instanceof CredentialRegistrator)
|
if (execution.getAuthenticator() != null && DISABLED != execution.getRequirement()) {
|
||||||
// .collect(Collectors.toList());
|
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}")
|
@Path("{credentialId}")
|
||||||
@DELETE
|
@DELETE
|
||||||
|
@ -222,18 +257,23 @@ public class AccountCredentialResource {
|
||||||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||||
session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
|
session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
|
||||||
}
|
}
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Update a credential label for a user
|
/**
|
||||||
// */
|
* Update a user label of specified credential of current user
|
||||||
// @PUT
|
*
|
||||||
// @Consumes(javax.ws.rs.core.MediaType.TEXT_PLAIN)
|
* @param credentialId ID of the credential, which will be updated
|
||||||
// @Path("{credentialId}/label")
|
* @param userLabel new user label
|
||||||
// public void setLabel(final @PathParam("credentialId") String credentialId, String userLabel) {
|
*/
|
||||||
// auth.require(AccountRoles.MANAGE_ACCOUNT);
|
@PUT
|
||||||
// session.userCredentialManager().updateCredentialLabel(realm, user, credentialId, userLabel);
|
@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
|
// * Move a credential to a position behind another credential
|
||||||
// * @param credentialId The credential to move
|
// * @param credentialId The credential to move
|
||||||
|
|
|
@ -45,7 +45,6 @@ public abstract class LanguageComboboxAwarePage extends AbstractPage {
|
||||||
@FindBy(id = "kc-attempted-username")
|
@FindBy(id = "kc-attempted-username")
|
||||||
private WebElement attemptedUsernameLabel;
|
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")
|
@FindBy(id = "reset-login")
|
||||||
private WebElement resetLoginLink;
|
private WebElement resetLoginLink;
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.NoSuchElementException;
|
import org.openqa.selenium.NoSuchElementException;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
import org.openqa.selenium.support.FindBy;
|
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...)
|
* 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 {
|
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")
|
@FindBy(id = "authenticators-choice")
|
||||||
private WebElement authenticatorsSelect;
|
private WebElement authenticatorsSelect;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names like for example [ "Password", "Authenticator Application", "Security Key" ]
|
||||||
|
*/
|
||||||
public List<String> getAvailableLoginMethods() {
|
public List<String> getAvailableLoginMethods() {
|
||||||
return new Select(authenticatorsSelect).getOptions()
|
List<WebElement> rows = getLoginMethodsRows();
|
||||||
.stream()
|
|
||||||
.map(WebElement::getText)
|
return rows.stream()
|
||||||
|
.map(this::getLoginMethodNameFromRow)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public String getSelectedLoginMethod() {
|
/**
|
||||||
return new Select(authenticatorsSelect).getOptions()
|
*
|
||||||
.stream()
|
* Selects the chosen login method (For example "Password") by click on it.
|
||||||
.filter(webElement -> webElement.getAttribute("selected") != null)
|
*
|
||||||
.findFirst()
|
* @param loginMethodName name as displayed. For example "Password" or "Authenticator Application"
|
||||||
.orElseThrow(() -> new AssertionError("Selected login method not found"))
|
*
|
||||||
.getText();
|
*/
|
||||||
|
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) {
|
private List<WebElement> getLoginMethodsRows() {
|
||||||
new Select(authenticatorsSelect).selectByVisibleText(loginMethod);
|
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
|
// Check the authenticators-choice available
|
||||||
try {
|
try {
|
||||||
driver.findElement(By.id("authenticators-choice"));
|
driver.findElement(By.id("kc-select-credential-form"));
|
||||||
} catch (NoSuchElementException nfe) {
|
} catch (NoSuchElementException nfe) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,17 +20,37 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
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.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.ClientRepresentation;
|
||||||
import org.keycloak.representations.account.ConsentRepresentation;
|
import org.keycloak.representations.account.ConsentRepresentation;
|
||||||
import org.keycloak.representations.account.ConsentScopeRepresentation;
|
import org.keycloak.representations.account.ConsentScopeRepresentation;
|
||||||
import org.keycloak.representations.account.SessionRepresentation;
|
import org.keycloak.representations.account.SessionRepresentation;
|
||||||
import org.keycloak.representations.account.UserRepresentation;
|
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.ClientScopeRepresentation;
|
||||||
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
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.messages.Messages;
|
||||||
import org.keycloak.services.resources.account.AccountCredentialResource;
|
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.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.TokenUtil;
|
import org.keycloak.testsuite.util.TokenUtil;
|
||||||
|
|
||||||
|
@ -41,11 +61,14 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
import org.keycloak.services.resources.account.AccountCredentialResource.PasswordUpdate;
|
import org.keycloak.services.resources.account.AccountCredentialResource.PasswordUpdate;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||||
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -277,6 +300,198 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
assertEquals(expectedStatus, status);
|
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 {
|
public void testDeleteSessions() throws IOException {
|
||||||
TokenUtil viewToken = new TokenUtil("view-account-access", "password");
|
TokenUtil viewToken = new TokenUtil("view-account-access", "password");
|
||||||
oauth.doLogin("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
|
// Just click "Try another way" to verify that both Password and OTP are available. But go back to Password then
|
||||||
passwordPage.clickTryAnotherWayLink();
|
passwordPage.clickTryAnotherWayLink();
|
||||||
selectAuthenticatorPage.assertCurrent();
|
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(SelectAuthenticatorPage.PASSWORD);
|
||||||
selectAuthenticatorPage.selectLoginMethod("OTP");
|
|
||||||
loginTotpPage.clickTryAnotherWayLink();
|
|
||||||
selectAuthenticatorPage.selectLoginMethod("Password");
|
|
||||||
|
|
||||||
// Login with password
|
// Login with password
|
||||||
Assert.assertTrue(passwordPage.isCurrent("consumer"));
|
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
|
// Click "Try another way", Select OTP and assert OTP form present
|
||||||
passwordPage.clickTryAnotherWayLink();
|
passwordPage.clickTryAnotherWayLink();
|
||||||
selectAuthenticatorPage.assertCurrent();
|
selectAuthenticatorPage.assertCurrent();
|
||||||
selectAuthenticatorPage.selectLoginMethod("OTP");
|
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION);
|
||||||
|
|
||||||
loginTotpPage.assertCurrent();
|
loginTotpPage.assertCurrent();
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,6 @@ import org.keycloak.testsuite.pages.PasswordPage;
|
||||||
import org.keycloak.testsuite.pages.SelectAuthenticatorPage;
|
import org.keycloak.testsuite.pages.SelectAuthenticatorPage;
|
||||||
import org.keycloak.testsuite.util.FlowUtil;
|
import org.keycloak.testsuite.util.FlowUtil;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.WaitUtils;
|
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||||
|
@ -101,14 +100,6 @@ public class MultiFactorAuthenticationTest extends AbstractTestRealmKeycloakTest
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void importTestRealm(Consumer<RealmRepresentation> realmUpdater) {
|
|
||||||
RealmRepresentation realm = loadTestRealm();
|
|
||||||
if (realmUpdater != null) {
|
|
||||||
realmUpdater.accept(realm);
|
|
||||||
}
|
|
||||||
importRealm(realm);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
log.debug("Adding test realm for import from testrealm.json");
|
log.debug("Adding test realm for import from testrealm.json");
|
||||||
|
@ -138,10 +129,14 @@ public class MultiFactorAuthenticationTest extends AbstractTestRealmKeycloakTest
|
||||||
passwordPage.clickTryAnotherWayLink();
|
passwordPage.clickTryAnotherWayLink();
|
||||||
|
|
||||||
selectAuthenticatorPage.assertCurrent();
|
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
|
// Select OTP and see that just single OTP is available for this user
|
||||||
selectAuthenticatorPage.selectLoginMethod("OTP");
|
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION);
|
||||||
loginTotpPage.assertCurrent();
|
loginTotpPage.assertCurrent();
|
||||||
loginTotpPage.assertTryAnotherWayLinkAvailability(true);
|
loginTotpPage.assertTryAnotherWayLinkAvailability(true);
|
||||||
loginTotpPage.assertOtpCredentialSelectorAvailability(false);
|
loginTotpPage.assertOtpCredentialSelectorAvailability(false);
|
||||||
|
@ -159,7 +154,7 @@ public class MultiFactorAuthenticationTest extends AbstractTestRealmKeycloakTest
|
||||||
loginTotpPage.clickTryAnotherWayLink();
|
loginTotpPage.clickTryAnotherWayLink();
|
||||||
|
|
||||||
selectAuthenticatorPage.assertCurrent();
|
selectAuthenticatorPage.assertCurrent();
|
||||||
Assert.assertEquals(Arrays.asList("OTP", "Password"), selectAuthenticatorPage.getAvailableLoginMethods());
|
Assert.assertEquals(Arrays.asList(SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION, SelectAuthenticatorPage.PASSWORD), selectAuthenticatorPage.getAvailableLoginMethods());
|
||||||
} finally {
|
} finally {
|
||||||
BrowserFlowTest.revertFlows(testRealm(), "browser - alternative");
|
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
|
// Click "Try another way" . Ability to have both password and OTP should be possible even if OTP is in different subflow
|
||||||
passwordPage.clickTryAnotherWayLink();
|
passwordPage.clickTryAnotherWayLink();
|
||||||
selectAuthenticatorPage.assertCurrent();
|
selectAuthenticatorPage.assertCurrent();
|
||||||
Assert.assertEquals(Arrays.asList("Password", "OTP"), selectAuthenticatorPage.getAvailableLoginMethods());
|
Assert.assertEquals(Arrays.asList(SelectAuthenticatorPage.PASSWORD, SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION), selectAuthenticatorPage.getAvailableLoginMethods());
|
||||||
selectAuthenticatorPage.selectLoginMethod("OTP");
|
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.AUTHENTICATOR_APPLICATION);
|
||||||
|
|
||||||
// Should be on the OTP now. Click "Try another way" again. Should see again both Password and OTP
|
// Should be on the OTP now. Click "Try another way" again. Should see again both Password and OTP
|
||||||
loginTotpPage.assertCurrent();
|
loginTotpPage.assertCurrent();
|
||||||
|
@ -227,9 +222,9 @@ public class MultiFactorAuthenticationTest extends AbstractTestRealmKeycloakTest
|
||||||
|
|
||||||
loginTotpPage.clickTryAnotherWayLink();
|
loginTotpPage.clickTryAnotherWayLink();
|
||||||
selectAuthenticatorPage.assertCurrent();
|
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.assertCurrent();
|
||||||
passwordPage.login("password");
|
passwordPage.login("password");
|
||||||
|
|
||||||
|
|
|
@ -104,7 +104,7 @@ loginTotpCounter=Counter
|
||||||
loginTotp.totp=Time-based
|
loginTotp.totp=Time-based
|
||||||
loginTotp.hotp=Counter-based
|
loginTotp.hotp=Counter-based
|
||||||
|
|
||||||
loginChooseAuthenticator=Select your authentication method
|
loginChooseAuthenticator=Select login method
|
||||||
|
|
||||||
oauthGrantRequest=Do you grant these access privileges?
|
oauthGrantRequest=Do you grant these access privileges?
|
||||||
inResource=in
|
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.
|
saml.post-form.js-disabled=JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.
|
||||||
|
|
||||||
#authenticators
|
#authenticators
|
||||||
auth-otp-form=OTP
|
otp-display-name=Authenticator Application
|
||||||
auth-password-form=Password
|
otp-help-text=Enter a verification code from authenticator application.
|
||||||
auth-username-form=Username
|
password-help-text=Log in by entering your password.
|
||||||
auth-username-password-form=Username and password
|
auth-username-form-display-name=Username
|
||||||
webauthn-authenticator=WebAuthn
|
auth-username-form-help-text=Start log in by entering your username
|
||||||
webauthn-authenticator-passwordless=WebAuthn Passwordless
|
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
|
identity-provider-redirector=Connect with another Identity Provider
|
||||||
|
|
||||||
|
|
|
@ -2,34 +2,41 @@
|
||||||
<@layout.registrationLayout displayInfo=true; section>
|
<@layout.registrationLayout displayInfo=true; section>
|
||||||
<#if section = "header" || section = "show-username">
|
<#if section = "header" || section = "show-username">
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
// Fill up the two hidden and submit the form
|
function fillAndSubmit(authExecId) {
|
||||||
function fillAndSubmit() {
|
document.getElementById('authexec-hidden-input').value = authExecId;
|
||||||
document.getElementById('authexec-hidden-input').value = document.getElementById('authenticators-choice').value;
|
|
||||||
document.getElementById('kc-select-credential-form').submit();
|
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>
|
</script>
|
||||||
|
<#if section = "header">
|
||||||
|
${msg("loginChooseAuthenticator")}
|
||||||
|
</#if>
|
||||||
<#elseif section = "form">
|
<#elseif section = "form">
|
||||||
|
|
||||||
<form id="kc-select-credential-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
<form id="kc-select-credential-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
||||||
<div class="${properties.kcFormGroupClass!}">
|
<div class="${properties.kcSelectAuthListClass!}">
|
||||||
<div class="${properties.kcLabelWrapperClass!}">
|
<#list auth.authenticationSelections as authenticationSelection>
|
||||||
<label for="authenticators-choice" class="${properties.kcLabelClass!}">${msg("loginCredential")}</label>
|
<div class="${properties.kcSelectAuthListItemClass!}">
|
||||||
</div>
|
<div class="${properties.kcSelectAuthListItemInfoClass!}" onclick="fillAndSubmit('${authenticationSelection.authExecId}')">
|
||||||
<div class="${properties.kcInputWrapperClass!}">
|
<div class="${properties.kcSelectAuthListItemLeftClass!}">
|
||||||
<select id="authenticators-choice" class="form-control" size="1">
|
<span class="${properties['${authenticationSelection.iconCssClass}']!authenticationSelection.iconCssClass}"></span>
|
||||||
<#list auth.authenticationSelections as authenticationSelection>
|
</div>
|
||||||
<option value="${authenticationSelection.authExecId}" <#if authenticationSelection.authExecId == execution>selected</#if>>${msg('${authenticationSelection.authExecDisplayName}')}</option>
|
<div class="${properties.kcSelectAuthListItemBodyClass!}">
|
||||||
</#list>
|
<div class="${properties.kcSelectAuthListItemDescriptionClass!}">
|
||||||
</select>
|
<div class="${properties.kcSelectAuthListItemHeadingClass!}">
|
||||||
<input type="hidden" id="authexec-hidden-input" name="authenticationExecution" />
|
${msg('${authenticationSelection.displayName}')}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="${properties.kcSelectAuthListItemHelpTextClass!}">
|
||||||
|
${msg('${authenticationSelection.helpText}')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</#list>
|
||||||
|
<input type="hidden" id="authexec-hidden-input" name="authenticationExecution" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</#if>
|
</#if>
|
||||||
</@layout.registrationLayout>
|
</@layout.registrationLayout>
|
||||||
|
|
||||||
|
|
|
@ -56,26 +56,22 @@
|
||||||
<h1 id="kc-page-title"><#nested "header"></h1>
|
<h1 id="kc-page-title"><#nested "header"></h1>
|
||||||
<#else>
|
<#else>
|
||||||
<#nested "show-username">
|
<#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>
|
</#if>
|
||||||
</header>
|
</header>
|
||||||
<div id="kc-content">
|
<div id="kc-content">
|
||||||
<div id="kc-content-wrapper">
|
<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 -->
|
<#-- App-initiated actions should not see warning messages about the need to complete the action -->
|
||||||
<#-- during login. -->
|
<#-- during login. -->
|
||||||
<#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
|
<#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
|
||||||
|
|
|
@ -68,12 +68,12 @@ notSetUp={0} is not set up.
|
||||||
two-factor=Two-Factor Authentication
|
two-factor=Two-Factor Authentication
|
||||||
passwordless=Passwordless
|
passwordless=Passwordless
|
||||||
unknown=Unknown
|
unknown=Unknown
|
||||||
otp=One Time Password
|
otp-display-name=Authenticator Application
|
||||||
webauthn=WebAuthn
|
otp-help-text=Enter a verification code from authenticator application.
|
||||||
webauthn-passwordless=WebAuthn Passwordless
|
webauthn-display-name=Security Key
|
||||||
otpHelptext=A one-time password (OTP), is a password that is valid for only one login session.
|
webauthn-help-text=Use your security key to log in.
|
||||||
webauthnHelptext=WebAuthn lets web applications authenticate users without storing their passwords on servers.
|
webauthn-passwordless-display-name=Security Key
|
||||||
webauthn-passwordlessHelptext=I need help with this help text. Any suggestions?
|
webauthn-passwordless-help-text=Use your security key for passwordless log in.
|
||||||
|
|
||||||
# Applications page
|
# Applications page
|
||||||
applicationsPageTitle=Applications
|
applicationsPageTitle=Applications
|
||||||
|
|
|
@ -65,9 +65,11 @@ interface UserCredential {
|
||||||
interface CredentialContainer {
|
interface CredentialContainer {
|
||||||
category: CredCategory;
|
category: CredCategory;
|
||||||
type: CredType;
|
type: CredType;
|
||||||
|
displayName: string;
|
||||||
helptext?: string;
|
helptext?: string;
|
||||||
createAction: string;
|
createAction: string;
|
||||||
updateAction: string;
|
updateAction: string;
|
||||||
|
iconCssClass: string;
|
||||||
removeable: boolean;
|
removeable: boolean;
|
||||||
userCredentials: UserCredential[];
|
userCredentials: UserCredential[];
|
||||||
}
|
}
|
||||||
|
@ -176,17 +178,17 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
|
||||||
const userCredentials: UserCredential[] = credTypeMap.get(credType)!.userCredentials;
|
const userCredentials: UserCredential[] = credTypeMap.get(credType)!.userCredentials;
|
||||||
const removeable: boolean = credTypeMap.get(credType)!.removeable;
|
const removeable: boolean = credTypeMap.get(credType)!.removeable;
|
||||||
const updateAction: string = credTypeMap.get(credType)!.updateAction;
|
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) {
|
if (userCredentials.length === 0) {
|
||||||
const localizedType = Msg.localize(type);
|
const localizedDisplayName = Msg.localize(displayName);
|
||||||
return (
|
return (
|
||||||
<DataListItem aria-labelledby='no-credentials-list-item'>
|
<DataListItem aria-labelledby='no-credentials-list-item'>
|
||||||
<DataListItemRow key='no-credentials-list-item-row'>
|
<DataListItemRow key='no-credentials-list-item-row'>
|
||||||
<DataListItemCells
|
<DataListItemCells
|
||||||
dataListCells={[
|
dataListCells={[
|
||||||
<DataListCell/>,
|
<DataListCell/>,
|
||||||
<strong><Msg msgKey='notSetUp' params={[localizedType]}/></strong>,
|
<strong><Msg msgKey='notSetUp' params={[localizedDisplayName]}/></strong>,
|
||||||
<DataListCell/>
|
<DataListCell/>
|
||||||
]}/>
|
]}/>
|
||||||
</DataListItemRow>
|
</DataListItemRow>
|
||||||
|
@ -229,7 +231,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
|
||||||
if (!credContainer.createAction) return;
|
if (!credContainer.createAction) return;
|
||||||
|
|
||||||
const setupAction: AIACommand = new AIACommand(credContainer.createAction, this.props.location.pathname);
|
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 (
|
return (
|
||||||
<DataListItem aria-labelledby={'type-datalistitem-' + credContainer.type}>
|
<DataListItem aria-labelledby={'type-datalistitem-' + credContainer.type}>
|
||||||
|
@ -238,7 +240,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
|
||||||
dataListCells={[
|
dataListCells={[
|
||||||
<DataListCell width={5} key={'credTypeTitle-' + credContainer.type}>
|
<DataListCell width={5} key={'credTypeTitle-' + credContainer.type}>
|
||||||
<Title headingLevel={TitleLevel.h3} size='2xl'>
|
<Title headingLevel={TitleLevel.h3} size='2xl'>
|
||||||
<strong><Msg msgKey={credContainer.type}/></strong>
|
<strong><Msg msgKey={credContainer.displayName}/></strong>
|
||||||
</Title>
|
</Title>
|
||||||
<Msg msgKey={credContainer.helptext}/>
|
<Msg msgKey={credContainer.helptext}/>
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
|
@ -249,7 +251,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
|
||||||
<span className="pf-c-button__icon">
|
<span className="pf-c-button__icon">
|
||||||
<i className="fas fa-plus-circle" aria-hidden="true"></i>
|
<i className="fas fa-plus-circle" aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
<Msg msgKey='setUpNew' params={[credContainerType]}/>
|
<Msg msgKey='setUpNew' params={[credContainerDisplayName]}/>
|
||||||
</button>
|
</button>
|
||||||
</DataListAction>
|
</DataListAction>
|
||||||
</DataListItemRow>
|
</DataListItemRow>
|
||||||
|
|
|
@ -493,6 +493,14 @@ a.zocial {
|
||||||
margin-top: 0;
|
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-form-login div.form-group:last-of-type,
|
||||||
#kc-register-form 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 {
|
#kc-update-profile-form div.form-group:last-of-type {
|
||||||
|
|
|
@ -68,3 +68,20 @@ kcInputLargeClass=input-lg
|
||||||
|
|
||||||
##### css classes for form accessability
|
##### css classes for form accessability
|
||||||
kcSrOnlyClass=sr-only
|
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