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:
Marek Posolda 2025-08-20 11:42:24 +02:00 committed by GitHub
parent 32ae5fe100
commit dd7ad5b866
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 431 additions and 62 deletions

View File

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

View File

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

View File

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

View File

@ -41,6 +41,7 @@ export interface CredentialMetadataRepresentationMessage {
export interface CredentialMetadataRepresentation {
infoMessage: CredentialMetadataRepresentationMessage;
infoProperties: CredentialMetadataRepresentationMessage[];
warningMessageTitle: CredentialMetadataRepresentationMessage;
warningMessageDescription: CredentialMetadataRepresentationMessage;
credential: CredentialRepresentation;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}
}

View 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)"
}
}

View File

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