From 37c7c7670481f90a35ac020f1cf997648072d429 Mon Sep 17 00:00:00 2001 From: Dusan Jakub Date: Tue, 26 Sep 2023 13:57:08 +0200 Subject: [PATCH] First stab at integrating webauthn login to the rest --- .../java/com/ysoft/geecon/OAuthResource.java | 88 ++++++- .../OAuthResource/loginPasswordless.html | 242 ++++++++++++++++++ src/main/resources/templates/base.html | 1 + 3 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 src/main/resources/templates/OAuthResource/loginPasswordless.html diff --git a/src/main/java/com/ysoft/geecon/OAuthResource.java b/src/main/java/com/ysoft/geecon/OAuthResource.java index a622232..7ae6e9c 100644 --- a/src/main/java/com/ysoft/geecon/OAuthResource.java +++ b/src/main/java/com/ysoft/geecon/OAuthResource.java @@ -11,6 +11,11 @@ import com.ysoft.geecon.repo.UsersRepo; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import io.quarkus.runtime.util.StringUtil; +import io.quarkus.security.webauthn.WebAuthnLoginResponse; +import io.quarkus.security.webauthn.WebAuthnRegisterResponse; +import io.quarkus.security.webauthn.WebAuthnSecurity; +import io.vertx.ext.auth.webauthn.Authenticator; +import io.vertx.ext.web.RoutingContext; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; @@ -24,6 +29,27 @@ import java.util.List; @Path("/auth") public class OAuthResource { + @Inject + ClientsRepo clientsRepo; + + @Inject + UsersRepo usersRepo; + @Inject + SessionsRepo sessionsRepo; + @Inject + UriInfo uriInfo; + + @Inject + WebAuthnSecurity webAuthnSecurity; + + @GET + @Produces(MediaType.TEXT_HTML) + public TemplateInstance get(AuthParams params) { + var client = validateClient(params); + String sessionId = sessionsRepo.newAuthorizationSession(params, client); + return Templates.login(params.getLoginHint(), sessionId, ""); + } + @POST @Produces(MediaType.TEXT_HTML) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @@ -43,21 +69,59 @@ public class OAuthResource { } } - @Inject - ClientsRepo clientsRepo; - @Inject - UsersRepo usersRepo; - @Inject - SessionsRepo sessionsRepo; - @Inject - UriInfo uriInfo; - @GET + @Path("passwordless") @Produces(MediaType.TEXT_HTML) - public TemplateInstance get(AuthParams params) { + public TemplateInstance getPasswordless(AuthParams params) { var client = validateClient(params); String sessionId = sessionsRepo.newAuthorizationSession(params, client); - return Templates.login(params.getLoginHint(), sessionId, ""); + return Templates.loginPasswordless(params.getLoginHint(), sessionId, ""); + } + + @POST + @Path("passwordless/register") + @Produces(MediaType.TEXT_HTML) + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public TemplateInstance registerPasswordless(@FormParam("sessionId") String sessionId, + @BeanParam WebAuthnRegisterResponse webAuthnResponse, + RoutingContext ctx) { + + sessionsRepo.getSession(sessionId).orElseThrow( + () -> new OAuthUserVisibleException(ErrorResponse.Error.access_denied, "Invalid session")); + // Input validation + if (!webAuthnResponse.isSet() || !webAuthnResponse.isValid()) { + return Templates.loginPasswordless("", sessionId, "Invalid request"); + } + + Authenticator authenticator = this.webAuthnSecurity.register(webAuthnResponse, ctx) + .await().indefinitely(); + + var user = usersRepo.getUser(authenticator.getUserName()).orElseThrow(); + AuthorizationSession session = sessionsRepo.assignUser(sessionId, user); + return Templates.consents(session.user(), session.client(), session.params().getScopes(), sessionId, ""); + } + + @GET + @Path("passwordless/login") + @Produces(MediaType.TEXT_HTML) + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public TemplateInstance loginPasswordless(@FormParam("sessionId") String sessionId, + @BeanParam WebAuthnLoginResponse webAuthnResponse, + RoutingContext ctx) { + + sessionsRepo.getSession(sessionId).orElseThrow( + () -> new OAuthUserVisibleException(ErrorResponse.Error.access_denied, "Invalid session")); + // Input validation + if (!webAuthnResponse.isSet() || !webAuthnResponse.isValid()) { + return Templates.loginPasswordless("", sessionId, "Invalid request"); + } + + Authenticator authenticator = this.webAuthnSecurity.login(webAuthnResponse, ctx) + .await().indefinitely(); + + var user = usersRepo.getUser(authenticator.getUserName()).orElseThrow(); + AuthorizationSession session = sessionsRepo.assignUser(sessionId, user); + return Templates.consents(session.user(), session.client(), session.params().getScopes(), sessionId, ""); } @POST @@ -212,6 +276,8 @@ public class OAuthResource { public static class Templates { public static native TemplateInstance login(String loginHint, String sessionId, String error); + public static native TemplateInstance loginPasswordless(String loginHint, String sessionId, String error); + public static native TemplateInstance loginSuccess(); public static native TemplateInstance consents(User user, OAuthClient client, List scopes, String sessionId, String error); diff --git a/src/main/resources/templates/OAuthResource/loginPasswordless.html b/src/main/resources/templates/OAuthResource/loginPasswordless.html new file mode 100644 index 0000000..4b5451b --- /dev/null +++ b/src/main/resources/templates/OAuthResource/loginPasswordless.html @@ -0,0 +1,242 @@ +{#include base} + {#title}Login passwordless{/title} + {#add-header} + + + + {/add-header} + +
+
+ + +
+ +
+ The interaction starts with an AJAX call. +
POST +
+
+ +
+ +
+ The server prepares a challenge for the browser to sign. +
+ +
+ + + + + + +
+
+ +
+ +
+
+ + +{/include} + diff --git a/src/main/resources/templates/base.html b/src/main/resources/templates/base.html index 57f5121..f4c2aaa 100644 --- a/src/main/resources/templates/base.html +++ b/src/main/resources/templates/base.html @@ -48,6 +48,7 @@ border: none; } + {#insert add-header}{/} {#insert}No body!{/}