mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 15:02:05 -03:30
Ability to display 'authenticator provider' of the WebAuthn credential (#41615)
closes #41613 Signed-off-by: mposolda <mposolda@gmail.com> Co-authored-by: Jon Koops <jonkoops@gmail.com> Signed-off-by: Marek Posolda <mposolda@gmail.com>
This commit is contained in:
parent
32ae5fe100
commit
dd7ad5b866
@ -1,10 +1,13 @@
|
||||
package org.keycloak.representations.account;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
|
||||
public class CredentialMetadataRepresentation {
|
||||
|
||||
LocalizedMessage infoMessage;
|
||||
List<LocalizedMessage> infoProperties;
|
||||
LocalizedMessage warningMessageTitle;
|
||||
LocalizedMessage warningMessageDescription;
|
||||
|
||||
@ -27,6 +30,14 @@ public class CredentialMetadataRepresentation {
|
||||
this.infoMessage = infoMessage;
|
||||
}
|
||||
|
||||
public List<LocalizedMessage> getInfoProperties() {
|
||||
return infoProperties;
|
||||
}
|
||||
|
||||
public void setInfoProperties(List<LocalizedMessage> infoProperties) {
|
||||
this.infoProperties = infoProperties;
|
||||
}
|
||||
|
||||
public LocalizedMessage getWarningMessageTitle() {
|
||||
return warningMessageTitle;
|
||||
}
|
||||
|
||||
@ -182,6 +182,8 @@ sharedWithMe=Shared with Me
|
||||
username=Username
|
||||
webauthn-display-name=Passkey
|
||||
webauthn-help-text=Use your Passkey to sign in.
|
||||
webauthn-authenticator-provider=Authenticator provider
|
||||
webauthn-transports=Transport media
|
||||
webauthn-passwordless-display-name=Passkey
|
||||
webauthn-passwordless-help-text=Use your Passkey for passwordless sign in.
|
||||
passwordless=Passwordless
|
||||
@ -216,4 +218,3 @@ domains=Domains
|
||||
refresh=Refresh
|
||||
termsAndConditionsDeclined=You need to accept the Terms and Conditions to continue
|
||||
defaultLocale=Choose...
|
||||
webauthn-info= Transport media: {{0}}
|
||||
@ -6,6 +6,10 @@ import {
|
||||
DataListItem,
|
||||
DataListItemCells,
|
||||
DataListItemRow,
|
||||
DescriptionList,
|
||||
DescriptionListDescription,
|
||||
DescriptionListGroup,
|
||||
DescriptionListTerm,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
MenuToggle,
|
||||
@ -124,6 +128,7 @@ export const SigningIn = () => {
|
||||
}
|
||||
if (
|
||||
credMetadata.infoMessage ||
|
||||
credMetadata.infoProperties ||
|
||||
(credMetadata.warningMessageTitle &&
|
||||
credMetadata.warningMessageDescription)
|
||||
) {
|
||||
@ -145,6 +150,30 @@ export const SigningIn = () => {
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{credMetadata.infoProperties && (
|
||||
<Split className="pf-v5-u-mb-lg">
|
||||
<SplitItem>
|
||||
<InfoAltIcon />
|
||||
</SplitItem>
|
||||
<SplitItem isFilled className="pf-v5-u-ml-xs">
|
||||
<DescriptionList
|
||||
isHorizontal
|
||||
horizontalTermWidthModifier={{
|
||||
"2xl": "15ch",
|
||||
}}
|
||||
>
|
||||
{credMetadata.infoProperties.map((prop) => (
|
||||
<DescriptionListGroup key={prop.key}>
|
||||
<DescriptionListTerm>{t(prop.key)}</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{prop.parameters ? prop.parameters[0] : ""}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
))}
|
||||
</DescriptionList>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
)}
|
||||
{credMetadata.warningMessageTitle &&
|
||||
credMetadata.warningMessageDescription && (
|
||||
<>
|
||||
|
||||
@ -41,6 +41,7 @@ export interface CredentialMetadataRepresentationMessage {
|
||||
|
||||
export interface CredentialMetadataRepresentation {
|
||||
infoMessage: CredentialMetadataRepresentationMessage;
|
||||
infoProperties: CredentialMetadataRepresentationMessage[];
|
||||
warningMessageTitle: CredentialMetadataRepresentationMessage;
|
||||
warningMessageDescription: CredentialMetadataRepresentationMessage;
|
||||
credential: CredentialRepresentation;
|
||||
|
||||
@ -719,6 +719,9 @@ public class ModelToRepresentation {
|
||||
|
||||
rep.setCredential(ModelToRepresentation.toRepresentation(credentialMetadata.getCredentialModel()));
|
||||
rep.setInfoMessage(toLocalizedMessage(credentialMetadata.getInfoMessage()));
|
||||
rep.setInfoProperties(credentialMetadata.getInfoProperties() == null ? null : credentialMetadata.getInfoProperties().stream()
|
||||
.map(ModelToRepresentation::toLocalizedMessage)
|
||||
.toList());
|
||||
rep.setWarningMessageDescription(toLocalizedMessage(credentialMetadata.getWarningMessageDescription()));
|
||||
rep.setWarningMessageTitle(toLocalizedMessage(credentialMetadata.getWarningMessageTitle()));
|
||||
return rep;
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
package org.keycloak.credential;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CredentialMetadata {
|
||||
LocalizedMessage infoMessage;
|
||||
List<LocalizedMessage> infoProperties;
|
||||
LocalizedMessage warningMessageTitle;
|
||||
LocalizedMessage warningMessageDescription;
|
||||
CredentialModel credentialModel;
|
||||
@ -18,6 +21,10 @@ public class CredentialMetadata {
|
||||
return infoMessage;
|
||||
}
|
||||
|
||||
public List<LocalizedMessage> getInfoProperties() {
|
||||
return infoProperties;
|
||||
}
|
||||
|
||||
public LocalizedMessage getWarningMessageTitle() {
|
||||
return warningMessageTitle;
|
||||
}
|
||||
@ -41,6 +48,10 @@ public class CredentialMetadata {
|
||||
this.infoMessage = message;
|
||||
}
|
||||
|
||||
public void setInfoProperties(List<LocalizedMessage> infoProperties) {
|
||||
this.infoProperties = infoProperties;
|
||||
}
|
||||
|
||||
public static class LocalizedMessage {
|
||||
private final String key;
|
||||
private final Object[] parameters;
|
||||
|
||||
@ -40,6 +40,21 @@ public interface CredentialProvider<T extends CredentialModel> extends Provider
|
||||
|
||||
T getCredentialFromModel(CredentialModel model);
|
||||
|
||||
/**
|
||||
* Get the credential (usually stored credential retrieved from the DB) and decorates it with additional metadata
|
||||
* to be present for example in the admin console. Those additional metadata could be various metadata, which are not saved in the DB,
|
||||
* but can be retrieved from saved data to be presented to admins/users in the nice way (For example display "authenticator Provider"
|
||||
* for WebAuthn credential based on the AAGUID of WebAuthn credential)
|
||||
*
|
||||
* @param model stored credential retrieved from the DB
|
||||
* @return credential model useful for the presentation (not necessarily only stored data, but possibly some other metadata added)
|
||||
*/
|
||||
default T getCredentialForPresentationFromModel(CredentialModel model) {
|
||||
T presentationModel = getCredentialFromModel(model);
|
||||
presentationModel.setFederationLink(model.getFederationLink());
|
||||
return presentationModel;
|
||||
}
|
||||
|
||||
default T getDefaultCredential(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
CredentialModel model = user.credentialManager().getStoredCredentialsByTypeStream(getType())
|
||||
.findFirst().orElse(null);
|
||||
|
||||
@ -66,6 +66,15 @@ public class WebAuthnCredentialModel extends CredentialModel {
|
||||
return credentialModel;
|
||||
}
|
||||
|
||||
public static WebAuthnCredentialModel create(String id, String credentialType, Long createdDate, String userLabel, WebAuthnCredentialData credentialData, WebAuthnSecretData secretData) {
|
||||
WebAuthnCredentialModel credentialModel = new WebAuthnCredentialModel(credentialType, credentialData, secretData);
|
||||
credentialModel.fillCredentialModelFields();
|
||||
credentialModel.setId(id);
|
||||
credentialModel.setCreatedDate(createdDate);
|
||||
credentialModel.setUserLabel(userLabel);
|
||||
return credentialModel;
|
||||
}
|
||||
|
||||
|
||||
public static WebAuthnCredentialModel createFromCredentialModel(CredentialModel credentialModel) {
|
||||
try {
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package org.keycloak.models.credential.dto;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class WebAuthnCredentialPresentationData extends WebAuthnCredentialData {
|
||||
|
||||
private final String authenticatorProvider;
|
||||
|
||||
@JsonCreator
|
||||
public WebAuthnCredentialPresentationData(@JsonProperty("aaguid") String aaguid,
|
||||
@JsonProperty("credentialId") String credentialId,
|
||||
@JsonProperty("counter") long counter,
|
||||
@JsonProperty("attestationStatement") String attestationStatement,
|
||||
@JsonProperty("credentialPublicKey") String credentialPublicKey,
|
||||
@JsonProperty("attestationStatementFormat") String attestationStatementFormat,
|
||||
@JsonProperty("transports") Set<String> transports,
|
||||
@JsonProperty("authenticatorProvider") String authenticatorProvider) {
|
||||
super(aaguid, credentialId, counter, attestationStatement, credentialPublicKey, attestationStatementFormat, transports);
|
||||
this.authenticatorProvider = authenticatorProvider;
|
||||
}
|
||||
|
||||
public String getAuthenticatorProvider() {
|
||||
return authenticatorProvider;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package org.keycloak.authentication.authenticators.browser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.FileUtils;
|
||||
|
||||
/**
|
||||
* Provides metadata for WebAuthn credentials
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class WebAuthnMetadataService {
|
||||
|
||||
// Based on https://github.com/duo-labs/webauthn.io/blob/master/_app/homepage/services/metadata.py
|
||||
private static final String FILE_NAME = "keycloak-webauthn-metadata.json";
|
||||
|
||||
private Map<String, String> aaguidToProviderNames;
|
||||
|
||||
private Map<String, String> readMetadata() {
|
||||
try {
|
||||
try (InputStream is = FileUtils.getJsonFileFromClasspathOrConfFolder(FILE_NAME)) {
|
||||
Map<String, Map<String, String>> map = JsonSerialization.readValue(is, new TypeReference<>() {});
|
||||
return map.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, entry -> {
|
||||
String value = entry.getValue().get("name");
|
||||
if (value == null) {
|
||||
throw new IllegalStateException("Not found 'name' for the AAGUID '" + entry.getKey() + "' in the file '" + FILE_NAME + "'.");
|
||||
}
|
||||
return value;
|
||||
}));
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalStateException("Error loading the webauthn metadata from file " + FILE_NAME, ioe);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Map<String, String> getAaguidToProviderNames() {
|
||||
if (aaguidToProviderNames == null) {
|
||||
synchronized (this) {
|
||||
if (aaguidToProviderNames == null) {
|
||||
// Make sure the file is not parsed during server startup, but "lazily" when needed for the 1st time
|
||||
this.aaguidToProviderNames = readMetadata();
|
||||
}
|
||||
}
|
||||
}
|
||||
return aaguidToProviderNames;
|
||||
}
|
||||
|
||||
public String getAuthenticatorProvider(String aaguid) {
|
||||
return aaguid == null ? null : getAaguidToProviderNames().get(aaguid);
|
||||
}
|
||||
}
|
||||
@ -35,6 +35,7 @@ import com.webauthn4j.verifier.OriginVerifierImpl;
|
||||
import com.webauthn4j.verifier.exception.BadOriginException;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.authenticators.browser.WebAuthnMetadataService;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.common.util.Time;
|
||||
@ -44,10 +45,12 @@ import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.WebAuthnPolicy;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
||||
import org.keycloak.models.credential.dto.WebAuthnCredentialPresentationData;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@ -58,19 +61,19 @@ import java.util.stream.Collectors;
|
||||
public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCredentialModel>, CredentialInputValidator {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(WebAuthnCredentialProvider.class);
|
||||
private static final String WEBAUTHN_INFO = "webauthn-info";
|
||||
private static final String WEBAUTHN_AUTHENTICATOR_PROVIDER = "webauthn-authenticator-provider";
|
||||
private static final String WEBAUTHN_TRANSPORTS = "webauthn-transports";
|
||||
|
||||
private KeycloakSession session;
|
||||
private final KeycloakSession session;
|
||||
private final WebAuthnMetadataService metadataService;
|
||||
private final CredentialPublicKeyConverter credentialPublicKeyConverter;
|
||||
private final AttestationStatementConverter attestationStatementConverter;
|
||||
|
||||
private CredentialPublicKeyConverter credentialPublicKeyConverter;
|
||||
private AttestationStatementConverter attestationStatementConverter;
|
||||
|
||||
public WebAuthnCredentialProvider(KeycloakSession session, ObjectConverter objectConverter) {
|
||||
public WebAuthnCredentialProvider(KeycloakSession session, WebAuthnMetadataService metadataService, ObjectConverter objectConverter) {
|
||||
this.session = session;
|
||||
if (credentialPublicKeyConverter == null)
|
||||
credentialPublicKeyConverter = new CredentialPublicKeyConverter(objectConverter);
|
||||
if (attestationStatementConverter == null)
|
||||
attestationStatementConverter = new AttestationStatementConverter(objectConverter);
|
||||
this.metadataService = metadataService;
|
||||
this.credentialPublicKeyConverter = new CredentialPublicKeyConverter(objectConverter);
|
||||
this.attestationStatementConverter = new AttestationStatementConverter(objectConverter);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -93,6 +96,21 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
||||
return WebAuthnCredentialModel.createFromCredentialModel(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthnCredentialModel getCredentialForPresentationFromModel(CredentialModel model) {
|
||||
WebAuthnCredentialModel origCredential = getCredentialFromModel(model);
|
||||
WebAuthnCredentialData data = origCredential.getWebAuthnCredentialData();
|
||||
|
||||
String authenticatorProvider = metadataService.getAuthenticatorProvider(data.getAaguid());
|
||||
if (authenticatorProvider == null)
|
||||
return origCredential;
|
||||
|
||||
WebAuthnCredentialPresentationData presentationData = new WebAuthnCredentialPresentationData(
|
||||
data.getAaguid(), data.getCredentialId(), data.getCounter(), data.getAttestationStatement(), data.getCredentialPublicKey(),
|
||||
data.getAttestationStatementFormat(), data.getTransports(), authenticatorProvider);
|
||||
return WebAuthnCredentialModel.create(origCredential.getId(), origCredential.getType(), origCredential.getCreatedDate(), origCredential.getUserLabel(),
|
||||
presentationData, origCredential.getWebAuthnSecretData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert WebAuthn credential input to the model, which can be saved in the persistent storage (DB)
|
||||
@ -309,16 +327,22 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
||||
public CredentialMetadata getCredentialMetadata(WebAuthnCredentialModel credentialModel, CredentialTypeMetadata credentialTypeMetadata) {
|
||||
|
||||
CredentialMetadata credentialMetadata = new CredentialMetadata();
|
||||
List<CredentialMetadata.LocalizedMessage> properties = new LinkedList<>();
|
||||
|
||||
try {
|
||||
WebAuthnCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), WebAuthnCredentialData.class);
|
||||
credentialModel = getCredentialForPresentationFromModel(credentialModel);
|
||||
WebAuthnCredentialPresentationData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), WebAuthnCredentialPresentationData.class);
|
||||
Set<String> transports = credentialData.getTransports();
|
||||
if (credentialData.getAuthenticatorProvider() != null) {
|
||||
properties.add(new CredentialMetadata.LocalizedMessage(WEBAUTHN_AUTHENTICATOR_PROVIDER, new String[] { credentialData.getAuthenticatorProvider() }));
|
||||
}
|
||||
if (transports != null && !transports.isEmpty()) {
|
||||
String joinedTransports = String.join(", ", transports);
|
||||
credentialMetadata.setInfoMessage(WEBAUTHN_INFO, joinedTransports);
|
||||
|
||||
properties.add(new CredentialMetadata.LocalizedMessage(WEBAUTHN_TRANSPORTS, new String[] { joinedTransports }));
|
||||
}
|
||||
if (!properties.isEmpty()) {
|
||||
credentialMetadata.setInfoProperties(properties);
|
||||
}
|
||||
|
||||
} catch (
|
||||
IOException e) {
|
||||
logger.warn("unable to deserialize model information, skipping messages", e);
|
||||
|
||||
@ -18,6 +18,7 @@ package org.keycloak.credential;
|
||||
|
||||
import com.webauthn4j.converter.util.ObjectConverter;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.authenticators.browser.WebAuthnMetadataService;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
@ -27,13 +28,14 @@ public class WebAuthnCredentialProviderFactory implements CredentialProviderFact
|
||||
public static final String PROVIDER_ID = "keycloak-webauthn";
|
||||
|
||||
private ObjectConverter converter;
|
||||
private volatile WebAuthnMetadataService metadataService;
|
||||
|
||||
@Override
|
||||
public CredentialProvider create(KeycloakSession session) {
|
||||
return new WebAuthnCredentialProvider(session, createOrGetObjectConverter());
|
||||
return new WebAuthnCredentialProvider(session, getMetadataService(), createOrGetObjectConverter());
|
||||
}
|
||||
|
||||
private ObjectConverter createOrGetObjectConverter() {
|
||||
protected ObjectConverter createOrGetObjectConverter() {
|
||||
if (converter == null) {
|
||||
synchronized (this) {
|
||||
if (converter == null) {
|
||||
@ -53,4 +55,15 @@ public class WebAuthnCredentialProviderFactory implements CredentialProviderFact
|
||||
public boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.WEB_AUTHN);
|
||||
}
|
||||
|
||||
protected WebAuthnMetadataService getMetadataService() {
|
||||
if (metadataService == null) {
|
||||
synchronized (this) {
|
||||
if (metadataService == null) {
|
||||
this.metadataService = new WebAuthnMetadataService();
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.metadataService;
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
package org.keycloak.credential;
|
||||
|
||||
import com.webauthn4j.converter.util.ObjectConverter;
|
||||
import org.keycloak.authentication.authenticators.browser.WebAuthnMetadataService;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.WebAuthnPolicy;
|
||||
@ -31,8 +32,8 @@ import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
*/
|
||||
public class WebAuthnPasswordlessCredentialProvider extends WebAuthnCredentialProvider {
|
||||
|
||||
public WebAuthnPasswordlessCredentialProvider(KeycloakSession session, ObjectConverter objectConverter) {
|
||||
super(session, objectConverter);
|
||||
public WebAuthnPasswordlessCredentialProvider(KeycloakSession session, WebAuthnMetadataService metadataService, ObjectConverter objectConverter) {
|
||||
super(session, metadataService, objectConverter);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -18,35 +18,18 @@
|
||||
|
||||
package org.keycloak.credential;
|
||||
|
||||
import com.webauthn4j.converter.util.ObjectConverter;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class WebAuthnPasswordlessCredentialProviderFactory implements CredentialProviderFactory<WebAuthnPasswordlessCredentialProvider>, EnvironmentDependentProviderFactory {
|
||||
public class WebAuthnPasswordlessCredentialProviderFactory extends WebAuthnCredentialProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "keycloak-webauthn-passwordless";
|
||||
|
||||
private ObjectConverter converter;
|
||||
|
||||
@Override
|
||||
public CredentialProvider create(KeycloakSession session) {
|
||||
return new WebAuthnPasswordlessCredentialProvider(session, createOrGetObjectConverter());
|
||||
}
|
||||
|
||||
private ObjectConverter createOrGetObjectConverter() {
|
||||
if (converter == null) {
|
||||
synchronized (this) {
|
||||
if (converter == null) {
|
||||
converter = new ObjectConverter();
|
||||
}
|
||||
}
|
||||
}
|
||||
return converter;
|
||||
return new WebAuthnPasswordlessCredentialProvider(session, getMetadataService(), createOrGetObjectConverter());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -54,8 +37,4 @@ public class WebAuthnPasswordlessCredentialProviderFactory implements Credential
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.WEB_AUTHN);
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ import org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProvide
|
||||
import org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.FileUtils;
|
||||
|
||||
/**
|
||||
* Utilities for treating client policies/profiles
|
||||
@ -66,16 +67,7 @@ public class ClientPoliciesUtil {
|
||||
|
||||
public static InputStream getJsonFileFromClasspathOrConfFolder(String name) throws IOException {
|
||||
final String fileName = name + ".json";
|
||||
// first try to read the json configuration file from classpath
|
||||
InputStream is = ClientPoliciesUtil.class.getResourceAsStream("/" + fileName);
|
||||
if (is == null) {
|
||||
Path path = Paths.get(System.getProperty("jboss.server.config.dir")).resolve(fileName);
|
||||
if (!Files.isReadable(path)) {
|
||||
throw new IOException(String.format("File \"%s\" does not exists under the config folder", path));
|
||||
}
|
||||
is = Files.newInputStream(path);
|
||||
}
|
||||
return is;
|
||||
return FileUtils.getJsonFileFromClasspathOrConfFolder(fileName);
|
||||
}
|
||||
|
||||
public static List<ClientProfileRepresentation> readGlobalClientProfilesRepresentation(KeycloakSession session, String name) throws ClientPolicyException {
|
||||
|
||||
@ -27,6 +27,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.reactive.NoCache;
|
||||
import org.keycloak.authentication.AuthenticatorUtil;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken;
|
||||
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
|
||||
@ -36,6 +37,7 @@ import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.CollectionUtil;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.credential.CredentialProvider;
|
||||
import org.keycloak.email.EmailException;
|
||||
import org.keycloak.email.EmailTemplateProvider;
|
||||
import org.keycloak.events.Details;
|
||||
@ -810,10 +812,23 @@ public class UserResource {
|
||||
auth.users().requireView(user);
|
||||
|
||||
return user.credentialManager().getCredentials()
|
||||
.map(this::decorateCredentialForPresentation)
|
||||
.map(ModelToRepresentation::toRepresentation)
|
||||
.peek(credentialRepresentation -> credentialRepresentation.setSecretData(null));
|
||||
}
|
||||
|
||||
private CredentialModel decorateCredentialForPresentation(CredentialModel credential) {
|
||||
CredentialProvider credentialProvider = AuthenticatorUtil.getCredentialProviders(session)
|
||||
.filter(p -> p.supportsCredentialType(credential.getType()))
|
||||
.findFirst().orElse(null);
|
||||
if (credentialProvider == null) {
|
||||
logger.warnf("Credential Provider not found for credential of type '%s'", credential.getType());
|
||||
return credential;
|
||||
}
|
||||
|
||||
return credentialProvider.getCredentialForPresentationFromModel(credential);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return credential types, which are provided by the user storage where user is stored. Returned values can contain for example "password", "otp" etc.
|
||||
* This will always return empty list for "local" users, which are not backed by any user storage
|
||||
|
||||
@ -34,6 +34,7 @@ import org.keycloak.securityprofile.SecurityProfileProviderFactory;
|
||||
import org.keycloak.services.clientpolicy.ClientPoliciesUtil;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.FileUtils;
|
||||
|
||||
/**
|
||||
* The default implementation for the security profile. It reads the configuration
|
||||
@ -89,16 +90,7 @@ public class DefaultSecurityProfileProviderFactory implements SecurityProfilePro
|
||||
SecurityProfileConfiguration conf;
|
||||
final String file = name + ".json";
|
||||
try {
|
||||
// first try to read the json configuration file from classpath
|
||||
InputStream tmp = getClass().getResourceAsStream("/" + file);
|
||||
if (tmp == null) {
|
||||
Path path = Paths.get(System.getProperty("jboss.server.config.dir")).resolve(file);
|
||||
if (!Files.isReadable(path)) {
|
||||
throw new IOException(String.format("File %s does not exists in the conf folder", file));
|
||||
}
|
||||
tmp = Files.newInputStream(path);
|
||||
}
|
||||
try (InputStream is = tmp) {
|
||||
try (InputStream is = FileUtils.getJsonFileFromClasspathOrConfFolder(file)) {
|
||||
conf = JsonSerialization.readValue(is, SecurityProfileConfiguration.class);
|
||||
}
|
||||
// read the list of client profiles and policies validated
|
||||
|
||||
35
services/src/main/java/org/keycloak/utils/FileUtils.java
Normal file
35
services/src/main/java/org/keycloak/utils/FileUtils.java
Normal file
@ -0,0 +1,35 @@
|
||||
package org.keycloak.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.keycloak.services.clientpolicy.ClientPoliciesUtil;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class FileUtils {
|
||||
|
||||
/**
|
||||
* Read the input stream from the specified file
|
||||
*
|
||||
* @param fileName file name without path
|
||||
* @return input stream
|
||||
* @throws IOException
|
||||
*/
|
||||
public static InputStream getJsonFileFromClasspathOrConfFolder(String fileName) throws IOException {
|
||||
// first try to read the json configuration file from classpath
|
||||
InputStream is = ClientPoliciesUtil.class.getResourceAsStream("/" + fileName);
|
||||
if (is == null) {
|
||||
Path path = Paths.get(System.getProperty("jboss.server.config.dir")).resolve(fileName);
|
||||
if (!Files.isReadable(path)) {
|
||||
throw new IOException(String.format("File \"%s\" does not exists under the config folder", path));
|
||||
}
|
||||
is = Files.newInputStream(path);
|
||||
}
|
||||
return is;
|
||||
}
|
||||
}
|
||||
124
services/src/main/resources/keycloak-webauthn-metadata.json
Normal file
124
services/src/main/resources/keycloak-webauthn-metadata.json
Normal file
@ -0,0 +1,124 @@
|
||||
{
|
||||
"ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4": {
|
||||
"name": "Google Password Manager"
|
||||
},
|
||||
"adce0002-35bc-c60a-648b-0b25f1f05503": {
|
||||
"name": "Chrome on Mac"
|
||||
},
|
||||
"08987058-cadc-4b81-b6e1-30de50dcbe96": {
|
||||
"name": "Windows Hello"
|
||||
},
|
||||
"9ddd1817-af5a-4672-a2b9-3e3dd95000a9": {
|
||||
"name": "Windows Hello"
|
||||
},
|
||||
"6028b017-b1d4-4c02-b4b3-afcdafc96bb2": {
|
||||
"name": "Windows Hello"
|
||||
},
|
||||
"dd4ec289-e01d-41c9-bb89-70fa845d4bf2": {
|
||||
"name": "iCloud Keychain (Managed)"
|
||||
},
|
||||
"531126d6-e717-415c-9320-3d9aa6981239": {
|
||||
"name": "Dashlane"
|
||||
},
|
||||
"bada5566-a7aa-401f-bd96-45619a55120d": {
|
||||
"name": "1Password"
|
||||
},
|
||||
"b84e4048-15dc-4dd0-8640-f4f60813c8af": {
|
||||
"name": "NordPass"
|
||||
},
|
||||
"0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6": {
|
||||
"name": "Keeper"
|
||||
},
|
||||
"f3809540-7f14-49c1-a8b3-8f813b225541": {
|
||||
"name": "Enpass"
|
||||
},
|
||||
"b5397666-4885-aa6b-cebf-e52262a439a2": {
|
||||
"name": "Chromium Browser"
|
||||
},
|
||||
"771b48fd-d3d4-4f74-9232-fc157ab0507a": {
|
||||
"name": "Edge on Mac"
|
||||
},
|
||||
"39a5647e-1853-446c-a1f6-a79bae9f5bc7": {
|
||||
"name": "IDmelon"
|
||||
},
|
||||
"d548826e-79b4-db40-a3d8-11116f7e8349": {
|
||||
"name": "Bitwarden"
|
||||
},
|
||||
"fbfc3007-154e-4ecc-8c0b-6e020557d7bd": {
|
||||
"name": "iCloud Keychain"
|
||||
},
|
||||
"53414d53-554e-4700-0000-000000000000": {
|
||||
"name": "Samsung Pass"
|
||||
},
|
||||
"66a0ccb3-bd6a-191f-ee06-e375c50b9846": {
|
||||
"name": "Thales Bio iOS SDK"
|
||||
},
|
||||
"8836336a-f590-0921-301d-46427531eee6": {
|
||||
"name": "Thales Bio Android SDK"
|
||||
},
|
||||
"cd69adb5-3c7a-deb9-3177-6800ea6cb72a": {
|
||||
"name": "Thales PIN Android SDK"
|
||||
},
|
||||
"17290f1e-c212-34d0-1423-365d729f09d9": {
|
||||
"name": "Thales PIN iOS SDK"
|
||||
},
|
||||
"0bb43545-fd2c-4185-87dd-feb0b2916ace": {
|
||||
"name": "Security Key NFC by Yubico - Enterprise Edition (5.4)"
|
||||
},
|
||||
"149a2021-8ef6-4133-96b8-81f8d5b7f1f5": {
|
||||
"name": "Security Key by Yubico with NFC (5.2, 5.4)"
|
||||
},
|
||||
"2fc0579f-8113-47ea-b116-bb5a8db9202a": {
|
||||
"name": "YubiKey 5 Series with NFC (5.2, 5.4)"
|
||||
},
|
||||
"6d44ba9b-f6ec-2e49-b930-0c8fe920cb73": {
|
||||
"name": "Security Key by Yubico with NFC (5.1)"
|
||||
},
|
||||
"73bb0cd4-e502-49b8-9c6f-b59445bf720b": {"name": "YubiKey 5 FIPS Series (5.4)"},
|
||||
"85203421-48f9-4355-9bc8-8a53846e5083": {"name": "YubiKey 5Ci FIPS (5.4)"},
|
||||
"a4e9fc6d-4cbe-4758-b8ba-37598bb5bbaa": {
|
||||
"name": "Security Key by Yubico with NFC (5.4)"
|
||||
},
|
||||
"b92c3f9a-c014-4056-887f-140a2501163b": {"name": "Security Key by Yubico (5.2)"},
|
||||
"c1f9a0bc-1dd2-404a-b27f-8e29047a43fd": {
|
||||
"name": "YubiKey 5 FIPS Series with NFC (5.4)"
|
||||
},
|
||||
"c5ef55ff-ad9a-4b9f-b580-adebafe026d0": {"name": "YubiKey 5Ci (5.2, 5.4)"},
|
||||
"cb69481e-8ff7-4039-93ec-0a2729a154a8": {"name": "YubiKey 5 Series (5.1)"},
|
||||
"d8522d9f-575b-4866-88a9-ba99fa02f35b": {"name": "YubiKey Bio Series (5.5, 5.6)"},
|
||||
"ee882879-721c-4913-9775-3dfcce97072a": {"name": "YubiKey 5 Series (5.2, 5.4)"},
|
||||
"f8a011f3-8c0a-4d15-8006-17111f9edc7d": {"name": "Security Key by Yubico (5.1)"},
|
||||
"fa2b99dc-9e39-4257-8f92-4a30d23c4118": {"name": "YubiKey 5 Series with NFC (5.1)"},
|
||||
"e77e3c64-05e3-428b-8824-0cbeb04b829d": {
|
||||
"name": "Security Key by Yubico with NFC (5.7)"
|
||||
},
|
||||
"47ab2fb4-66ac-4184-9ae1-86be814012d5": {
|
||||
"name": "Security Key by Yubico with NFC - Enterprise Edition (5.7)"
|
||||
},
|
||||
"a25342c0-3cdc-4414-8e46-f4807fca511c": {"name": "YubiKey 5 Series with NFC (5.7)"},
|
||||
"19083c3d-8383-4b18-bc03-8f1c9ab2fd1b": {"name": "YubiKey 5 Series (5.7)"},
|
||||
"a02167b9-ae71-4ac7-9a07-06432ebb6f1c": {"name": "YubiKey 5Ci (5.7)"},
|
||||
"7d1351a6-e097-4852-b8bf-c9ac5c9ce4a3": {
|
||||
"name": "YubiKey Bio Series - Multi-protocol Edition (5.7)"
|
||||
},
|
||||
"30b5035e-d297-4ff1-b00b-addc96ba6a98": {"name": "DIGIPASS FX1 Bio"},
|
||||
"30b5035e-d297-4ff1-010b-addc96ba6a98": {"name": "DIGIPASS FX1a Bio"},
|
||||
"30b5035e-d297-4ff2-b00b-addc96ba6a98": {"name": "DIGIPASS FX2"},
|
||||
"30b5035e-d297-4ff3-b00b-addc96ba6a98": {"name": "DIGIPASS FX3"},
|
||||
"30b5035e-d297-4ff7-b00b-addc96ba6a98": {"name": "DIGIPASS FX7"},
|
||||
"9ff4cc65-6154-4fff-ba09-9e2af7882ad2": {
|
||||
"name": "Security Key by Yubico with NFC - Enterprise Edition (5.7, enterprise profile)"
|
||||
},
|
||||
"1ac71f64-468d-4fe0-bef1-0e5f2f551f18": {
|
||||
"name": "YubiKey 5 Series with NFC (5.7, enterprise profile)"
|
||||
},
|
||||
"20ac7a17-c814-4833-93fe-539f0d5e3389": {
|
||||
"name": "YubiKey 5 Series (5.7, enterprise profile)"
|
||||
},
|
||||
"b90e7dc1-316e-4fee-a25a-56a666a670fe": {
|
||||
"name": "YubiKey 5Ci (5.7, enterprise profile)"
|
||||
},
|
||||
"68167692-ff5a-4ac8-9fd7-0be292a1af68": {
|
||||
"name": "YubiKey Bio Series - Multi-protocol Edition (5.7, enterprise profile)"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package org.keycloak.authentication.authenticators.browser;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.nullValue;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class WebAuthnMetadataServiceTest {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testWebAuthnMetadata() {
|
||||
WebAuthnMetadataService service = new WebAuthnMetadataService();
|
||||
Assert.assertThat( service.getAuthenticatorProvider(null), nullValue());
|
||||
Assert.assertThat( service.getAuthenticatorProvider("00000000-0000-0000-0000-000000000000"), nullValue());
|
||||
Assert.assertThat( service.getAuthenticatorProvider("ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4"), is("Google Password Manager"));
|
||||
Assert.assertThat( service.getAuthenticatorProvider("d548826e-79b4-db40-a3d8-11116f7e8349"), is("Bitwarden"));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user