/** * Copyright 2013 Tommi S.E. Laukkanen * * 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.bubblecloud.ilves.security; import com.vaadin.annotations.JavaScript; import com.vaadin.server.AbstractJavaScriptExtension; import com.vaadin.server.Page; import com.vaadin.server.Sizeable; import com.vaadin.ui.*; import com.yubico.u2f.U2F; import com.yubico.u2f.data.DeviceRegistration; import com.yubico.u2f.data.messages.AuthenticateRequestData; import com.yubico.u2f.data.messages.AuthenticateResponse; import com.yubico.u2f.data.messages.RegisterRequestData; import com.yubico.u2f.data.messages.RegisterResponse; import com.yubico.u2f.exceptions.DeviceCompromisedException; import elemental.json.JsonArray; import elemental.json.impl.JreJsonNull; import org.apache.log4j.Logger; import org.bubblecloud.ilves.model.Company; import org.bubblecloud.ilves.model.User; import org.bubblecloud.ilves.site.SecurityProviderSessionImpl; import org.bubblecloud.ilves.site.Site; import java.util.HashMap; import java.util.Map; /** * Universal second factor (U2F) JavaScript API connector. * * @author Tommi S.E. Laukkanen */ @JavaScript({"u2f-api.js", "u2f_connector.js"}) public class U2fConnector extends AbstractJavaScriptExtension { /** The logger. */ private static final Logger LOGGER = Logger.getLogger(U2fConnector.class); /** * The server side U2F API implementation. */ private final U2F u2f = new U2F(); /** * The existing device register requests. */ private final Map<String, String> requests = new HashMap<>(); /** * The register window. */ private final Window registerWindow = new Window(Site.getCurrent().localize("header-register-u2f-device")); /** * The site. */ private final Site site; /** * The company. */ private final Company company; /** * The APP ID. */ private final String appId; /** * U2F registration listener. */ private U2fRegistrationListener u2FRegistrationListener; /** * U2F authentication listener. */ private U2fAuthenticationListener u2fAuthenticationListener; /** * Constructor for setting up the JavaScript connector. */ public U2fConnector() { extend(UI.getCurrent()); addFunction("onRegisterResponse", new JavaScriptFunction() { @Override public void call(final JsonArray arguments) { onReqisterResponse(arguments); } }); addFunction("onAuthenticateResponse", new JavaScriptFunction() { @Override public void call(final JsonArray arguments) { onAuthenticateResponse(arguments); } }); site = Site.getCurrent(); company = site.getSiteContext().getObject(Company.class); appId = company.getUrl().charAt(company.getUrl().length() - 1) == '/' ? company.getUrl().substring(0, company.getUrl().length() - 1) : company.getUrl(); } /** * Start the registration process. */ public void startRegistration(final U2fRegistrationListener u2FRegistrationListener) { this.u2FRegistrationListener = u2FRegistrationListener; sendRegisterRequest(); registerWindow.setModal(true); final VerticalLayout verticalLayout = new VerticalLayout(); verticalLayout.setMargin(true); final Label label = new Label(Site.getCurrent().localize("message-insert-u2f-device")); verticalLayout.addComponent(label); verticalLayout.setComponentAlignment(label, Alignment.MIDDLE_CENTER); registerWindow.setContent(verticalLayout); registerWindow.setResizable(false); registerWindow.setWidth(300, Sizeable.Unit.PIXELS); registerWindow.setHeight(200, Sizeable.Unit.PIXELS); registerWindow.center(); UI.getCurrent().addWindow(registerWindow); } /** * Send registration request to U2F JavaScript API. */ private void sendRegisterRequest() { final User user = ((SecurityProviderSessionImpl) site.getSecurityProvider()).getUserFromSession(); try { final RegisterRequestData registerRequestData = u2f.startRegistration(appId, U2fService.getDeviceRegistrations(site.getSiteContext(), user.getEmailAddress())); requests.put(registerRequestData.getRequestId(), registerRequestData.toJson()); callFunction("register", registerRequestData.toJson()); } catch(final Exception e) { LOGGER.error("Error sending U2F registration request.", e); new Notification( site.localize("message-u2f-device-registration-failed"), Notification.Type.ERROR_MESSAGE).show(Page.getCurrent()); } } /** * Event handler for register response from U2F JavaScript API. * @param arguments the response arguments (data and error code) */ public void onReqisterResponse(JsonArray arguments) { registerWindow.close(); try { final User user = ((SecurityProviderSessionImpl) site.getSecurityProvider()).getUserFromSession(); if (arguments.length() == 2 && !(arguments.get(1) instanceof JreJsonNull)) { final double errorCode = arguments.getNumber(1); LOGGER.error("Error processing U2F registration due to error code: " + errorCode); new Notification( site.localize("message-u2f-device-registration-failed") + " (" + errorCode + ")", Notification.Type.ERROR_MESSAGE).show(Page.getCurrent()); return; } final RegisterResponse registerResponse = RegisterResponse.fromJson(arguments.getString(0)); final RegisterRequestData registerRequestData = RegisterRequestData.fromJson(requests.remove(registerResponse.getRequestId())); final DeviceRegistration registration = u2f.finishRegistration(registerRequestData, registerResponse); U2fService.addDeviceRegistration(site.getSiteContext(), user.getEmailAddress(), registration); AuditService.log(site.getSiteContext(), "u2f device register"); u2FRegistrationListener.onDeviceRegistrationSuccess(); new Notification( site.localize("message-u2f-device-registered"), Notification.Type.HUMANIZED_MESSAGE).show(Page.getCurrent()); } catch(final Exception e) { LOGGER.error("Error processing U2F registration response.", e); new Notification( site.localize("message-u2f-device-registration-failed"), Notification.Type.ERROR_MESSAGE).show(Page.getCurrent()); } } /** * Field for holding email address between authenticate request and response call. */ private String authenticateEmailAddress = null; /** * Starts authentication. * @param emailAddress the email address * @param u2fAuthenticationListener the U2F authentication listener */ public void startAuthentication(final String emailAddress, final U2fAuthenticationListener u2fAuthenticationListener) { this.u2fAuthenticationListener = u2fAuthenticationListener; sendAuthenticateRequest(emailAddress); } /** * Send authenticate request to U2F JavaScript API. */ private void sendAuthenticateRequest(final String emailAddress) { this.authenticateEmailAddress = emailAddress; try { final AuthenticateRequestData authenticateRequestDataa = u2f.startAuthentication(appId, U2fService.getDeviceRegistrations(site.getSiteContext(), emailAddress)); requests.put(authenticateRequestDataa.getRequestId(), authenticateRequestDataa.toJson()); callFunction("authenticate", authenticateRequestDataa.toJson(), emailAddress); } catch(final Exception e) { LOGGER.error("Error sending U2F authentication request.", e); new Notification( site.localize("message-u2f-authentication-failed"), Notification.Type.ERROR_MESSAGE).show(Page.getCurrent()); } } /** * Event handler for authenticate response from U2F JavaScript API. * @param arguments the response arguments (data and error code) */ public void onAuthenticateResponse(JsonArray arguments) { registerWindow.close(); try { if (arguments.length() == 2 && !(arguments.get(1) instanceof JreJsonNull)) { final double errorCode = arguments.getNumber(1); LOGGER.error("Error processing U2F authentication due to error code: " + errorCode); new Notification( site.localize("message-u2f-authentication-failed") + " (" + errorCode + ")", Notification.Type.ERROR_MESSAGE).show(Page.getCurrent()); u2fAuthenticationListener.onDeviceAuthenticationFailure(); return; } final AuthenticateResponse authenticateResponse = AuthenticateResponse.fromJson(arguments.getString(0)); final AuthenticateRequestData authenticateRequest = AuthenticateRequestData.fromJson(requests.remove(authenticateResponse.getRequestId())); DeviceRegistration registration = null; try { registration = u2f.finishAuthentication(authenticateRequest, authenticateResponse, U2fService.getDeviceRegistrations(site.getSiteContext(), authenticateEmailAddress)); } catch (final DeviceCompromisedException e) { registration = e.getDeviceRegistration(); LOGGER.error("Device compromised."); new Notification( site.localize("message-u2f-device-compromised"), Notification.Type.ERROR_MESSAGE).show(Page.getCurrent()); } finally { U2fService.updateDeviceRegistration(site.getSiteContext(), authenticateEmailAddress, registration); } AuditService.log(site.getSiteContext(), "u2f authentication success"); /*new Notification( site.localize("message-u2f-device-authentication success"), Notification.Type.HUMANIZED_MESSAGE).show(Page.getCurrent());*/ u2fAuthenticationListener.onDeviceAuthenticationSuccess(authenticateEmailAddress); } catch(final Exception e) { LOGGER.error("Error processing U2F authenticate response.", e); new Notification( site.localize("message-u2f-authentication-failed"), Notification.Type.ERROR_MESSAGE).show(Page.getCurrent()); u2fAuthenticationListener.onDeviceAuthenticationFailure(); } } }