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