diff options
Diffstat (limited to 'java/vaadin-u2f/src/main')
28 files changed, 1500 insertions, 0 deletions
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/help.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/help.png Binary files differnew file mode 100644 index 0000000..e91d95a --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/help.png diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/home.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/home.png Binary files differnew file mode 100644 index 0000000..7362322 --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/home.png diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_down.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_down.png Binary files differnew file mode 100644 index 0000000..7e1709f --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_down.png diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_up.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_up.png Binary files differnew file mode 100644 index 0000000..cc442d5 --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_up.png diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-128.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-128.png Binary files differnew file mode 100644 index 0000000..52d0761 --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-128.png diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-24.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-24.png Binary files differnew file mode 100644 index 0000000..bd5594a --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-24.png diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-32.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-32.png Binary files differnew file mode 100644 index 0000000..78a15c4 --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-32.png diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-64.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-64.png Binary files differnew file mode 100644 index 0000000..c89c931 --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-64.png diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-lock.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-lock.png Binary files differnew file mode 100644 index 0000000..7823ff2 --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-lock.png diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo-48.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo-48.png Binary files differnew file mode 100644 index 0000000..3641de1 --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo-48.png diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo.png Binary files differnew file mode 100644 index 0000000..a18234b --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo.png diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/layouts/About.html b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/layouts/About.html new file mode 100644 index 0000000..7020e1d --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/layouts/About.html @@ -0,0 +1,10 @@ +<div style='margin:10px'> +<h1>HelloWorld 0.0.0</h1> + +<p>powered by <ul> +<li><a target="_blank" href="https://vaadin.com">vaadin</a> java framework for building modern web applications</li> +<li><a target="_blank" href="http://debian.org">debian</a> a free operating system (OS) for your computer.</li> +</ul></p> +<p>written by <a target="_blank" href="mailto:jeremy@asynk.ch">Jérémy Zurcher</a> +as a part of <a target="_blank" href="https://github.com/jeremyz/share">share</a></p>. +</div> diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/mytheme.scss b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/mytheme.scss new file mode 100644 index 0000000..ad29293 --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/mytheme.scss @@ -0,0 +1,17 @@ + +@import "../valo/valo.scss"; + +@mixin mytheme { + @include valo; + .v-app { background-color: #e6e6e9; } + .mybg { background-color: #e6e6e6; } + .v-verticallayout-margin-top { padding-top: $v-layout-margin-top; } + .v-verticallayout-margin-right { padding-right: $v-layout-margin-top; } + .v-menubar { + .v-menubar-menuitem-menuRight { + position: absolute; + right: 10px; + top: 0; + } + } +} diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/styles.scss b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/styles.scss new file mode 100644 index 0000000..447ce98 --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/styles.scss @@ -0,0 +1,4 @@ +@import "mytheme.scss"; +.mytheme { + @include mytheme; +} diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/widgetsets/mywidgetset.gwt.xml b/java/vaadin-u2f/src/main/WebContent/VAADIN/widgetsets/mywidgetset.gwt.xml new file mode 100644 index 0000000..3c11cb9 --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/widgetsets/mywidgetset.gwt.xml @@ -0,0 +1,3 @@ +<module> + <inherits name="com.vaadin.DefaultWidgetSet" /> +</module> diff --git a/java/vaadin-u2f/src/main/WebContent/WEB-INF/web.xml b/java/vaadin-u2f/src/main/WebContent/WEB-INF/web.xml new file mode 100644 index 0000000..6e03c3d --- /dev/null +++ b/java/vaadin-u2f/src/main/WebContent/WEB-INF/web.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<web-app id="HelloWorld" version="3.0" + xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> + + <display-name>Hello World Vaadin</display-name> + <description> + Hello World in Vaadin + </description> + + <context-param> + <description>Vaadin production mode</description> + <param-name>productionMode</param-name> + <param-value>false</param-value> + </context-param> + + <session-config> + <session-timeout>60</session-timeout> + <!-- <tracking-mode>COOKIE</tracking-mode> --> + </session-config> + + + <!-- <welcome-file-list> --> + <!-- <welcome-file>index.html</welcome-file> --> + <!-- <welcome-file>index.htm</welcome-file> --> + <!-- <welcome-file>index.jsp</welcome-file> --> + <!-- <welcome-file>default.html</welcome-file> --> + <!-- <welcome-file>default.htm</welcome-file> --> + <!-- <welcome-file>default.jsp</welcome-file> --> + <!-- </welcome-file-list> --> +</web-app> diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/Daddy.java b/java/vaadin-u2f/src/main/java/ch/asynk/Daddy.java new file mode 100644 index 0000000..3f279fd --- /dev/null +++ b/java/vaadin-u2f/src/main/java/ch/asynk/Daddy.java @@ -0,0 +1,25 @@ +package ch.asynk; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Daddy +{ + private static final long serialVersionUID = 1L; + + public static final String SESSION_STATUS = "SESSION_STATUS"; + + private static final Logger logger; + + static { + logger = LoggerFactory.getLogger(Daddy.class); + warn("beware Daddy is up"); + } + + public static void trace(String msg) { logger.trace(msg); } + public static void debug(String msg) { logger.debug(msg); } + public static void warn(String msg) { logger.warn(msg); } + public static void info(String msg) { logger.info(msg); } + public static void error(String msg) { logger.error(msg); } + public static void error(String msg, Exception e) { logger.error(String.format("%s : %s", msg, e.getMessage())); } +} diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldServlet.java b/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldServlet.java new file mode 100644 index 0000000..d0e8374 --- /dev/null +++ b/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldServlet.java @@ -0,0 +1,38 @@ +package ch.asynk; + +import javax.servlet.annotation.WebServlet; +import javax.servlet.ServletException; + +import com.vaadin.annotations.VaadinServletConfiguration; +import com.vaadin.server.ServiceException; +import com.vaadin.server.SessionInitEvent; +import com.vaadin.server.SessionInitListener; +import com.vaadin.server.SessionDestroyEvent; +import com.vaadin.server.SessionDestroyListener; +import com.vaadin.server.VaadinServlet; + +@WebServlet(value = "/*", asyncSupported = true) +@VaadinServletConfiguration(productionMode = false, ui = ch.asynk.HelloWorldUI.class, closeIdleSessions = true) +public class HelloWorldServlet extends VaadinServlet implements SessionInitListener, SessionDestroyListener +{ + private static final long serialVersionUID = 511085337415583793L; + @Override + protected void servletInitialized() throws ServletException { + super.servletInitialized(); + getService().addSessionInitListener(this); + getService().addSessionDestroyListener(this); + } + + @Override + public void sessionInit(SessionInitEvent event) throws ServiceException + { + event.getSession().setLocale(new java.util.Locale("fr", "CH")); + event.getSession().setAttribute(Daddy.SESSION_STATUS, "unknown"); + System.err.println("sessionInit"); + } + + @Override + public void sessionDestroy(SessionDestroyEvent event) { + System.err.println("sessionDestroy"); + } +} diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldUI.java b/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldUI.java new file mode 100644 index 0000000..1f293df --- /dev/null +++ b/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldUI.java @@ -0,0 +1,135 @@ +package ch.asynk; + +import com.vaadin.annotations.PreserveOnRefresh; +import com.vaadin.annotations.Theme; +import com.vaadin.annotations.Title; +import com.vaadin.annotations.Widgetset; +import com.vaadin.navigator.Navigator; +import com.vaadin.server.VaadinRequest; +import com.vaadin.ui.Button; +import com.vaadin.ui.Component; +import com.vaadin.ui.CustomLayout; +import com.vaadin.ui.HorizontalSplitPanel; +import com.vaadin.ui.Label; +import com.vaadin.ui.MenuBar; +import com.vaadin.ui.MenuBar.Command; +import com.vaadin.ui.MenuBar.MenuItem; +import com.vaadin.ui.Notification; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; + +import ch.asynk.ui.Icons; +import ch.asynk.ui.ViewMain; + +@PreserveOnRefresh +@Theme("mytheme") +@Title("Hello!!") +@Widgetset("mywidgetset") +public class HelloWorldUI extends UI +{ + private static final long serialVersionUID = 1L; + + private static final String HOME = "Home"; + private static final String ABOUT = "About"; + + private Navigator navigator; + + @Override + protected void init(VaadinRequest request) + { + System.err.println("UI init()"); + final VerticalLayout vl = new VerticalLayout(); + vl.setSizeFull(); + vl.setMargin(true); + setContent(vl); + + final MenuBar menu = createMenuBar(); + + final HorizontalSplitPanel hsplit = new HorizontalSplitPanel(); + hsplit.setSplitPosition(80f); + hsplit.addComponent(createLeftPanel()); + hsplit.addComponent(createRightPanel()); + + vl.addComponents(menu, hsplit); + vl.setExpandRatio(menu, 0); + vl.setExpandRatio(hsplit, 1); + + navigateToHome(); + } + + private Component createLeftPanel() + { + final VerticalLayout vl = new VerticalLayout(); + vl.addStyleName("margin-top"); + vl.addStyleName("margin-right"); + navigator = new Navigator(this, vl); + navigator.addView(HOME, ViewMain.class); + return vl; + } + + private Component createRightPanel() + { + final VerticalLayout vl = new VerticalLayout(); + vl.setMargin(true); + vl.addStyleName("mybg"); + // vl.addStyleName("margin-top"); + // vl.addStyleName("margin-right"); + vl.addComponent(new Label("Hello World using mytheme")); + Button btn = new Button("Push Me!", Icons.home); + btn.addClickListener(new Button.ClickListener() { + private static final long serialVersionUID = 1L; + @Override + public void buttonClick(Button.ClickEvent event) { + Notification.show("Pushed!"); + Daddy.trace("trace"); + Daddy.debug("debug"); + Daddy.info("info"); + Daddy.warn("warn"); + Daddy.error("error"); + } + }); + vl.addComponent(btn); + return vl; + } + + private MenuBar createMenuBar() + { + Command menuCommand = new Command() { + private static final long serialVersionUID = 1L; + @Override + public void menuSelected(MenuItem selectedItem) { + String itemText = selectedItem.getText(); + if (itemText.equals(HOME)) { + navigateToHome(); + } else if (itemText.equals(ABOUT)) { + showAbout(); + } else { + Daddy.error("unhandeled MenuItem : " + itemText); + } + } + }; + + final MenuBar menu = new MenuBar(); + menu.addItem(HOME, Icons.home, menuCommand); + menu.addItem(ABOUT, Icons.help, menuCommand); + MenuBar.MenuItem login = menu.addItem((String) UI.getCurrent().getSession().getAttribute(Daddy.SESSION_STATUS), menuCommand); + login.setStyleName("menuRight"); + menu.setWidth("100%"); + + return menu; + } + + public void navigateToHome() { navigator.navigateTo(HOME); } + + private void showAbout() + { + final Window about = new Window(ABOUT); + about.setContent( new CustomLayout(ABOUT) ); + about.setHeight("50%"); + about.setWidth("600px"); + about.center(); + about.setModal(true); + UI.getCurrent().addWindow(about); + } +} diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fConnector.java b/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fConnector.java new file mode 100644 index 0000000..24b406d --- /dev/null +++ b/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fConnector.java @@ -0,0 +1,164 @@ +package ch.asynk.security; + +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.annotations.JavaScript; +import com.vaadin.server.AbstractJavaScriptExtension; +import com.vaadin.ui.UI; +import com.vaadin.ui.JavaScriptFunction; +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 com.yubico.u2f.exceptions.NoEligibleDevicesException; +import elemental.json.JsonArray; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@JavaScript({"u2f-api.js", "u2f_connector.js"}) +public class U2fConnector extends AbstractJavaScriptExtension +{ + private static final long serialVersionUID = 1L; + + private static final Logger logger = LoggerFactory.getLogger(U2fConnector.class); + + // appId MUST match the URL - https://developers.yubico.com/U2F/App_ID.html + private final String appId = "https://localhost:8666"; + + private final U2F u2f = new U2F(); + // private final U2F u2f = U2F.withoutAppIdValidation(); + + // https://developers.yubico.com/U2F/Libraries/Client_error_codes.html + private static String U2F_ERRORS[] = {"","OTHER_ERROR","BAD_REQUEST","CONFIGURATION_UNSUPPORTED","DEVICE_INELIGIBLE","TIMEOUT","DEVICE_COMPROMISED"}; + public String getErrorMsg(int i) { return U2F_ERRORS[i]; } + + private final U2fService u2fService = new U2fServiceImpl(); + + class Request + { + String userId; + String data; + U2fListener listener; + public Request(String userId, String data, U2fListener listener) + { + this.userId = userId; + this.data = data; + this.listener = listener; + } + } + + private final Map<String, Request> requests = new HashMap<>(); + + public enum U2fAction { + REGISTRATION_PENDING, + REGISTRATION_SUCCESS, + REGISTRATION_FAILURE, + AUTHENTICATION_PENDING, + AUTHENTICATION_FAILURE, + AUTHENTICATION_SUCCESS + }; + + public interface U2fListener + { + public void u2fCallback(U2fAction action, String msg); + } + + public U2fConnector() { + extend(UI.getCurrent()); + addFunction("onRegisterResponse", new JavaScriptFunction() { + private static final long serialVersionUID = 1L; + @Override + public void call(final JsonArray arguments) { + onRegisterResponse(arguments); + } + }); + addFunction("onAuthenticateResponse", new JavaScriptFunction() { + private static final long serialVersionUID = 1L; + @Override + public void call(final JsonArray arguments) { + onAuthenticateResponse(arguments); + } + }); + } + + public void sendRegisterRequest(final String userId, final U2fListener listener) + { + final RegisterRequestData registerRequestData = u2f.startRegistration(appId, u2fService.getDeviceRegistrations(userId)); + final String registerRequestDataJson = registerRequestData.toJson(); + final String registerRequestDataId = registerRequestData.getRequestId(); + requests.put(registerRequestDataId, new Request(userId, registerRequestDataJson, listener)); + callFunction("register", registerRequestDataJson); + listener.u2fCallback(U2fAction.REGISTRATION_PENDING, null); + logger.debug(String.format("register[%s] : %s", userId, registerRequestDataJson)); + } + + public void onRegisterResponse(JsonArray arguments) + { + if (arguments.get(0) instanceof elemental.json.impl.JreJsonNumber) { + int errorCode = (int) arguments.getNumber(0); + // FIXME why does it not work ?? + // final RegisterRequestData registerRequestData = RegisterRequestData.fromJson(arguments.getString(1)); + final String registerRequestDataJson = arguments.getObject(1).toString(); + final RegisterRequestData registerRequestData = RegisterRequestData.fromJson(registerRequestDataJson); + final Request request = requests.remove(registerRequestData.getRequestId()); + request.listener.u2fCallback(U2fAction.REGISTRATION_FAILURE, getErrorMsg(errorCode)); + logger.debug(String.format("failure[%d] : %s", errorCode, registerRequestDataJson)); + } else { + final String registerResponseJson = arguments.getString(0); + final RegisterResponse registerResponse = RegisterResponse.fromJson(registerResponseJson); + final Request request = requests.remove(registerResponse.getRequestId()); + final RegisterRequestData registerRequestData = RegisterRequestData.fromJson(request.data); + u2fService.addDeviceRegistration(request.userId, u2f.finishRegistration(registerRequestData, registerResponse)); + request.listener.u2fCallback(U2fAction.REGISTRATION_SUCCESS, null); + logger.debug(String.format("success : %s", registerResponseJson)); + } + } + + public void startAuthentication(final String userId, final U2fListener listener) + { + try { + final AuthenticateRequestData authenticateRequestData = u2f.startAuthentication(appId, u2fService.getDeviceRegistrations(userId)); + final String authenticateRequestDataJson = authenticateRequestData.toJson(); + final String authenticateRequestDataId = authenticateRequestData.getRequestId(); + requests.put(authenticateRequestDataId, new Request(userId, authenticateRequestDataJson, listener)); + callFunction("authenticate", authenticateRequestDataJson, userId); + listener.u2fCallback(U2fAction.AUTHENTICATION_PENDING, null); + logger.debug(String.format("authenticate[%s] : %s", userId, authenticateRequestDataJson)); + } catch (NoEligibleDevicesException e) { + listener.u2fCallback(U2fAction.AUTHENTICATION_FAILURE, getErrorMsg(4)); + } + } + + public void onAuthenticateResponse(JsonArray arguments) + { + if (arguments.get(0) instanceof elemental.json.impl.JreJsonNumber) { + int errorCode = (int) arguments.getNumber(0); + // FIXME why does it not work ?? + // final AuthenticateRequestData authenticateRequestData = AuthenticateRequestData.fromJson(arguments.getString(1)); + final String authenticateRequestDataJson = arguments.getObject(1).toString(); + final AuthenticateRequestData authenticateRequestData = AuthenticateRequestData.fromJson(authenticateRequestDataJson); + final Request request = requests.remove(authenticateRequestData.getRequestId()); + request.listener.u2fCallback(U2fAction.AUTHENTICATION_FAILURE, getErrorMsg(errorCode)); + logger.debug(String.format("failure[%d] : %s", errorCode, authenticateRequestDataJson)); + } else { + final String authenticateResponseJson = arguments.getString(0); + final AuthenticateResponse authenticateResponse = AuthenticateResponse.fromJson(authenticateResponseJson); + final Request request = requests.remove(authenticateResponse.getRequestId()); + final AuthenticateRequestData authenticateRequestData = AuthenticateRequestData.fromJson(request.data); + DeviceRegistration registration = null; + try { + registration = u2f.finishAuthentication(authenticateRequestData, authenticateResponse, u2fService.getDeviceRegistrations(request.userId)); + } catch (final DeviceCompromisedException e) { + request.listener.u2fCallback(U2fAction.AUTHENTICATION_FAILURE, getErrorMsg(6)); + logger.error(String.format("device compromised: %s", request.data)); + } + request.listener.u2fCallback(U2fAction.AUTHENTICATION_SUCCESS, null); + logger.debug(String.format("success : %s", authenticateResponseJson)); + } + } +} diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fService.java b/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fService.java new file mode 100644 index 0000000..9bce94e --- /dev/null +++ b/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fService.java @@ -0,0 +1,12 @@ +package ch.asynk.security; + +import java.util.List; + +import com.yubico.u2f.data.DeviceRegistration; + +public interface U2fService +{ + public boolean hasDeviceRegistrations(final String userId); + public List<DeviceRegistration> getDeviceRegistrations(final String userId); + public void addDeviceRegistration(final String userId, final DeviceRegistration deviceRegistration); +} diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fServiceImpl.java b/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fServiceImpl.java new file mode 100644 index 0000000..f8e785f --- /dev/null +++ b/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fServiceImpl.java @@ -0,0 +1,43 @@ +package ch.asynk.security; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.yubico.u2f.data.DeviceRegistration; + +public class U2fServiceImpl implements U2fService +{ + private HashMap<String, List<DeviceRegistration>> store; + + public U2fServiceImpl() + { + store = new HashMap<String, List<DeviceRegistration>>(); + } + + public boolean hasDeviceRegistrations(final String userId) + { + return store.containsKey(userId); + } + + public List<DeviceRegistration> getDeviceRegistrations(final String userId) + { + return get(userId); + } + + public void addDeviceRegistration(final String userId, final DeviceRegistration deviceRegistration) + { + List<DeviceRegistration> registrations = get(userId); + registrations.add(deviceRegistration); + store.put(userId, registrations); + } + + private List<DeviceRegistration> get(final String userId) + { + List<DeviceRegistration> registrations = store.get(userId); + if (registrations == null) + registrations = new ArrayList<DeviceRegistration>(); + return registrations; + } +} diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/security/u2f-api.js b/java/vaadin-u2f/src/main/java/ch/asynk/security/u2f-api.js new file mode 100644 index 0000000..9244d14 --- /dev/null +++ b/java/vaadin-u2f/src/main/java/ch/asynk/security/u2f-api.js @@ -0,0 +1,748 @@ +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ +'use strict'; + + +/** + * Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * FIDO U2F Javascript API Version + * @number + */ +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. + u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' +}; + + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + + +/** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.U2fRequest; + + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.U2fResponse; + + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array<u2f.Transport>} + */ +u2f.Transports; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ +u2f.RegisterRequest; + + +/** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ +u2f.RegisterResponse; + + +/** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { + return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedIosPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array<u2f.SignRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatSignRequest_ = + function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + +/** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array<u2f.SignRequest>} signRequests + * @param {Array<u2f.RegisterRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatRegisterRequest_ = + function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } +}; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { + return "WrappedAuthenticatorPort_"; +}; + + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } +}; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } + + callback({'data': responseObject}); +}; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedIosPort_.prototype.getPortType = function() { + return "WrappedIosPort_"; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } +}; + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +//High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse)) + * |function((u2f.Error|u2f.SignResponse)))>} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.<u2f.Response>} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array<u2f.RegisterRequest>} registerRequests + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array<u2f.RegisterRequest>} registerRequests + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); +}; diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/security/u2f_connector.js b/java/vaadin-u2f/src/main/java/ch/asynk/security/u2f_connector.js new file mode 100644 index 0000000..8723cb3 --- /dev/null +++ b/java/vaadin-u2f/src/main/java/ch/asynk/security/u2f_connector.js @@ -0,0 +1,30 @@ +window.ch_asynk_security_U2fConnector = function() { + + var connector = this; + + this.register = function(requestJson) { + var request = JSON.parse(requestJson); + // alert(JSON.stringify(request)); + setTimeout(function() { + u2f.register(request.registerRequests, request.authenticateRequests, function(data) { + if(data.errorCode) + connector.onRegisterResponse(data.errorCode, request); + else + connector.onRegisterResponse(JSON.stringify(data)); + }); + }, 500); + } + + this.authenticate = function(requestJson) { + var request = JSON.parse(requestJson); + //alert(JSON.stringify(request)); + setTimeout(function() { + u2f.sign(request.authenticateRequests, function(data) { + if(data.errorCode) + connector.onAuthenticateResponse(data.errorCode, request); + else + connector.onAuthenticateResponse(JSON.stringify(data)); + }); + }, 500); + } +} diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/ui/Icons.java b/java/vaadin-u2f/src/main/java/ch/asynk/ui/Icons.java new file mode 100644 index 0000000..3cc96bc --- /dev/null +++ b/java/vaadin-u2f/src/main/java/ch/asynk/ui/Icons.java @@ -0,0 +1,18 @@ +package ch.asynk.ui; + +import com.vaadin.server.ThemeResource; + +public class Icons +{ + public static final ThemeResource help = new ThemeResource("icons/help.png"); + public static final ThemeResource home = new ThemeResource("icons/home.png"); + public static final ThemeResource u2f128 = new ThemeResource("icons/u2f-128.png"); + public static final ThemeResource u2f64 = new ThemeResource("icons/u2f-64.png"); + public static final ThemeResource u2f32 = new ThemeResource("icons/u2f-32.png"); + public static final ThemeResource u2f24 = new ThemeResource("icons/u2f-24.png"); + public static final ThemeResource u2flogo = new ThemeResource("icons/u2f-logo.png"); + public static final ThemeResource u2flogo48 = new ThemeResource("icons/u2f-logo-48.png"); + public static final ThemeResource u2flock = new ThemeResource("icons/u2f-lock.png"); + public static final ThemeResource thumbUp = new ThemeResource("icons/thumb_up.png"); + public static final ThemeResource thumbDown = new ThemeResource("icons/thumb_down.png"); +} diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/ui/ViewMain.java b/java/vaadin-u2f/src/main/java/ch/asynk/ui/ViewMain.java new file mode 100644 index 0000000..8fae5f0 --- /dev/null +++ b/java/vaadin-u2f/src/main/java/ch/asynk/ui/ViewMain.java @@ -0,0 +1,176 @@ +package ch.asynk.ui; + +import com.vaadin.data.validator.RegexpValidator; +import com.vaadin.navigator.View; +import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent; +import com.vaadin.server.Page; +import com.vaadin.data.Binder; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.Component; +import com.vaadin.ui.Image; +import com.vaadin.ui.Label; +import com.vaadin.ui.Notification; +import com.vaadin.ui.Panel; +import com.vaadin.ui.TabSheet; +import com.vaadin.ui.TextField; +import com.vaadin.ui.UI; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; + +import ch.asynk.security.U2fConnector; + +class Login +{ + private String login; + public String getLogin() { return login; } + public void setLogin(final String login) { this.login = login; } +} + +public class ViewMain extends TabSheet implements View, U2fConnector.U2fListener +{ + private static final long serialVersionUID = 1L; + + private static final String U2F_TITLE = "FIDO U2F (Universal 2nd factor)"; + + private final Window u2fWindow; + private final U2fConnector u2fConnector; + + private enum Action { U2F_REGISTER, U2F_AUTHENTICATE } + + public ViewMain() + { + u2fWindow = u2fWindow(); + u2fConnector = new U2fConnector(); + addStyleName("framed"); + addTab(u2fComponent("register", Action.U2F_REGISTER), "Register", Icons.u2flogo48); + addTab(u2fComponent("authenticate", Action.U2F_AUTHENTICATE), "Authenticate", Icons.u2f32); + setSelectedTab(2); + } + + @Override + public void enter(ViewChangeEvent event) { + System.err.println(this.getClass().getName()+"::enter"); + } + + private VerticalLayout content(String s) + { + VerticalLayout ly = new VerticalLayout(); + ly.addComponent(new Label(" Hi, this is " + s)); + return ly; + } + + private Component u2fComponent(final String title, Action action) + { + final Login login = new Login(); + final TextField tf = new TextField(); + final Button bt = new Button(title.toLowerCase()); + final Binder<Login> binder = new Binder<>(); + binder.addValueChangeListener(evt -> bt.setEnabled(binder.isValid()) ); + binder.forField(tf) + .asRequired("Login required") + .withValidator(s -> s.length() >= 6, "at least 6 characters") + .withValidator(s -> s.length() <= 20, "at most 20 characters") + .withValidator(new RegexpValidator("invalid username", "^(?=.{6,20}$)[a-zA-Z][a-zA-Z0-9#@.]+[a-zA-Z]$")) + .bind(Login::getLogin, Login::setLogin); + + bt.setEnabled(false); + bt.addClickListener(e -> { + if (binder.writeBeanIfValid(login)) { + tf.setValue(""); + tryAction(action, login); + } + }); + + Panel panel = new Panel(title); + panel.setWidth("450px"); + final GridLayout ly = new GridLayout(2,2); + ly.setMargin(true); + ly.setSpacing(true); + panel.setContent(ly); + ly.addComponent(new Image(null, Icons.u2flock), 0, 0, 0, 1); + ly.addComponent(tf, 1, 0); + ly.addComponent(bt,1,1); + ly.setComponentAlignment(bt, Alignment.MIDDLE_CENTER); + + VerticalLayout vl = new VerticalLayout(); + vl.setMargin(true); + vl.setSizeFull(); + vl.addComponent(panel); + vl.setComponentAlignment(panel, Alignment.MIDDLE_CENTER); + return vl; + } + + private void tryAction(Action action, Login login) + { + final String userId = login.getLogin(); + login.setLogin(null); + try { + if (action == Action.U2F_REGISTER) + u2fConnector.sendRegisterRequest(userId, this); + else if (action == Action.U2F_AUTHENTICATE) + u2fConnector.startAuthentication(userId, this); + } catch (com.yubico.u2f.exceptions.U2fBadConfigurationException ex) { + Notification.show(U2F_TITLE, ex.getMessage(), Notification.Type.ERROR_MESSAGE); + } + } + + public void u2fCallback(U2fConnector.U2fAction action, String msg) + { + Notification n = null; + switch (action) { + case REGISTRATION_PENDING: + case AUTHENTICATION_PENDING: + UI.getCurrent().addWindow(u2fWindow); + break; + case REGISTRATION_SUCCESS: + msg = "Your token has been successfully registred."; + break; + case REGISTRATION_FAILURE: + msg = "Registration failed with error : " + msg; + break; + case AUTHENTICATION_SUCCESS: + msg = "You have been successfully authenticated."; + break; + case AUTHENTICATION_FAILURE: + msg = "Authentication failed with error : " + msg; + break; + } + switch (action) { + case REGISTRATION_SUCCESS: + case AUTHENTICATION_SUCCESS: + n = new Notification(U2F_TITLE, msg, Notification.Type.HUMANIZED_MESSAGE); + n.setIcon(Icons.thumbUp); + break; + case REGISTRATION_FAILURE: + case AUTHENTICATION_FAILURE: + n = new Notification(U2F_TITLE, msg, Notification.Type.ERROR_MESSAGE); + n.setIcon(Icons.thumbDown); + break; + } + if (n != null) { + n.setDelayMsec(5000); + u2fWindow.close(); + n.show(Page.getCurrent()); + } + } + + private Window u2fWindow() + { + Window w = new Window(U2F_TITLE); + final VerticalLayout vy = new VerticalLayout(); + vy.setMargin(true); + vy.setSpacing(true); + vy.addComponent(new Label("Insert your u2f token and click on it.")); + vy.addComponent(new Image(null, Icons.u2flock)); + w.setModal(true); + w.setClosable(false); + w.setContent(vy); + w.setResizable(false); + w.setWidth("280px"); + w.setHeight("220px"); + w.center(); + return w; + } +} diff --git a/java/vaadin-u2f/src/main/resources/Application.properties b/java/vaadin-u2f/src/main/resources/Application.properties new file mode 100644 index 0000000..512362b --- /dev/null +++ b/java/vaadin-u2f/src/main/resources/Application.properties @@ -0,0 +1,5 @@ +app.name=VaadinHelloWorld +app.version=0.0.1 +app.path=/hello +app.env=DEV +app.logfile=/var/log/tomcat7/HelloWorld.log diff --git a/java/vaadin-u2f/src/main/resources/logback-test.xml b/java/vaadin-u2f/src/main/resources/logback-test.xml new file mode 100644 index 0000000..a8fcf4b --- /dev/null +++ b/java/vaadin-u2f/src/main/resources/logback-test.xml @@ -0,0 +1,41 @@ +<configuration debug="true"> + + <contextName>HelloWorld</contextName> + <property resource="Application.properties" /> + <property name="LOG_LEVEL" value="INFO" /> + <property name="LOG_REF" value="FILE" /> + <property name="LOG_FILE" value="${app.logfile}" /> + <property name="LOG_PATTERN" value="%contextName %date{ISO8601} [%-5level %thread] [%file:%line] - %msg%n" /> + + <if condition='property("app.env").contains("DEV")'> + <then> + <property name="LOG_LEVEL" value="DEBUG" /> + </then> + </if> + + <if condition='isDefined("TEST")'> + <then> + <property name="LOG_FILE" value="/tmp/${app.name}.log" /> + <property name="LOG_LEVEL" value="DEBUG" /> + <property name="LOG_REF" value="STDOUT" /> + </then> + </if> + + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>${LOG_PATTERN}</pattern> + </encoder> + </appender> + + <appender name="FILE" class="ch.qos.logback.core.FileAppender"> + <file>${LOG_FILE}</file> + <encoder> + <pattern>${LOG_PATTERN}</pattern> + </encoder> + </appender> + + <root level="${LOG_LEVEL}"> + <appender-ref ref="${LOG_REF}" /> + </root> + +</configuration> |