x5cs = new ArrayList<>(credential.x5c.size());
+ for (WebAuthnCertificate webAuthnCertificate : credential.x5c) {
+ x5cs.add(webAuthnCertificate.x5c);
+ }
+ ret.setAttestationCertificates(attestationCertificates);
+ ret.setCounter(credential.counter);
+ ret.setCredID(credential.credID);
+ ret.setFmt(credential.fmt);
+ ret.setPublicKey(credential.publicKey);
+ ret.setType(credential.type);
+ ret.setUserName(credential.userName);
+ return ret;
+ }
+}
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/java/util/Startup.java b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/java/util/Startup.java
new file mode 100644
index 000000000..c20ef4d71
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/java/util/Startup.java
@@ -0,0 +1,40 @@
+package util;
+
+import java.util.Date;
+
+import javax.enterprise.event.Observes;
+import javax.transaction.Transactional;
+
+import io.quarkus.elytron.security.common.BcryptUtil;
+import io.quarkus.runtime.StartupEvent;
+import model.Todo;
+import model.User;
+import model.UserStatus;
+
+public class Startup {
+ @Transactional
+ public void onStartup(@Observes StartupEvent start) {
+ System.err.println("Adding user fromage");
+ User stef = new User();
+ stef.email = "fromage@example.com";
+ stef.firstName = "Stef";
+ stef.lastName = "Epardaud";
+ stef.userName = "fromage";
+ stef.password = BcryptUtil.bcryptHash("1q2w3e4r");
+ stef.status = UserStatus.REGISTERED;
+ stef.isAdmin = true;
+ stef.persist();
+
+ Todo todo = new Todo();
+ todo.owner = stef;
+ todo.task = "Buy cheese";
+ todo.done = true;
+ todo.doneDate = new Date();
+ todo.persist();
+
+ todo = new Todo();
+ todo.owner = stef;
+ todo.task = "Eat cheese";
+ todo.persist();
+ }
+}
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/java/util/Util.java b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/java/util/Util.java
new file mode 100644
index 000000000..3febe0ada
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/java/util/Util.java
@@ -0,0 +1,7 @@
+package util;
+
+public class Util {
+
+ public static final int VARCHAR_SIZE = 255;
+
+}
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/Apple-key-dev.p8 b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/Apple-key-dev.p8
new file mode 100644
index 000000000..1efa33177
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/Apple-key-dev.p8
@@ -0,0 +1,5 @@
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQNNVeHUtGDGEJGEM
+UcCzTr4DC8Yf9NJ/LrXmU6LJHQmhRANCAATDS9rjbOzoeEt+lHlbaWnBXIdqlZBn
+gIT+qieWnhWyIaf1pTIbGp2AI0shGMSzU+zflug7Gy+BkQExbihnP3Ly
+-----END PRIVATE KEY-----
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-apple.svg b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-apple.svg
new file mode 100644
index 000000000..73630c9ba
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-apple.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-facebook.svg b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-facebook.svg
new file mode 100644
index 000000000..7382e4681
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-facebook.svg
@@ -0,0 +1,63 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-github.svg b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-github.svg
new file mode 100644
index 000000000..cdafcd125
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-github.svg
@@ -0,0 +1,51 @@
+
+image/svg+xml
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-google.svg b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-google.svg
new file mode 100644
index 000000000..c652a5a18
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-google.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-microsoft.svg b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-microsoft.svg
new file mode 100644
index 000000000..5334aa7ca
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-microsoft.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-twitter.svg b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-twitter.svg
new file mode 100644
index 000000000..b60552810
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/signin-twitter.svg
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/webauthn.svg b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/webauthn.svg
new file mode 100644
index 000000000..0dde24a44
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/images/webauthn.svg
@@ -0,0 +1,89 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/javascripts/main.js b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/javascripts/main.js
new file mode 100644
index 000000000..d4bec2c7c
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/javascripts/main.js
@@ -0,0 +1,35 @@
+// INSERT YOUR JS HERE
+
+function clearValidationError(inputElement) {
+ // remove any lingering error
+ const previousError = inputElement.parentElement.querySelector(".invalid-feedback");
+ if(previousError) {
+ previousError.remove();
+ }
+}
+
+function addValidationError(inputElement, error){
+ clearValidationError(inputElement);
+ inputElement.classList.add('is-invalid');
+ // add ​error
+ const span = document.createElement("span");
+ span.classList.add("invalid-feedback");
+ span.append(error);
+ inputElement.parentElement.append(span);
+}
+
+function requireField(name){
+ const field = document.getElementById(name);
+ clearValidationError(field);
+ if(!field.value || field.value.length == 0) {
+ addValidationError(field, "must not be blank");
+ return Promise.reject("must not be blank");
+ } else {
+ return Promise.resolve(field.value);
+ }
+}
+
+function requireFields(...args){
+ return Promise.all(args.map(arg => requireField(arg)));
+}
+
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/stylesheets/main.css b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/stylesheets/main.css
new file mode 100644
index 000000000..e12023d97
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/META-INF/resources/stylesheets/main.css
@@ -0,0 +1,4 @@
+/* INSERT YOUR STYLE HERE */
+.inline {
+ display: inline;
+}
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/application-test.properties b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/application-test.properties
new file mode 100644
index 000000000..fbc1c484f
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/application-test.properties
@@ -0,0 +1,33 @@
+# Google
+quarkus.oidc.google.client-id=GGLCLIENT
+quarkus.oidc.google.credentials.secret=GGLSECRET
+
+# Github
+quarkus.oidc.github.client-id=GHCLIENT
+# FIXME: must be long otherwise we get an exception:
+#io.smallrye.jwt.build.JwtSignatureException: SRJWT05012: Failure to create a signed JWT token: A key of the same size as the hash output (i.e. 256 bits for HS256) or larger MUST be used with the HMAC SHA algorithms but this key is only 64 bits
+# at io.smallrye.jwt.build.impl.JwtSignatureImpl.signInternal(JwtSignatureImpl.java:150)
+# at io.smallrye.jwt.build.impl.JwtSignatureImpl.sign(JwtSignatureImpl.java:50)
+# at io.quarkus.oidc.runtime.CodeAuthenticationMechanism.generateInternalIdToken(CodeAuthenticationMechanism.java:406)
+quarkus.oidc.github.credentials.secret=GHSECRETGHSECRETGHSECRETGHSECRET
+
+# Twitter
+quarkus.oidc.twitter.client-id=TWCLIENT
+quarkus.oidc.twitter.credentials.secret=TWSECRETTWSECRETTWSECRETTWSECRET
+
+# MS
+quarkus.oidc.microsoft.client-id=MSCLIENT
+quarkus.oidc.microsoft.credentials.secret=MSSECRET
+
+# Facebook
+quarkus.oidc.facebook.client-id=FBCLIENT
+quarkus.oidc.facebook.credentials.secret=FBSECRETFBSECRETFBSECRETFBSECRETFBSECRET
+
+# Apple
+quarkus.oidc.apple.client-id=APLCLIENT
+#set by mock
+#quarkus.oidc.apple.credentials.jwt.key-file=apple-key.txt
+quarkus.oidc.apple.credentials.jwt.token-key-id=APLKEYID
+quarkus.oidc.apple.credentials.jwt.issuer=APLISSUER
+quarkus.oidc.apple.credentials.jwt.subject=APLSUBJECT
+
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/application.properties b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/application.properties
new file mode 100644
index 000000000..75fd572d9
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/application.properties
@@ -0,0 +1,51 @@
+
+# Twitter
+quarkus.oidc.twitter.provider=twitter
+quarkus.oidc.twitter.client-id=SECRET
+quarkus.oidc.twitter.credentials.secret=SECRETSECRETSECRETSECRETSECRETSECRET
+
+# Google
+quarkus.oidc.google.provider=google
+quarkus.oidc.google.client-id=SECRET
+quarkus.oidc.google.credentials.secret=SECRET
+
+# Github
+quarkus.oidc.github.provider=github
+quarkus.oidc.github.client-id=SECRET
+quarkus.oidc.github.credentials.secret=SECRET
+
+# MS
+quarkus.oidc.microsoft.provider=microsoft
+quarkus.oidc.microsoft.client-id=SECRET
+quarkus.oidc.microsoft.credentials.secret=SECRET
+
+# Facebook
+quarkus.oidc.facebook.provider=facebook
+quarkus.oidc.facebook.client-id=SECRET
+quarkus.oidc.facebook.credentials.secret=SECRET
+
+# Apple
+quarkus.oidc.apple.provider=apple
+quarkus.oidc.apple.client-id=SECRET
+quarkus.oidc.apple.credentials.jwt.key-file=Apple-key-dev.p8
+# this actually needs to be set for Apple keys, but not the fake dev one
+#quarkus.oidc.apple.credentials.jwt.token-key-id=SECRET
+quarkus.oidc.apple.credentials.jwt.issuer=SECRET
+quarkus.oidc.apple.credentials.jwt.subject=SECRET
+
+# Manual context
+quarkus.oidc.manual.tenant-enabled=false
+
+# Default is just disabled
+quarkus.oidc.tenant-enabled=false
+
+# Get rid of keycloak
+quarkus.keycloak.devservices.enabled=false
+
+# can't seem to set it from Renarde because it's a build time config
+quarkus.http.auth.proactive=false
+
+quarkus.log.category."io.netty.handler.logging.LoggingHandler".level=DEBUG
+quarkus.log.category."io.quarkus.oidc.runtime".level=DEBUG
+
+quarkus.webauthn.login-page=/Login/login
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/imports.sql b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/imports.sql
new file mode 100644
index 000000000..e69de29bb
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Application/about.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Application/about.html
new file mode 100644
index 000000000..1628f6053
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Application/about.html
@@ -0,0 +1,24 @@
+{#include main.html }
+{#title}About Todos{/title}
+
+
+
+
Super cool
+
+ Yay!.
+
+
+
+
Super cool
+
+ Yay!.
+
+
+
+
Super cool
+
+ Yay!.
+
+
+
+{/include}
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Application/index.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Application/index.html
new file mode 100644
index 000000000..0e6f17676
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Application/index.html
@@ -0,0 +1,11 @@
+{#include main.html }
+{#title}Welcome to Todos{/title}
+
+
+
+
Welcome to Todos
+
Start adding todos today!
+
+
+
+{/include}
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Emails/confirm.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Emails/confirm.html
new file mode 100644
index 000000000..309139fb8
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Emails/confirm.html
@@ -0,0 +1,18 @@
+{#include email.html }
+
+
+ Welcome to Todos.
+
+
+
+ You received this email because someone (hopefully you) wants to register on Todos.
+
+
+
+ If you don't want to register, you can safely ignore this email.
+
+
+
+ If you want to register, complete your registration .
+
+{/include}
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Emails/confirm.txt b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Emails/confirm.txt
new file mode 100644
index 000000000..c775b32ed
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Emails/confirm.txt
@@ -0,0 +1,12 @@
+{#include email.txt}
+
+Welcome to Todos.
+
+You received this email because someone (hopefully you) wants to register on Todos.
+
+If you don't want to register, you can safely ignore this email.
+
+If you want to register, complete your registration by going to the following address:
+
+{uriabs:Login.confirm(user.confirmationCode)}
+{/include}
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/confirm.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/confirm.html
new file mode 100644
index 000000000..f26144344
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/confirm.html
@@ -0,0 +1,74 @@
+{#include main.html }
+{#title}Complete registration{/title}
+{#moreScripts}
+
+{/moreScripts}
+
+{#form uri:Login.complete(newUser.confirmationCode)}
+
+
+ Complete registration for {newUser.email}
+ {#formElement name="userName" label="User Name"}
+ {#input name="userName" value=newUser.userName id="username"/}
+ {/formElement}
+ {#if !newUser.authId}
+ {#formElement name="password" label="Password" class="input-group"}
+ {#input name="password" type="password" id="password"/}
+ WebAuthn Login
+ {/formElement}
+ {#formElement name="password2" label="Password Confirmation"}
+ {#input name="password2" type="password" id="password2"/}
+ {/formElement}
+ {/if}
+ {#formElement name="firstName" label="First Name"}
+ {#input name="firstName" value=newUser.firstName id="firstname"/}
+ {/formElement}
+ {#formElement name="lastName" label="Last Name"}
+ {#input name="lastName" value=newUser.lastName id="lastname"/}
+ {/formElement}
+
+ {#input name="webAuthnId" type="hidden" id="webAuthnId"/}
+ {#input name="webAuthnRawId" type="hidden" id="webAuthnRawId"/}
+ {#input name="webAuthnResponseClientDataJSON" type="hidden" id="webAuthnResponseClientDataJSON"/}
+ {#input name="webAuthnResponseAttestationObject" type="hidden" id="webAuthnResponseAttestationObject"/}
+ {#input name="webAuthnType" type="hidden" id="webAuthnType"/}
+
+ Complete registration
+
+
+{/form}
+
+
+
+{/include}
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/login.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/login.html
new file mode 100644
index 000000000..ae646f4b8
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/login.html
@@ -0,0 +1,90 @@
+{#include main.html }
+{#title}Login{/title}
+{#moreScripts}
+
+{/moreScripts}
+
+
+
+
+ {#form uri:Login.manualLogin() id="login"}
+
+ Login
+ {#formElement name="userName" label="User Name" class="input-group"}
+ {#input name="userName" id="username"/}
+ WebAuthn Login
+ {/formElement}
+ {#formElement name="password" label="Password" class="input-group"}
+ {#input name="password" type="password" id="password"/}
+ Password Login
+ {/formElement}
+
+ {#input name="webAuthnId" type="hidden" id="webAuthnId"/}
+ {#input name="webAuthnRawId" type="hidden" id="webAuthnRawId"/}
+ {#input name="webAuthnResponseClientDataJSON" type="hidden" id="webAuthnResponseClientDataJSON"/}
+ {#input name="webAuthnResponseAuthenticatorData" type="hidden" id="webAuthnResponseAuthenticatorData"/}
+ {#input name="webAuthnResponseSignature" type="hidden" id="webAuthnResponseSignature"/}
+ {#input name="webAuthnResponseUserHandle" type="hidden" id="webAuthnResponseUserHandle"/}
+ {#input name="webAuthnType" type="hidden" id="webAuthnType"/}
+
+ {/form}
+
+
+ {#form uri:Login.register()}
+
+ Register
+ {#formElement name="email" label="Email"}
+ {#input name="email"/}
+ {/formElement}
+ Register
+
+ {/form}
+
+
+
+
+
+{/include}
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/logoutFirst.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/logoutFirst.html
new file mode 100644
index 000000000..faeeabbd9
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/logoutFirst.html
@@ -0,0 +1,11 @@
+{#include main.html }
+{#title}Logout before completing registration{/title}
+
+
+{/include}
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/register.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/register.html
new file mode 100644
index 000000000..f6ae47c60
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/register.html
@@ -0,0 +1,14 @@
+{#include main.html }
+{#title}Check your email for confirmation{/title}
+
+
+
+
+
+ An email has been sent to {email} to verify your email address.
+ Please follow the instructions in this email in order to complete your registration.
+
+
+
+
+{/include}
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/welcome.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/welcome.html
new file mode 100644
index 000000000..14c0433ce
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Login/welcome.html
@@ -0,0 +1,16 @@
+{#include main.html }
+{#title}Home{/title}
+
+
+
+
+
+ Welcome, {inject:user.userName}
+
+
+ Your registration is complete. You've been logged in.
+
+
+
+
+{/include}
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Todos/index.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Todos/index.html
new file mode 100644
index 000000000..9fc15ff7c
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/Todos/index.html
@@ -0,0 +1,49 @@
+{#include main.html }
+{#title}Todos{/title}
+
+
+
+
+ #
+ Task
+ Action
+
+
+
+ {#for todo in todos}
+
+ {todo.id}
+
+ {#if todo.done}
+ {todo.task} (done {todo.doneDate.since})
+ {#else}
+ {todo.task}
+ {/if}
+
+
+ {#form uri:Todos.done(todo.id) klass="inline"}
+ {#if todo.done}
+ Mark Undone
+ {#else}
+ Mark Done
+ {/if}
+ {/form}
+ {#form uri:Todos.delete(todo.id) klass="inline"}
+ Delete
+ {/form}
+
+
+ {/for}
+
+ New
+
+ {#form uri:Todos.add()}
+ {#input name="task" placeholder="Type task and press ENTER"/}
+ {/form}
+
+
+
+
+
+
+{/include}
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/email.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/email.html
new file mode 100644
index 000000000..f62e3c90f
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/email.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+ {#insert /}
+
+ This is an automated email, you should not reply to it: your mail will be ignored.
+
+
+
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/email.txt b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/email.txt
new file mode 100644
index 000000000..3930319fc
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/email.txt
@@ -0,0 +1,3 @@
+{#insert /}
+
+This is an automated email, you should not reply to it: your mail will be ignored.
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/main.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/main.html
new file mode 100644
index 000000000..2534b7ee9
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/main.html
@@ -0,0 +1,64 @@
+
+
+
+ {#insert title /}
+
+
+
+
+
+ {#insert moreStyles /}
+
+
+
+ {#insert moreScripts /}
+
+
+
+
+
Todo
+ {#if inject:user}
+
+ {#if inject:user.isAdmin} {/if}
+ {#user inject:user img=true size=20/}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if flash:message}
+
+ {flash:message}
+
+ {/if}
+ {#insert /}
+
+
+
+
+ © FroMage 2013-2021
+
+
+
+
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/tags/formElement.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/tags/formElement.html
new file mode 100644
index 000000000..58ed6e8c9
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/tags/formElement.html
@@ -0,0 +1,7 @@
+{label}
+
+ {nested-content}
+{#ifError name}
+ ​{#error name/} ​
+{/ifError}
+
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/tags/input.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/tags/input.html
new file mode 100644
index 000000000..054fdf545
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/tags/input.html
@@ -0,0 +1,7 @@
+
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/tags/user.html b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/tags/user.html
new file mode 100644
index 000000000..1ed2e22f3
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/main/resources/templates/tags/user.html
@@ -0,0 +1,8 @@
+{#if it??}
+
+
+{#if img??}
+{#gravatar it.email size=size.or(20) default='mm' /}
+{/if}
+{it.userName}
+{/if}
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/test/java/fr/epardaud/NativeReactiveGreetingResourceIT.java b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/test/java/fr/epardaud/NativeReactiveGreetingResourceIT.java
new file mode 100644
index 000000000..ee73929d9
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/test/java/fr/epardaud/NativeReactiveGreetingResourceIT.java
@@ -0,0 +1,9 @@
+package fr.epardaud;
+
+import io.quarkus.test.junit.NativeImageTest;
+
+@NativeImageTest
+public class NativeReactiveGreetingResourceIT extends TodoResourceTest {
+
+ // Execute the same tests but in native mode.
+}
\ No newline at end of file
diff --git a/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/test/java/fr/epardaud/TodoResourceTest.java b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/test/java/fr/epardaud/TodoResourceTest.java
new file mode 100644
index 000000000..599eec4e2
--- /dev/null
+++ b/qute.jdt/com.redhat.qute.jdt.test/projects/maven/quarkus-renarde-todo/src/test/java/fr/epardaud/TodoResourceTest.java
@@ -0,0 +1,519 @@
+package fr.epardaud;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+import org.apache.http.client.CookieStore;
+import org.apache.http.cookie.Cookie;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.quarkiverse.renarde.oidc.test.MockAppleOidc;
+import io.quarkiverse.renarde.oidc.test.MockFacebookOidc;
+import io.quarkiverse.renarde.oidc.test.MockGithubOidc;
+import io.quarkiverse.renarde.oidc.test.MockGoogleOidc;
+import io.quarkiverse.renarde.oidc.test.MockMicrosoftOidc;
+import io.quarkiverse.renarde.oidc.test.MockTwitterOidc;
+import io.quarkiverse.renarde.oidc.test.RenardeCookieFilter;
+import io.quarkiverse.renarde.util.Flash;
+import io.quarkiverse.renarde.util.JavaExtensions;
+import io.quarkus.mailer.Mail;
+import io.quarkus.mailer.MockMailbox;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.webauthn.WebAuthnEndpointHelper;
+import io.quarkus.test.security.webauthn.WebAuthnHardware;
+import io.restassured.filter.Filter;
+import io.restassured.path.json.JsonPath;
+import io.restassured.response.ExtractableResponse;
+import io.restassured.response.Response;
+import io.restassured.response.ValidatableResponse;
+import io.restassured.specification.RequestSpecification;
+import io.smallrye.jwt.build.Jwt;
+import io.vertx.core.json.JsonObject;
+
+@MockFacebookOidc
+@MockGoogleOidc
+@MockAppleOidc
+@MockMicrosoftOidc
+@MockTwitterOidc
+@MockGithubOidc
+@QuarkusTest
+public class TodoResourceTest {
+
+ @TestHTTPResource
+ String url;
+
+ @Inject
+ MockMailbox mailbox;
+
+ @BeforeEach
+ void init() {
+ mailbox.clear();
+ }
+
+ @Test
+ public void testMainPage() {
+ given()
+ .when().get("/")
+ .then()
+ .statusCode(200)
+ .body("html.head.title", is("Welcome to Todos"));
+ }
+
+ @Test
+ public void testProtectedPage() {
+ // cannot go to Todo page
+ given()
+ .when()
+ .redirects().follow(false)
+ .get("/Todos/index")
+ .then()
+ .statusCode(302);
+ }
+
+ @Test
+ public void testProtectedPageWithInvalidJwt() throws NoSuchAlgorithmException {
+ // canary: valid
+ String token = Jwt.issuer("https://example.com/issuer")
+ .upn("fromage")
+ .issuedAt(Instant.now())
+ .expiresIn(Duration.ofDays(10))
+ .innerSign().encrypt();
+ // valid
+ given()
+ .when()
+ .cookie("QuarkusUser", token)
+ .log().ifValidationFails()
+ .redirects().follow(false)
+ .get("/")
+ .then()
+ .log().ifValidationFails()
+ .statusCode(200);
+ // expired
+ token = Jwt.issuer("https://example.com/issuer")
+ .upn("fromage")
+ .issuedAt(Instant.now().minus(20, ChronoUnit.DAYS))
+ .expiresIn(Duration.ofDays(10))
+ .innerSign().encrypt();
+ assertRedirectWithMessage(token, "Login expired, you've been logged out");
+ // invalid issuer
+ token = Jwt.issuer("https://example.com/other-issuer")
+ .upn("fromage")
+ .issuedAt(Instant.now())
+ .expiresIn(Duration.ofDays(10))
+ .innerSign().encrypt();
+ assertRedirectWithMessage(token, "Invalid session (bad JWT), you've been logged out");
+ // invalid signature
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+ kpg.initialize(2048);
+ KeyPair kp = kpg.generateKeyPair();
+ token = Jwt.issuer("https://example.com/issuer")
+ .upn("fromage")
+ .issuedAt(Instant.now())
+ .expiresIn(Duration.ofDays(10))
+ .innerSign(kp.getPrivate()).encrypt(kp.getPublic());
+ assertRedirectWithMessage(token, "Invalid session (bad signature), you've been logged out");
+ // invalid user
+ token = Jwt.issuer("https://example.com/issuer")
+ .upn("cheesy")
+ .issuedAt(Instant.now())
+ .expiresIn(Duration.ofDays(10))
+ .innerSign().encrypt();
+ assertRedirectWithMessage(token, "Invalid user: cheesy");
+ }
+
+ private void assertRedirectWithMessage(String token, String message) {
+ // redirect with message
+ String flash = given()
+ .when()
+ .cookie("QuarkusUser", token)
+ .log().ifValidationFails()
+ .redirects().follow(false)
+ .get("/")
+ .then()
+ .log().ifValidationFails()
+ .statusCode(303)
+ // logout
+ .cookie("QuarkusUser", "")
+ .extract().cookie(Flash.FLASH_COOKIE_NAME);
+ Map data = Flash.decodeCookieValue(flash);
+ Assertions.assertTrue(data.containsKey("message"));
+ Assertions.assertEquals(message, data.get("message"));
+ }
+
+ @Test
+ public void testManualRegistration() {
+ String confirmationCode = register("manual");
+
+ RenardeCookieFilter cookieFilter = new RenardeCookieFilter();
+ completeRegistration(confirmationCode, cookieFilter, "manual", "shield-lock", request -> {
+ request
+ .formParam("userName", "manual")
+ .formParam("password", "1q2w3e4r")
+ .formParam("password2", "1q2w3e4r")
+ .formParam("firstName", "Stef")
+ .formParam("lastName", "Epardaud");
+ });
+ }
+
+ @Test
+ public void testWebAuthnRegistration() {
+ String confirmationCode = register("webauthn");
+
+ RenardeCookieFilter cookieFilter = new RenardeCookieFilter();
+ WebAuthnHardware token = new WebAuthnHardware();
+ String challenge = WebAuthnEndpointHelper.invokeRegistration("webauthn", cookieFilter);
+
+ JsonObject registrationJson = token.makeRegistrationJson(challenge);
+
+ completeRegistration(confirmationCode, cookieFilter, "webauthn", "fingerprint", request -> {
+ WebAuthnEndpointHelper.addWebAuthnRegistrationFormParameters(request, registrationJson);
+ request
+ .formParam("userName", "webauthn")
+ .formParam("firstName", "Stef")
+ .formParam("lastName", "Epardaud");
+ });
+
+ // now try logging in
+ challenge = WebAuthnEndpointHelper.invokeLogin("webauthn", cookieFilter);
+
+ JsonObject loginJson = token.makeLoginJson(challenge);
+ testManualLogin(cookieFilter, "webauthn", "fingerprint", request -> {
+ WebAuthnEndpointHelper.addWebAuthnLoginFormParameters(request, loginJson);
+ request
+ .formParam("userName", "webauthn");
+ });
+ }
+
+ private void completeRegistration(String confirmationCode, Filter cookieFilter, String userName, String icon, Consumer completeCustomiser) {
+ // confirm form action
+ RequestSpecification completeRequest = given()
+ .when()
+ .queryParam("confirmationCode", confirmationCode);
+
+ completeCustomiser.accept(completeRequest);
+
+ completeRequest
+ .log().ifValidationFails()
+ .filter(cookieFilter)
+ .redirects().follow(false)
+ .post("/Login/complete")
+ .then()
+ .log().ifValidationFails()
+ .statusCode(303)
+ .cookie("QuarkusUser")
+ .header("Location", url+"Login/welcome");
+
+ testLoggedIn(userName, icon, cookieFilter);
+ }
+
+ private String register(String userName) {
+ // register email
+ given()
+ .when()
+ .formParam("email", userName+"@example.com")
+ .post("/Login/register")
+ .then()
+ .statusCode(200)
+ .body("html.head.title", is("Check your email for confirmation"))
+ .body(containsString("An email has been sent to "+userName+"@example.com "));
+
+ // get the confirmation email
+ List mails = mailbox.getMessagesSentTo(userName+"@example.com");
+ Assertions.assertEquals(1, mails.size());
+ Mail mail = mails.get(0);
+ Assertions.assertNotNull(mail.getText());
+ Assertions.assertNotNull(mail.getHtml());
+ String linkStart = "If you want to register, complete your registration by going to the following address:\n"
+ + "\n" + url;
+ int absoluteUriIndex = mail.getText().indexOf(linkStart) + linkStart.length() - 1;
+ Assertions.assertTrue(absoluteUriIndex > -1, "Failed to find confirmation URI in email: "+mail.getText());
+ String confirmationPath = mail.getText().substring(absoluteUriIndex);
+ Assertions.assertTrue(confirmationPath.startsWith("/Login/confirm?confirmationCode="), "Failed to parse confirmation path: "+confirmationPath);
+ Assertions.assertTrue(confirmationPath.indexOf('\n') > -1);
+ String confirmationCode = confirmationPath.substring("/Login/confirm?confirmationCode=".length(), confirmationPath.indexOf('\n'));
+ Assertions.assertFalse(confirmationCode.isEmpty());
+
+ // confirm page
+ given()
+ .when()
+ .queryParam("confirmationCode", confirmationCode)
+ .get("/Login/confirm")
+ .then()
+ .statusCode(200)
+ .body("html.head.title", is("Complete registration"));
+
+ return confirmationCode;
+ }
+
+ private void testLoggedIn(String userName, String icon, Filter cookieFilter) {
+ // welcome page
+ given()
+ .when()
+ .filter(cookieFilter)
+ .get("/Login/welcome")
+ .then()
+ .statusCode(200)
+ .body(containsString("Home "))
+ // alert
+ .body(containsString("Welcome, "+userName))
+ // user gravatar in menu
+ .body(containsString("\n"
+ + " \n"
+ + userName+" "))
+ // Todo link
+ .body(containsString("Todos "))
+ // Logout link
+ .body(containsString("Logout "));
+
+ // can go to Todo page
+ given()
+ .when()
+ .filter(cookieFilter)
+ .get("/Todos/index")
+ .then()
+ .statusCode(200);
+
+ // now logout
+ given()
+ .when()
+ .filter(cookieFilter)
+ .redirects().follow(false)
+ .get("/_renarde/security/logout")
+ .then()
+ .statusCode(303)
+ // go home
+ .header("Location", url)
+ // clear cookie
+ .cookie("QuarkusUser", "");
+
+ }
+
+ @Test
+ public void testManualLogin() {
+ RenardeCookieFilter cookieFilter = new RenardeCookieFilter();
+ testManualLogin(cookieFilter, "fromage", "shield-lock", request -> {
+ request
+ .formParam("userName", "fromage")
+ .formParam("password", "1q2w3e4r");
+ });
+ }
+
+ private void testManualLogin(Filter cookieFilter, String userName, String icon, Consumer requestCustomiser) {
+ // login form action
+ RequestSpecification request = given()
+ .when();
+ requestCustomiser.accept(request);
+ request
+ .filter(cookieFilter)
+ .redirects().follow(false)
+ .post("/Login/manualLogin")
+ .then()
+ .statusCode(303)
+ .cookie("QuarkusUser")
+ .header("Location", url);
+
+ testLoggedIn(userName, icon, cookieFilter);
+ }
+
+ private void oidcTest(String provider, String email, String firstName, String lastName, String userName) {
+ RenardeCookieFilter cookieFilter = new RenardeCookieFilter();
+ ValidatableResponse response = follow("/_renarde/security/login-"+provider, cookieFilter);
+ response.statusCode(200)
+ .body(containsString("Complete registration for "+email))
+ // lastname and username
+ .body(containsString("value=\""+lastName+"\"/>"))
+ // firstname
+ .body(containsString("value=\""+firstName+"\"/>"))
+ ;
+
+ Assertions.assertNotNull(findCookie(cookieFilter.getCookieStore(), "q_session_"+provider));
+
+ String body = response.extract().body().asString();
+ String clue = "