WebAuthnAuthenticatorsBean.java

/*
 * Copyright 2002-2019 the original author or authors.
 *
 * 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.forms.login.freemarker.model;

import com.webauthn4j.data.AuthenticatorTransport;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.CollectionUtil;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.theme.DateTimeFormatterUtil;
import org.keycloak.utils.StringUtil;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

public class WebAuthnAuthenticatorsBean {

    private final List<WebAuthnAuthenticatorBean> authenticators;

    public WebAuthnAuthenticatorsBean(KeycloakSession session, RealmModel realm, UserModel user, String credentialType) {
        // should consider multiple credentials in the future, but only single credential supported now.
        this.authenticators = user.credentialManager().getStoredCredentialsByTypeStream(credentialType)
                .map(WebAuthnCredentialModel::createFromCredentialModel)
                .map(webAuthnCredential -> {
                    String credentialId = Base64Url.encodeBase64ToBase64Url(webAuthnCredential.getWebAuthnCredentialData().getCredentialId());
                    String label = (webAuthnCredential.getUserLabel() == null || webAuthnCredential.getUserLabel().isEmpty()) ? "label missing" : webAuthnCredential.getUserLabel();
                    String createdAt = DateTimeFormatterUtil.getDateTimeFromMillis(webAuthnCredential.getCreatedDate(), session.getContext().resolveLocale(user));
                    final Set<String> transports = webAuthnCredential.getWebAuthnCredentialData().getTransports();

                    return new WebAuthnAuthenticatorBean(credentialId, label, createdAt, transports);
                }).collect(Collectors.toList());
    }

    public List<WebAuthnAuthenticatorBean> getAuthenticators() {
        return authenticators;
    }

    public static class WebAuthnAuthenticatorBean {
        public static final String DEFAULT_ICON = "kcWebAuthnDefaultIcon";
        public static final String UNKNOWN_AUTH_ICON = "kcWebAuthnUnknownIcon";

        private final String credentialId;
        private final String label;
        private final String createdAt;
        private final TransportsBean transports;

        public WebAuthnAuthenticatorBean(String credentialId, String label, String createdAt, Set<String> transports) {
            this.credentialId = credentialId;
            this.label = label;
            this.createdAt = createdAt;
            this.transports = TransportsBean.convertFromSet(transports);
        }

        public String getCredentialId() {
            return this.credentialId;
        }

        public String getLabel() {
            return this.label;
        }

        public String getCreatedAt() {
            return this.createdAt;
        }

        public TransportsBean getTransports() {
            return transports;
        }

        public static class TransportsBean {
            private final Set<String> displayNameProperties;
            private final String iconClass;

            public TransportsBean(Set<String> displayNameProperties, String iconClass) {
                this.displayNameProperties = displayNameProperties;
                this.iconClass = iconClass;
            }

            public TransportsBean(String displayNameProperty, String iconClass) {
                this(Collections.singleton(displayNameProperty), iconClass);
            }

            public TransportsBean(Transport transport) {
                this(transport.getDisplayNameProperty(), transport.getIconClass());
            }

            public Set<String> getDisplayNameProperties() {
                return displayNameProperties;
            }

            public String getIconClass() {
                return iconClass;
            }

            /**
             * Converts set of available transport media to TransportsBean
             *
             * @param transports set of available transport media
             * @return TransportBean
             */
            public static TransportsBean convertFromSet(Set<String> transports) {
                if (CollectionUtil.isEmpty(transports)) {
                    return new TransportsBean(Transport.UNKNOWN);
                }

                final Set<Transport> trans = transports.stream()
                        .filter(Objects::nonNull)
                        .map(Transport::getByMapperName)
                        .collect(Collectors.toSet());

                if (trans.size() <= 1) {
                    final Transport transport = trans.stream()
                            .findFirst()
                            .orElse(Transport.UNKNOWN);

                    return new TransportsBean(transport);
                } else {
                    final Set<String> displayNameProperties = trans.stream()
                            .map(Transport::getDisplayNameProperty)
                            .filter(StringUtil::isNotBlank)
                            .collect(Collectors.toSet());

                    return new TransportsBean(displayNameProperties, DEFAULT_ICON);
                }
            }

            protected enum Transport {
                USB("usb", AuthenticatorTransport.USB.getValue(), "kcWebAuthnUSB"),
                NFC("nfc", AuthenticatorTransport.NFC.getValue(), "kcWebAuthnNFC"),
                BLE("bluetooth", AuthenticatorTransport.BLE.getValue(), "kcWebAuthnBLE"),
                INTERNAL("internal", AuthenticatorTransport.INTERNAL.getValue(), "kcWebAuthnInternal"),
                UNKNOWN("", "", UNKNOWN_AUTH_ICON);

                private final String displayNameProperty;
                private final String mapperName;
                private final String iconClass;

                /**
                 * @param displayNameProperty Message property - defined in messages_xx.properties
                 * @param mapperName          used for mapping transport media name
                 * @param iconClass           icon class for particular transport media - defined in theme.properties
                 */
                Transport(String displayNameProperty, String mapperName, String iconClass) {
                    this.displayNameProperty = displayNameProperty;
                    this.mapperName = mapperName;
                    this.iconClass = iconClass;
                }

                public String getDisplayNameProperty() {
                    return displayNameProperty;
                }

                public String getMapperName() {
                    return mapperName;
                }

                public String getIconClass() {
                    return iconClass;
                }

                public static Transport getByDisplayNameProperty(String property) {
                    return Arrays.stream(Transport.values())
                            .filter(f -> f.getDisplayNameProperty().equals(property))
                            .findFirst()
                            .orElse(UNKNOWN);
                }

                public static Transport getByMapperName(String mapperName) {
                    if (StringUtil.isBlank(mapperName)) return UNKNOWN;

                    return Arrays.stream(Transport.values())
                            .filter(f -> StringUtil.isNotBlank(f.getMapperName()))
                            .filter(f -> f.getMapperName().equals(mapperName))
                            .findFirst()
                            .orElse(UNKNOWN);
                }
            }
        }
    }
}