Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RS256 signature support; CORS support; i2b2 jwt support #73

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion IRCT-CL/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,21 @@
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
<version>3.4.0</version>
</dependency>

<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.3.0</version>
</dependency>

<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>

<!-- Apache Commons CSV -->
<dependency>
<groupId>org.apache.commons</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package edu.harvard.hms.dbmi.bd2k.irct.cl.filter;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;

@Provider
public class CORSFilter implements ContainerResponseFilter {

@javax.annotation.Resource(mappedName = "java:global/cors_allow_origin")
private String corsAllowOrigin;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This annotation will stop the server during start up when the name binding is missing, which means the default situation will need user to put value in this paramter, but in default, we'd like to disable Cross-Origin Resource Sharing. To implement not interrupting startup procedure, you can take ideas from this code: https://github.com/hms-dbmi/IRCT/blob/master/IRCT-API/src/main/java/edu/harvard/hms/dbmi/bd2k/irct/IRCTApplication.java#L469

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback, see the most recent commit which should address the issue.
Anything else problematic?


@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {

responseContext.getHeaders().add("Access-Control-Allow-Origin", this.corsAllowOrigin);
responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true");
responseContext.getHeaders().add("Access-Control-Allow-Headers","origin, content-type, accept, authorization");
responseContext.getHeaders().add("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, OPTIONS, HEAD");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public class SessionFilter implements Filter {
@Inject
private IRCTApplication irctApp;

@javax.annotation.Resource(mappedName = "java:global/jwks_uri")
private String jwksUri;

@javax.annotation.Resource(mappedName = "java:global/userField")
private String userField;

Expand All @@ -44,8 +47,10 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain fc) th
logger.debug("doFilter() Starting");
HttpServletRequest request = (HttpServletRequest) req;

// If processing URL /securityService/*, we are creating a session/secureSession
if (request.getRequestURI().endsWith("/securityService/startSession") || request.getRequestURI().endsWith("/securityService/createKey")) {
// If processing URL /securityService/*, we are creating a session/secureSession; ignore CORS preflight calls
if ( request.getRequestURI().endsWith("/securityService/startSession") ||
request.getRequestURI().endsWith("/securityService/createKey") ||
request.getMethod().equals("OPTIONS")) {
// Do Nothing
logger.debug("doFilter() securityService URL is NOT filtered.");
} else {
Expand Down Expand Up @@ -84,7 +89,7 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain fc) th
//Get information from token introspection endpoint in 2.0
user = sc.ensureUserExists(Utilities.extractUserFromTokenIntrospection((HttpServletRequest) req, this.userField, irctApp.getToken_introspection_url(), irctApp.getToken_introspection_token()));
} else{
user = sc.ensureUserExists(Utilities.extractEmailFromJWT((HttpServletRequest) req, irctApp.getClientSecret(), this.userField));
user = sc.ensureUserExists(Utilities.extractEmailFromJWT((HttpServletRequest) req, irctApp.getClientSecret(), this.jwksUri, this.userField));
}
}

Expand All @@ -107,7 +112,7 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain fc) th
// TODO DI-896 change. Since the user above gets created without an actual token, we need
// to re-extract the token, from the header and parse it and place it inside the user object,
// for future playtime.
if (user.getToken() == null) {
if (user.getToken() == null || !user.getToken().equals(tokenString)) {
logger.debug("doFilter() No token in user object, so let's add one.");
user.setToken(tokenString);
sc.updateUserRecord(user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public class SecurityService implements Serializable {
@javax.annotation.Resource(mappedName ="java:global/client_secret")
private String clientSecret;

@javax.annotation.Resource(mappedName ="java:global/jwks_uri")
private String jwksUri;

@javax.annotation.Resource(mappedName ="java:global/userField")
private String userField;

Expand All @@ -74,7 +77,7 @@ public Response createKey(@Context HttpServletRequest req) {

try {
User userObject = sc.ensureUserExists(Utilities
.extractEmailFromJWT(req, this.clientSecret, this.userField));
.extractEmailFromJWT(req, this.clientSecret, this.jwksUri, this.userField));
logger.debug("/createKey user exists");
userObject.setToken(Utilities.extractToken(req));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package edu.harvard.hms.dbmi.bd2k.irct.cl.util;

import com.auth0.jwk.*;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
Expand Down Expand Up @@ -33,6 +34,9 @@
import javax.ws.rs.core.MultivaluedMap;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -44,6 +48,16 @@ public class Utilities {

private static Logger logger = Logger.getLogger(Utilities.class);

private static JwkProvider jwkProvider = null;
private static String jwksUri = "";
private static JwkProvider getJwkProvider(String pJwksUri) throws MalformedURLException {
if (!jwksUri.equals(pJwksUri) || jwkProvider == null) {
jwksUri = pJwksUri;
jwkProvider = new GuavaCachedJwkProvider(new UrlJwkProvider(new URL(jwksUri)));
}
return jwkProvider;
}

/**
* Returns all the first values from a MultiValue Map
*
Expand Down Expand Up @@ -88,12 +102,13 @@ public static Map<String, String> getFirstFromMultiMap(
* extract specific user field from JWT token
* @param req
* @param clientSecret
* @param pJwksUri
* @param userField specifies which user field is going to be extracted from JWT token
* @return
* @throws NotAuthorizedException
*/
public static String extractEmailFromJWT(HttpServletRequest req, String clientSecret, String userField) {
logger.debug("extractEmailFromJWT() with secret:"+clientSecret);
public static String extractEmailFromJWT(HttpServletRequest req, String clientSecret, String pJwksUri, String userField) {
logger.debug("extractEmailFromJWT() with secret:"+clientSecret+" and jwks URI:" + pJwksUri);

//No point in doing anything if there's no userField
if (userField == null){
Expand All @@ -112,27 +127,46 @@ public static String extractEmailFromJWT(HttpServletRequest req, String clientSe
DecodedJWT jwt = null;
try {
logger.debug("validateAuthorizationHeader() validating with un-decoded secret.");
jwt = com.auth0.jwt.JWT.require(Algorithm
.HMAC256(clientSecret
.getBytes("UTF-8")))
.build()
.verify(tokenString);
} catch (UnsupportedEncodingException e){
logger.error("extractEmailFromJWT() getting bytes for initialize jwt token algorithm error: " + e.getMessage());
throw new NotAuthorizedException("Token is invalid, please request a new one");
} catch (JWTVerificationException e) {
try{
jwt = com.auth0.jwt.JWT.decode(tokenString);

if (jwt.getAlgorithm().equals("RS256")) {
Jwk jwk = getJwkProvider(pJwksUri).get(jwt.getKeyId());
RSAPublicKey signingPubKey = (RSAPublicKey) jwk.getPublicKey();

if (signingPubKey == null) {
throw new NotAuthorizedException("Problematic public key (null)");
}

jwt = com.auth0.jwt.JWT
.require(Algorithm.RSA256(signingPubKey, null))
.build()
.verify(tokenString);

} else if (jwt.getAlgorithm().equals("HS256")) {
jwt = com.auth0.jwt.JWT.require(Algorithm
.HMAC256(Base64.decodeBase64(clientSecret
.getBytes("UTF-8"))))
.HMAC256(clientSecret
.getBytes("UTF-8")))
.build()
.verify(tokenString);
} catch (UnsupportedEncodingException ex){
} else {
throw new NotAuthorizedException("Problematic signature algorithm = " + jwt.getAlgorithm());
}

} catch (UnsupportedEncodingException | MalformedURLException | JwkException e){
logger.error("extractEmailFromJWT() error decoding token: " + e.getMessage());
throw new NotAuthorizedException("Token is invalid, please request a new one");
} catch (JWTVerificationException e) {
try{
if (jwt != null && jwt.getAlgorithm().equals("HS256")) {
jwt = com.auth0.jwt.JWT.require(Algorithm
.HMAC256(Base64.decodeBase64(clientSecret
.getBytes("UTF-8"))))
.build()
.verify(tokenString);
}
} catch (UnsupportedEncodingException | JWTVerificationException ex){
logger.error("extractEmailFromJWT() getting bytes for initialize jwt token algorithm error: " + e.getMessage());
throw new NotAuthorizedException("Token is invalid, please request a new one");
} catch (JWTVerificationException ex) {
logger.error("extractEmailFromJWT() token is invalid after tried with another algorithm: " + e.getMessage());
throw new NotAuthorizedException("Token is invalid, please request a new one");
}
}

Expand Down
Loading