diff --git a/pom.xml b/pom.xml
index 721c0ef2..36d712d2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -180,6 +180,13 @@
provided
+
+ javax.json
+ javax.json-api
+ 1.1
+ provided
+
+
javax.annotation
javax.annotation-api
diff --git a/security/jwt/README.md b/security/jwt/README.md
new file mode 100644
index 00000000..153b2220
--- /dev/null
+++ b/security/jwt/README.md
@@ -0,0 +1,33 @@
+# Java EE Security 1.0 JWT Example
+Sample to demonstrate JWT integration with Java EE Security 1.0 (Soteria RI)
+
+### Sample Users
+Username | Password | Roles
+--- | --- | ---
+payara | fish | ADMIN_ROLE, USER_ROLE
+duke | secret | USER_ROLE
+
+### Login EndPoint
+http://localhost:8080/security-jwt-example/api/auth/login?name=duke&password=secret&rememberme=false
+
+### Protected REST EndPoint
+
+EndPoint | HTTP Method | Roles Allowed
+--- | --- | ---
+http://localhost:8080/security-jwt-example/api/sample/read | GET | ANONYMOUS, USER_ROLE, ADMIN_ROLE
+http://localhost:8080/security-jwt-example/api/sample/write | POST | USER_ROLE, ADMIN_ROLE
+http://localhost:8080/security-jwt-example/api/sample/delete | DELETE | ADMIN_ROLE
+
+
+#### rememberme=false
+
+Whenever the user wants to access a protected resource, the user agent send the JWT in the Authorization header using the Bearer schema. The content of the header should look like the following:
+
+`Authorization: Bearer `
+
+This is a stateless authentication mechanism as the user state is never saved in server memory.
+The server's protected routes will check for a valid JWT in the Authorization header, and if it's present, the user will be allowed to access protected resources.
+
+#### rememberme=true
+Whenever the user wants to access a protected resource, the user agent would automatically include the JWT in the cookie with `JREMEMBERMEID` key.
+It does not require state to be stored on the server because the JWT encapsulates everything the server needs to serve the request.
\ No newline at end of file
diff --git a/security/jwt/pom.xml b/security/jwt/pom.xml
new file mode 100644
index 00000000..f45387f1
--- /dev/null
+++ b/security/jwt/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+
+
+ org.javaee8
+ security
+ 1.0-SNAPSHOT
+
+
+ jwt
+ war
+ Java EE 8 Samples: Security - JWT
+
+
+ 0.6.0
+
+
+
+
+ io.jsonwebtoken
+ jjwt
+ ${version.jsonwebtoken}
+
+
+
+
diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/AuthenticationIdentityStore.java b/security/jwt/src/main/java/org/javaee8/security/jwt/AuthenticationIdentityStore.java
new file mode 100644
index 00000000..a5bc602d
--- /dev/null
+++ b/security/jwt/src/main/java/org/javaee8/security/jwt/AuthenticationIdentityStore.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.javaee8.security.jwt;
+
+import static java.util.Collections.singleton;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.RequestScoped;
+import javax.security.enterprise.credential.Credential;
+import javax.security.enterprise.credential.UsernamePasswordCredential;
+import javax.security.enterprise.identitystore.CredentialValidationResult;
+import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;
+import static javax.security.enterprise.identitystore.CredentialValidationResult.NOT_VALIDATED_RESULT;
+import javax.security.enterprise.identitystore.IdentityStore;
+import javax.security.enterprise.identitystore.IdentityStore.ValidationType;
+import static javax.security.enterprise.identitystore.IdentityStore.ValidationType.VALIDATE;
+
+@RequestScoped
+public class AuthenticationIdentityStore implements IdentityStore {
+
+ private Map callerToPassword;
+
+ @PostConstruct
+ public void init() {
+ callerToPassword = new HashMap<>();
+ callerToPassword.put("payara", "fish");
+ callerToPassword.put("duke", "secret");
+ }
+
+ @Override
+ public CredentialValidationResult validate(Credential credential) {
+ CredentialValidationResult result;
+
+ if (credential instanceof UsernamePasswordCredential) {
+ UsernamePasswordCredential usernamePassword = (UsernamePasswordCredential) credential;
+ String expectedPW = callerToPassword.get(usernamePassword.getCaller());
+ if (expectedPW != null && expectedPW.equals(usernamePassword.getPasswordAsString())) {
+ result = new CredentialValidationResult(usernamePassword.getCaller());
+ } else {
+ result = INVALID_RESULT;
+ }
+ } else {
+ result = NOT_VALIDATED_RESULT;
+ }
+ return result;
+ }
+
+ @Override
+ public Set validationTypes() {
+ return singleton(VALIDATE);
+ }
+}
diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/AuthorizationIdentityStore.java b/security/jwt/src/main/java/org/javaee8/security/jwt/AuthorizationIdentityStore.java
new file mode 100644
index 00000000..8fecbeb0
--- /dev/null
+++ b/security/jwt/src/main/java/org/javaee8/security/jwt/AuthorizationIdentityStore.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.javaee8.security.jwt;
+
+import static org.javaee8.security.jwt.Constants.ADMIN;
+import static org.javaee8.security.jwt.Constants.USER;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singleton;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.RequestScoped;
+import javax.security.enterprise.identitystore.CredentialValidationResult;
+import javax.security.enterprise.identitystore.IdentityStore;
+import javax.security.enterprise.identitystore.IdentityStore.ValidationType;
+import static javax.security.enterprise.identitystore.IdentityStore.ValidationType.PROVIDE_GROUPS;
+
+@RequestScoped
+public class AuthorizationIdentityStore implements IdentityStore {
+
+ private Map> groupsPerCaller;
+
+ @PostConstruct
+ public void init() {
+ groupsPerCaller = new HashMap<>();
+ groupsPerCaller.put("payara", new HashSet<>(asList(USER, ADMIN)));
+ groupsPerCaller.put("duke", singleton(USER));
+ }
+
+ @Override
+ public Set getCallerGroups(CredentialValidationResult validationResult) {
+ Set result = groupsPerCaller.get(validationResult.getCallerPrincipal().getName());
+ if (result == null) {
+ result = emptySet();
+ }
+ return result;
+ }
+
+ @Override
+ public Set validationTypes() {
+ return singleton(PROVIDE_GROUPS);
+ }
+
+}
diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/Constants.java b/security/jwt/src/main/java/org/javaee8/security/jwt/Constants.java
new file mode 100644
index 00000000..d7b1947f
--- /dev/null
+++ b/security/jwt/src/main/java/org/javaee8/security/jwt/Constants.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.javaee8.security.jwt;
+
+public final class Constants {
+
+ public static final String AUTHORIZATION_HEADER = "Authorization";
+
+ public static final String BEARER = "Bearer ";
+
+ public static final String ADMIN = "ROLE_ADMIN";
+
+ public static final String USER = "ROLE_USER";
+
+ public static final int REMEMBERME_VALIDITY_SECONDS = 24 * 60 * 60; //24 hours
+
+ // Load the TokenProvider configuration[secretKey,tokenValidity] and REMEMBERME_VALIDITY_SECONDS from config
+}
diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/JWTAuthenticationMechanism.java b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTAuthenticationMechanism.java
new file mode 100644
index 00000000..fbd7bbdc
--- /dev/null
+++ b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTAuthenticationMechanism.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.javaee8.security.jwt;
+
+import static org.javaee8.security.jwt.Constants.AUTHORIZATION_HEADER;
+import static org.javaee8.security.jwt.Constants.BEARER;
+import static org.javaee8.security.jwt.Constants.REMEMBERME_VALIDITY_SECONDS;
+import io.jsonwebtoken.ExpiredJwtException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.security.enterprise.AuthenticationStatus;
+import javax.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism;
+import javax.security.enterprise.authentication.mechanism.http.HttpMessageContext;
+import javax.security.enterprise.authentication.mechanism.http.RememberMe;
+import javax.security.enterprise.credential.UsernamePasswordCredential;
+import javax.security.enterprise.identitystore.CredentialValidationResult;
+import javax.security.enterprise.identitystore.IdentityStoreHandler;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@RememberMe(
+ cookieMaxAgeSeconds = REMEMBERME_VALIDITY_SECONDS,
+ isRememberMeExpression = "self.isRememberMe(httpMessageContext)"
+)
+@RequestScoped
+public class JWTAuthenticationMechanism implements HttpAuthenticationMechanism {
+
+ private static final Logger LOGGER = Logger.getLogger(JWTAuthenticationMechanism.class.getName());
+
+ /**
+ * Access to the
+ * IdentityStore(AuthenticationIdentityStore,AuthorizationIdentityStore) is
+ * abstracted by the IdentityStoreHandler to allow for multiple identity
+ * stores to logically act as a single IdentityStore
+ */
+ @Inject
+ private IdentityStoreHandler identityStoreHandler;
+
+ @Inject
+ private TokenProvider tokenProvider;
+
+ @Override
+ public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response, HttpMessageContext context) {
+
+ LOGGER.log(Level.INFO, "validateRequest: {0}", request.getRequestURI());
+ // Get the (caller) name and password from the request
+ // NOTE: This is for the smallest possible example only. In practice
+ // putting the password in a request query parameter is highly insecure
+ String name = request.getParameter("name");
+ String password = request.getParameter("password");
+ String token = extractToken(context);
+
+ if (name != null && password != null) {
+ LOGGER.log(Level.INFO, "credentials : {0}, {1}", new String[]{name, password});
+ // validation of the credential using the identity store
+ CredentialValidationResult result = identityStoreHandler.validate(new UsernamePasswordCredential(name, password));
+ if (result.getStatus() == CredentialValidationResult.Status.VALID) {
+ // Communicate the details of the authenticated user to the container and return SUCCESS.
+ return createToken(result, context);
+ }
+ // if the authentication failed, we return the unauthorized status in the http response
+ return context.responseUnauthorized();
+ } else if (token != null) {
+ // validation of the jwt credential
+ return validateToken(token, context);
+ } else if (context.isProtected()) {
+ // A protected resource is a resource for which a constraint has been defined.
+ // if there are no credentials and the resource is protected, we response with unauthorized status
+ return context.responseUnauthorized();
+ }
+ // there are no credentials AND the resource is not protected,
+ // SO Instructs the container to "do nothing"
+ return context.doNothing();
+ }
+
+ /**
+ * To validate the JWT token e.g Signature check, JWT claims
+ * check(expiration) etc
+ *
+ * @param token The JWT access tokens
+ * @param context
+ * @return the AuthenticationStatus to notify the container
+ */
+ private AuthenticationStatus validateToken(String token, HttpMessageContext context) {
+ try {
+ if (tokenProvider.validateToken(token)) {
+ JWTCredential credential = tokenProvider.getCredential(token);
+ return context.notifyContainerAboutLogin(credential.getPrincipal(), credential.getAuthorities());
+ }
+ // if token invalid, response with unauthorized status
+ return context.responseUnauthorized();
+ } catch (ExpiredJwtException eje) {
+ LOGGER.log(Level.INFO, "Security exception for user {0} - {1}", new String[]{eje.getClaims().getSubject(), eje.getMessage()});
+ return context.responseUnauthorized();
+ }
+ }
+
+ /**
+ * Create the JWT using CredentialValidationResult received from
+ * IdentityStoreHandler
+ *
+ * @param result the result from validation of UsernamePasswordCredential
+ * @param context
+ * @return the AuthenticationStatus to notify the container
+ */
+ private AuthenticationStatus createToken(CredentialValidationResult result, HttpMessageContext context) {
+ if (!isRememberMe(context)) {
+ String jwt = tokenProvider.createToken(result.getCallerPrincipal().getName(), result.getCallerGroups(), false);
+ context.getResponse().setHeader(AUTHORIZATION_HEADER, BEARER + jwt);
+ }
+ return context.notifyContainerAboutLogin(result.getCallerPrincipal(), result.getCallerGroups());
+ }
+
+ /**
+ * To extract the JWT from Authorization HTTP header
+ *
+ * @param context
+ * @return The JWT access tokens
+ */
+ private String extractToken(HttpMessageContext context) {
+ String authorizationHeader = context.getRequest().getHeader(AUTHORIZATION_HEADER);
+ if (authorizationHeader != null && authorizationHeader.startsWith(BEARER)) {
+ String token = authorizationHeader.substring(BEARER.length(), authorizationHeader.length());
+ return token;
+ }
+ return null;
+ }
+
+ /**
+ * this function invoked using RememberMe.isRememberMeExpression EL
+ * expression
+ *
+ * @param context
+ * @return The remember me flag
+ */
+ public Boolean isRememberMe(HttpMessageContext context) {
+ return Boolean.valueOf(context.getRequest().getParameter("rememberme"));
+ }
+
+}
diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/JWTCredential.java b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTCredential.java
new file mode 100644
index 00000000..260b0517
--- /dev/null
+++ b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTCredential.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.javaee8.security.jwt;
+
+import java.util.Set;
+import javax.security.enterprise.credential.Credential;
+
+public class JWTCredential implements Credential {
+
+ private final String principal;
+ private final Set authorities;
+
+ public JWTCredential(String principal, Set authorities) {
+ this.principal = principal;
+ this.authorities = authorities;
+ }
+
+ public String getPrincipal() {
+ return principal;
+ }
+
+ public Set getAuthorities() {
+ return authorities;
+ }
+
+}
diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/JWTRememberMeIdentityStore.java b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTRememberMeIdentityStore.java
new file mode 100644
index 00000000..fc156c55
--- /dev/null
+++ b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTRememberMeIdentityStore.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.javaee8.security.jwt;
+
+import io.jsonwebtoken.ExpiredJwtException;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.security.enterprise.CallerPrincipal;
+import javax.security.enterprise.credential.RememberMeCredential;
+import javax.security.enterprise.identitystore.CredentialValidationResult;
+import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;
+import javax.security.enterprise.identitystore.RememberMeIdentityStore;
+
+@ApplicationScoped
+public class JWTRememberMeIdentityStore implements RememberMeIdentityStore {
+
+ private static final Logger LOGGER = Logger.getLogger(JWTRememberMeIdentityStore.class.getName());
+
+ @Inject
+ private TokenProvider tokenProvider;
+
+ @Override
+ public CredentialValidationResult validate(RememberMeCredential rememberMeCredential) {
+ try {
+ if (tokenProvider.validateToken(rememberMeCredential.getToken())) {
+ JWTCredential credential = tokenProvider.getCredential(rememberMeCredential.getToken());
+ return new CredentialValidationResult(credential.getPrincipal(), credential.getAuthorities());
+ }
+ // if token invalid, response with invalid result status
+ return INVALID_RESULT;
+ } catch (ExpiredJwtException eje) {
+ LOGGER.log(Level.INFO, "Security exception for user {0} - {1}", new Object[]{eje.getClaims().getSubject(), eje.getMessage()});
+ return INVALID_RESULT;
+ }
+ }
+
+ @Override
+ public String generateLoginToken(CallerPrincipal callerPrincipal, Set groups) {
+ return tokenProvider.createToken(callerPrincipal.getName(), groups, true);
+ }
+
+ @Override
+ public void removeLoginToken(String token) {
+ // Stateless authentication means at server side we don't maintain the state
+ }
+
+}
diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/TokenProvider.java b/security/jwt/src/main/java/org/javaee8/security/jwt/TokenProvider.java
new file mode 100644
index 00000000..216ee8c0
--- /dev/null
+++ b/security/jwt/src/main/java/org/javaee8/security/jwt/TokenProvider.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.javaee8.security.jwt;
+
+import static org.javaee8.security.jwt.Constants.REMEMBERME_VALIDITY_SECONDS;
+import io.jsonwebtoken.*;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import static java.util.stream.Collectors.joining;
+import javax.annotation.PostConstruct;
+
+public class TokenProvider {
+
+ private static final Logger LOGGER = Logger.getLogger(TokenProvider.class.getName());
+
+ private static final String AUTHORITIES_KEY = "auth";
+
+ private String secretKey;
+
+ private long tokenValidity;
+
+ private long tokenValidityForRememberMe;
+
+ @PostConstruct
+ public void init() {
+ // load from config
+ this.secretKey = "my-secret-jwt-key";
+ this.tokenValidity = TimeUnit.HOURS.toMillis(10); //10 hours
+ this.tokenValidityForRememberMe = TimeUnit.SECONDS.toMillis(REMEMBERME_VALIDITY_SECONDS); //24 hours
+ }
+
+ public String createToken(String username, Set authorities, Boolean rememberMe) {
+ long now = (new Date()).getTime();
+ long validity = rememberMe ? tokenValidityForRememberMe : tokenValidity;
+
+ return Jwts.builder()
+ .setSubject(username)
+ .claim(AUTHORITIES_KEY, authorities.stream().collect(joining(",")))
+ .signWith(SignatureAlgorithm.HS512, secretKey)
+ .setExpiration(new Date(now + validity))
+ .compact();
+ }
+
+ public JWTCredential getCredential(String token) {
+ Claims claims = Jwts.parser()
+ .setSigningKey(secretKey)
+ .parseClaimsJws(token)
+ .getBody();
+
+ Set authorities
+ = Arrays.asList(claims.get(AUTHORITIES_KEY).toString().split(","))
+ .stream()
+ .collect(Collectors.toSet());
+
+ return new JWTCredential(claims.getSubject(), authorities);
+ }
+
+ public boolean validateToken(String authToken) {
+ try {
+ Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken);
+ return true;
+ } catch (SignatureException e) {
+ LOGGER.log(Level.INFO, "Invalid JWT signature: {0}", e.getMessage());
+ return false;
+ }
+ }
+}
diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/rest/ApplicationConfig.java b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/ApplicationConfig.java
new file mode 100644
index 00000000..bb39382a
--- /dev/null
+++ b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/ApplicationConfig.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.javaee8.security.jwt.rest;
+
+import static org.javaee8.security.jwt.Constants.ADMIN;
+import static org.javaee8.security.jwt.Constants.USER;
+import javax.annotation.security.DeclareRoles;
+import javax.ws.rs.ApplicationPath;
+import javax.ws.rs.core.Application;
+
+@DeclareRoles({ADMIN, USER})
+@ApplicationPath(value = "api")
+public class ApplicationConfig extends Application {
+}
diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/rest/AuthController.java b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/AuthController.java
new file mode 100644
index 00000000..6f1f2730
--- /dev/null
+++ b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/AuthController.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.javaee8.security.jwt.rest;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.inject.Inject;
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.security.enterprise.SecurityContext;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Response;
+import static javax.ws.rs.core.Response.Status.UNAUTHORIZED;
+
+@Path("auth")
+public class AuthController {
+
+ private static final Logger LOGGER = Logger.getLogger(AuthController.class.getName());
+
+ @Inject
+ private SecurityContext securityContext;
+
+ @GET
+ @Path("login")
+ public Response login() {
+ LOGGER.log(Level.INFO, "login");
+ if (securityContext.getCallerPrincipal() != null) {
+ JsonObject result = Json.createObjectBuilder()
+ .add("user", securityContext.getCallerPrincipal().getName())
+ .build();
+ return Response.ok(result).build();
+ }
+ return Response.status(UNAUTHORIZED).build();
+ }
+
+}
diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/rest/SampleController.java b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/SampleController.java
new file mode 100644
index 00000000..7be05c97
--- /dev/null
+++ b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/SampleController.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.javaee8.security.jwt.rest;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.security.PermitAll;
+import javax.inject.Inject;
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.security.enterprise.SecurityContext;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Response;
+
+@Path("sample")
+public class SampleController {
+
+ private static final Logger LOGGER = Logger.getLogger(SampleController.class.getName());
+
+ @Inject
+ private SecurityContext securityContext;
+
+ @GET
+ @Path("read")
+ @PermitAll
+ public Response read() {
+ LOGGER.log(Level.INFO, "read");
+ JsonObject result = Json.createObjectBuilder()
+ .add("user", securityContext.getCallerPrincipal() != null
+ ? securityContext.getCallerPrincipal().getName() : "Anonymous")
+ .add("message", "Read resource")
+ .build();
+ return Response.ok(result).build();
+ }
+
+ @POST
+ @Path("write")
+// @RolesAllowed({USER, ADMIN})
+ public Response write() {
+ LOGGER.log(Level.INFO, "write");
+ JsonObject result = Json.createObjectBuilder()
+ .add("user", securityContext.getCallerPrincipal().getName())
+ .add("message", "Write resource")
+ .build();
+ return Response.ok(result).build();
+ }
+
+ @DELETE
+ @Path("delete")
+// @RolesAllowed({ADMIN})
+ public Response delete() {
+ LOGGER.log(Level.INFO, "delete");
+ JsonObject result = Json.createObjectBuilder()
+ .add("user", securityContext.getCallerPrincipal().getName())
+ .add("message", "Delete resource")
+ .build();
+ return Response.ok(result).build();
+ }
+}
diff --git a/security/jwt/src/test/java/org/javaee8/security/jwt/JwtTest.java b/security/jwt/src/test/java/org/javaee8/security/jwt/JwtTest.java
new file mode 100644
index 00000000..4d96ec11
--- /dev/null
+++ b/security/jwt/src/test/java/org/javaee8/security/jwt/JwtTest.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.javaee8.security.jwt;
+
+import java.io.File;
+import static org.jboss.shrinkwrap.api.ShrinkWrap.create;
+import java.io.IOException;
+import java.net.URL;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.RunAsClient;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import java.net.URISyntaxException;
+import javax.ws.rs.client.ClientBuilder;
+import static javax.ws.rs.client.Entity.json;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+import static org.javaee8.security.jwt.Constants.AUTHORIZATION_HEADER;
+import org.javaee8.security.jwt.rest.ApplicationConfig;
+import org.jboss.shrinkwrap.resolver.api.maven.Maven;
+import org.jboss.shrinkwrap.resolver.api.maven.MavenResolverSystem;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @author Gaurav Gupta
+ */
+@RunWith(Arquillian.class)
+public class JwtTest {
+
+ @ArquillianResource
+ private URL base;
+
+ private WebTarget webTarget;
+
+ @Deployment
+ public static WebArchive createDeployment() {
+ MavenResolverSystem RESOLVER = Maven.resolver();
+ File[] jjwtFiles = RESOLVER.resolve("io.jsonwebtoken:jjwt:0.6.0").withTransitivity().asFile();
+
+ return create(WebArchive.class)
+ .addPackage(ApplicationConfig.class.getPackage())
+ .addPackage(JWTCredential.class.getPackage())
+ .addAsLibraries(jjwtFiles)
+ .setWebXML("web.xml")
+ .addAsWebInfResource("beans.xml");
+ }
+
+ @Before
+ public void setup() throws URISyntaxException {
+ webTarget = ClientBuilder.newClient().target(base.toURI().toString() + "api/");
+ }
+
+ @Test
+ @RunAsClient
+ public void testNotAuthenticated() throws IOException {
+ Response response = webTarget
+ .path("auth/login")
+ .queryParam("name", "duke")
+ .queryParam("password", "invalid")
+ .queryParam("rememberme", "false")
+ .request()
+ .get();
+ String authorizationHeader = response.getHeaderString(AUTHORIZATION_HEADER);
+ assertNull(authorizationHeader);
+ assertEquals(
+ 401,
+ response.getStatus());
+ }
+
+ @Test
+ @RunAsClient
+ public void testAuthenticatedWithoutRememberme() throws IOException {
+ Response response = webTarget
+ .path("auth/login")
+ .queryParam("name", "duke")
+ .queryParam("password", "secret")
+ .queryParam("rememberme", "false")
+ .request()
+ .get();
+ String authorizationHeader = response.getHeaderString(AUTHORIZATION_HEADER);
+ assertNotNull(authorizationHeader);
+ assertEquals(
+ 200,
+ response.getStatus());
+
+ Response protectedResponse = webTarget
+ .path("sample/write")
+ .request()
+ .header(AUTHORIZATION_HEADER, authorizationHeader)
+ .post(json(null));
+ assertEquals(
+ 200,
+ protectedResponse.getStatus());
+
+ Response adminResponse = webTarget
+ .path("sample/delete")
+ .request()
+ .header(AUTHORIZATION_HEADER, authorizationHeader)
+ .delete();
+ //Only ROLE_ADMIN user can access
+ assertEquals(
+ 403,
+ adminResponse.getStatus());
+
+ }
+
+ @Test
+ @RunAsClient
+ public void testAuthenticatedWithRememberme() throws IOException {
+ Response response = webTarget
+ .path("auth/login")
+ .queryParam("name", "payara")
+ .queryParam("password", "fish")
+ .queryParam("rememberme", "true")
+ .request()
+ .get();
+ String token = response.getCookies().get("JREMEMBERMEID").getValue();
+ assertNotNull(token);
+ assertEquals(
+ 200,
+ response.getStatus());
+
+ Response protectedResponse = webTarget
+ .path("sample/write")
+ .request()
+ .cookie("JREMEMBERMEID", token)
+ .post(json(null));
+ assertEquals(
+ 200,
+ protectedResponse.getStatus());
+
+ Response adminResponse = webTarget
+ .path("sample/delete")
+ .request()
+ .cookie("JREMEMBERMEID", token)
+ .delete();
+ assertEquals(
+ 200,
+ adminResponse.getStatus());
+
+ }
+}
diff --git a/security/jwt/src/test/resources/beans.xml b/security/jwt/src/test/resources/beans.xml
new file mode 100644
index 00000000..b09e5921
--- /dev/null
+++ b/security/jwt/src/test/resources/beans.xml
@@ -0,0 +1,38 @@
+
+
+
+
diff --git a/security/jwt/src/test/resources/web.xml b/security/jwt/src/test/resources/web.xml
new file mode 100644
index 00000000..dbfe954e
--- /dev/null
+++ b/security/jwt/src/test/resources/web.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+ Protected resources
+ /api/sample/write
+
+
+ ROLE_USER
+ ROLE_ADMIN
+
+
+
+
+
+ Admin resources
+ /api/sample/delete
+
+
+ ROLE_ADMIN
+
+
+
diff --git a/security/pom.xml b/security/pom.xml
index 3979a20f..37c82f50 100644
--- a/security/pom.xml
+++ b/security/pom.xml
@@ -14,6 +14,7 @@
dynamic-rememberme
+ jwt