diff --git a/build-dependencies.xml b/build-dependencies.xml index b4293959f..de9b3efd6 100644 --- a/build-dependencies.xml +++ b/build-dependencies.xml @@ -353,11 +353,11 @@ - - - - - + + + + + @@ -442,7 +442,10 @@ - + + + + @@ -641,16 +644,18 @@ - - - + + + - + + + diff --git a/build.xml b/build.xml index e1a28ad97..17fc030a7 100644 --- a/build.xml +++ b/build.xml @@ -309,13 +309,16 @@ - - - + + + - + + + + diff --git a/components/topcoder_cockpit_asset_services/build-dependencies.xml b/components/topcoder_cockpit_asset_services/build-dependencies.xml index fece17c25..c80f59ada 100644 --- a/components/topcoder_cockpit_asset_services/build-dependencies.xml +++ b/components/topcoder_cockpit_asset_services/build-dependencies.xml @@ -23,7 +23,7 @@ - + @@ -58,4 +58,4 @@ - \ No newline at end of file + diff --git a/conf/Direct.properties b/conf/Direct.properties index db87e29da..f68d84e7f 100644 --- a/conf/Direct.properties +++ b/conf/Direct.properties @@ -17,6 +17,7 @@ JWT_EXPIRATION_SECONDS = @JWT_EXPIRATION_SECONDS@ LDAP_AUTH0_CONNECTION_NAME = @LDAP_AUTH0_CONNECTION_NAME@ REDIRECT_URL_AUTH0 = /reg2/callback.action REG_SERVER_NAME= @REG_SERVER_NAME@ +JWT_VALID_ISSUERS=@JWT_VALID_ISSUERS@ #Parameter whether we use login processor or not USE_LOGIN_PROCESSOR = @useLoginProcessor@ diff --git a/lib/third_party/jackson/1.9.7/jackson-annotations-2.3.0.jar b/lib/third_party/jackson/1.9.7/jackson-annotations-2.3.0.jar deleted file mode 100644 index 3901f3281..000000000 Binary files a/lib/third_party/jackson/1.9.7/jackson-annotations-2.3.0.jar and /dev/null differ diff --git a/lib/third_party/jackson/1.9.7/jackson-core-2.3.2.jar b/lib/third_party/jackson/1.9.7/jackson-core-2.3.2.jar deleted file mode 100644 index 0b000af5f..000000000 Binary files a/lib/third_party/jackson/1.9.7/jackson-core-2.3.2.jar and /dev/null differ diff --git a/lib/third_party/jackson/1.9.7/jackson-databind-2.3.2.jar b/lib/third_party/jackson/1.9.7/jackson-databind-2.3.2.jar deleted file mode 100644 index 0449f39c0..000000000 Binary files a/lib/third_party/jackson/1.9.7/jackson-databind-2.3.2.jar and /dev/null differ diff --git a/lib/third_party/jackson/2.8.1/jackson-annotations-2.8.1.jar b/lib/third_party/jackson/2.8.1/jackson-annotations-2.8.1.jar new file mode 100644 index 000000000..d19b67b0f Binary files /dev/null and b/lib/third_party/jackson/2.8.1/jackson-annotations-2.8.1.jar differ diff --git a/lib/third_party/jackson/2.8.1/jackson-core-2.8.1.jar b/lib/third_party/jackson/2.8.1/jackson-core-2.8.1.jar new file mode 100644 index 000000000..29230d46b Binary files /dev/null and b/lib/third_party/jackson/2.8.1/jackson-core-2.8.1.jar differ diff --git a/lib/third_party/jackson/1.9.7/jackson-core-asl.jar b/lib/third_party/jackson/2.8.1/jackson-core-asl.jar similarity index 100% rename from lib/third_party/jackson/1.9.7/jackson-core-asl.jar rename to lib/third_party/jackson/2.8.1/jackson-core-asl.jar diff --git a/lib/third_party/jackson/2.8.1/jackson-databind-2.8.1.jar b/lib/third_party/jackson/2.8.1/jackson-databind-2.8.1.jar new file mode 100644 index 000000000..c4c48016f Binary files /dev/null and b/lib/third_party/jackson/2.8.1/jackson-databind-2.8.1.jar differ diff --git a/lib/third_party/jackson/1.9.7/jackson-mapper-asl.jar b/lib/third_party/jackson/2.8.1/jackson-mapper-asl.jar similarity index 100% rename from lib/third_party/jackson/1.9.7/jackson-mapper-asl.jar rename to lib/third_party/jackson/2.8.1/jackson-mapper-asl.jar diff --git a/lib/third_party/jwt/commons-codec-1.9.jar b/lib/third_party/jwt/commons-codec-1.9.jar new file mode 100644 index 000000000..ef35f1c50 Binary files /dev/null and b/lib/third_party/jwt/commons-codec-1.9.jar differ diff --git a/lib/third_party/jwt/guava-19.0.jar b/lib/third_party/jwt/guava-19.0.jar new file mode 100644 index 000000000..b175ca867 Binary files /dev/null and b/lib/third_party/jwt/guava-19.0.jar differ diff --git a/lib/third_party/jwt/java-jwt-0.2.jar b/lib/third_party/jwt/java-jwt-0.2.jar deleted file mode 100644 index 6b98c9b04..000000000 Binary files a/lib/third_party/jwt/java-jwt-0.2.jar and /dev/null differ diff --git a/lib/third_party/jwt/java-jwt-1.0.0.jar b/lib/third_party/jwt/java-jwt-1.0.0.jar deleted file mode 100644 index 59a79ef0f..000000000 Binary files a/lib/third_party/jwt/java-jwt-1.0.0.jar and /dev/null differ diff --git a/lib/third_party/jwt/java-jwt-3.3.0.jar b/lib/third_party/jwt/java-jwt-3.3.0.jar new file mode 100644 index 000000000..b7e6b9b7c Binary files /dev/null and b/lib/third_party/jwt/java-jwt-3.3.0.jar differ diff --git a/lib/third_party/jwt/jwks-rsa-0.3.0.jar b/lib/third_party/jwt/jwks-rsa-0.3.0.jar new file mode 100644 index 000000000..d118fed3c Binary files /dev/null and b/lib/third_party/jwt/jwks-rsa-0.3.0.jar differ diff --git a/services/cloud_vm_service/build-dependencies.xml b/services/cloud_vm_service/build-dependencies.xml index 89e6c29a0..afc6ed3c9 100644 --- a/services/cloud_vm_service/build-dependencies.xml +++ b/services/cloud_vm_service/build-dependencies.xml @@ -167,8 +167,8 @@ - - + + diff --git a/src/java/main/com/topcoder/direct/services/view/action/ServiceBackendDataTablesAction.java b/src/java/main/com/topcoder/direct/services/view/action/ServiceBackendDataTablesAction.java index 0bdc03655..4806d94c5 100644 --- a/src/java/main/com/topcoder/direct/services/view/action/ServiceBackendDataTablesAction.java +++ b/src/java/main/com/topcoder/direct/services/view/action/ServiceBackendDataTablesAction.java @@ -323,7 +323,7 @@ protected JsonNode getJsonResultFromAPI(URI apiEndPoint) throws Exception { // specify the get request HttpGet getRequest = new HttpGet(apiEndPoint); - String token = jwtTokenUpdater.check().getToken(); + String token = jwtTokenUpdater.getV3Token(); getRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token); diff --git a/src/java/main/com/topcoder/direct/services/view/action/contest/launch/GetGroupMemberAction.java b/src/java/main/com/topcoder/direct/services/view/action/contest/launch/GetGroupMemberAction.java index 3b26c9f5f..a62b0470c 100644 --- a/src/java/main/com/topcoder/direct/services/view/action/contest/launch/GetGroupMemberAction.java +++ b/src/java/main/com/topcoder/direct/services/view/action/contest/launch/GetGroupMemberAction.java @@ -190,7 +190,7 @@ private RestResult getGroupMemberByGid(Long gid) throws Exception { HttpGet request = new HttpGet(groupApiEndpointUri); String jwtToken; try{ - jwtToken = jwtTokenUpdater.check().getToken(); + jwtToken = jwtTokenUpdater.getV3Token(); } catch (Exception e) { logger.error("Can't get jwt token"); throw e; diff --git a/src/java/main/com/topcoder/direct/services/view/action/my/MyChallengesAction.java b/src/java/main/com/topcoder/direct/services/view/action/my/MyChallengesAction.java index acf13817f..a8f35544d 100644 --- a/src/java/main/com/topcoder/direct/services/view/action/my/MyChallengesAction.java +++ b/src/java/main/com/topcoder/direct/services/view/action/my/MyChallengesAction.java @@ -3,12 +3,15 @@ */ package com.topcoder.direct.services.view.action.my; +import com.topcoder.direct.services.configs.ServerConfiguration; import com.topcoder.direct.services.view.action.ServiceBackendDataTablesAction; import com.topcoder.direct.services.view.dto.my.Challenge; import com.topcoder.direct.services.view.dto.my.RestResult; -import com.topcoder.direct.services.view.exception.JwtAuthenticationException; +import com.topcoder.direct.services.view.util.DirectUtils; import org.codehaus.jackson.JsonNode; +import org.apache.struts2.ServletActionContext; + import java.text.DateFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; @@ -46,11 +49,9 @@ public class MyChallengesAction extends ServiceBackendDataTablesAction { */ @Override public String execute() throws Exception { - try { - getJwtTokenUpdater().check(); - } catch (JwtAuthenticationException e) { + if (DirectUtils.getCookieFromRequest(ServletActionContext.getRequest(), + ServerConfiguration.JWT_COOOKIE_KEY) == null) return "forward"; - } // populate filter data this.setupFilterPanel(); diff --git a/src/java/main/com/topcoder/direct/services/view/action/my/MyCreatedChallengesAction.java b/src/java/main/com/topcoder/direct/services/view/action/my/MyCreatedChallengesAction.java index f51522411..ecc0fea93 100644 --- a/src/java/main/com/topcoder/direct/services/view/action/my/MyCreatedChallengesAction.java +++ b/src/java/main/com/topcoder/direct/services/view/action/my/MyCreatedChallengesAction.java @@ -7,14 +7,11 @@ import com.topcoder.direct.services.view.action.ServiceBackendDataTablesAction; import com.topcoder.direct.services.view.dto.my.Challenge; import com.topcoder.direct.services.view.dto.my.RestResult; -import com.topcoder.direct.services.view.exception.JwtAuthenticationException; import com.topcoder.direct.services.view.util.DirectUtils; -import com.topcoder.direct.services.view.util.JwtTokenUpdater; import com.topcoder.service.user.UserService; import org.apache.struts2.ServletActionContext; import org.codehaus.jackson.JsonNode; -import javax.servlet.http.Cookie; import java.text.DateFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; @@ -63,11 +60,9 @@ public class MyCreatedChallengesAction extends ServiceBackendDataTablesAction { */ @Override public String execute() throws Exception { - try { - getJwtTokenUpdater().check(); - } catch (JwtAuthenticationException e) { + if (DirectUtils.getCookieFromRequest(ServletActionContext.getRequest(), + ServerConfiguration.JWT_COOOKIE_KEY) == null) return "forward"; - } // populate filter data this.setupFilterPanel(); diff --git a/src/java/main/com/topcoder/direct/services/view/interceptors/AuthenticationInterceptor.java b/src/java/main/com/topcoder/direct/services/view/interceptors/AuthenticationInterceptor.java index ed5c32c03..8851aafca 100644 --- a/src/java/main/com/topcoder/direct/services/view/interceptors/AuthenticationInterceptor.java +++ b/src/java/main/com/topcoder/direct/services/view/interceptors/AuthenticationInterceptor.java @@ -5,16 +5,17 @@ package com.topcoder.direct.services.view.interceptors; +import java.util.Arrays; import java.util.Set; -import java.util.Map; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import com.topcoder.direct.services.view.util.jwt.JWTToken; +import com.topcoder.direct.services.view.util.jwt.TokenExpiredException; import org.apache.struts2.ServletActionContext; -import com.auth0.jwt.JWTVerifier; import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.interceptor.AbstractInterceptor; @@ -282,30 +283,30 @@ public String intercept(ActionInvocation invocation) throws Exception { new SimpleResponse(response), BasicAuthentication.MAIN_SITE, DBMS.JTS_OLTP_DATASOURCE_NAME); User user = auth.getActiveUser(); - boolean jwtValid = true; - Cookie jwtCookie = DirectUtils.getCookieFromRequest(ServletActionContext.getRequest(), ServerConfiguration.JWT_COOOKIE_KEY); - - if (jwtCookie == null) { return loginPageName; } - Map decodedPayload; - + JWTToken jwtToken = null; try { - decodedPayload = new JWTVerifier(DirectProperties.CLIENT_SECRET_AUTH0, DirectProperties.CLIENT_ID_AUTH0).verify(jwtCookie.getValue()); + jwtToken = new JWTToken(jwtCookie.getValue(),DirectProperties.CLIENT_SECRET_AUTH0, + DirectProperties.JWT_VALID_ISSUERS, new JWTToken.Base64SecretEncoder()); + } catch (TokenExpiredException e) { + //refresh token here + //redirect to loginpage for now + logger.error("Token is expired. Should do refresh token here"); + return loginPageName; } catch (Exception e) { return loginPageName; } - - if (decodedPayload.get("sub") == null) { + + if (jwtToken.getSubject() == null) { return loginPageName; } - if (user != null && !user.isAnonymous()) { // get user roles for the user id Set roles = DirectUtils.getUserRoles(user.getId()); diff --git a/src/java/main/com/topcoder/direct/services/view/processor/security/LoginProcessor.java b/src/java/main/com/topcoder/direct/services/view/processor/security/LoginProcessor.java index f825ffda8..a0953df7a 100644 --- a/src/java/main/com/topcoder/direct/services/view/processor/security/LoginProcessor.java +++ b/src/java/main/com/topcoder/direct/services/view/processor/security/LoginProcessor.java @@ -3,7 +3,7 @@ */ package com.topcoder.direct.services.view.processor.security; -import com.auth0.jwt.Algorithm; +import com.auth0.jwt.algorithms.Algorithm; import com.topcoder.direct.services.configs.ServerConfiguration; import com.topcoder.direct.services.view.action.LoginAction; import com.topcoder.direct.services.view.form.LoginForm; @@ -11,6 +11,7 @@ import com.topcoder.direct.services.view.util.DirectProperties; import com.topcoder.direct.services.view.util.DirectUtils; import com.topcoder.direct.services.view.util.jwt.DirectJWTSigner; +import com.topcoder.direct.services.view.util.jwt.JWTToken; import com.topcoder.security.TCSubject; import com.topcoder.security.login.AuthenticationException; import com.topcoder.security.login.LoginRemote; @@ -75,7 +76,6 @@ public class LoginProcessor implements RequestProcessor { static { JWT_OPTIONS = new DirectJWTSigner.Options(); - JWT_OPTIONS.setAlgorithm(Algorithm.HS256); JWT_OPTIONS.setExpirySeconds(DirectProperties.JWT_EXPIRATION_SECONDS); JWT_OPTIONS.setIssuedAt(true); } @@ -131,6 +131,7 @@ public void processRequest(LoginAction action) { String sign = jwtSigner.sign(claims, JWT_OPTIONS); // add session cookie, use -1 for expiration time + log.info("Signed JWT: " + sign); DirectUtils.addDirectCookie(ServletActionContext.getResponse(), ServerConfiguration.JWT_COOOKIE_KEY, sign, -1); diff --git a/src/java/main/com/topcoder/direct/services/view/processor/security/MockLoginProcessor.java b/src/java/main/com/topcoder/direct/services/view/processor/security/MockLoginProcessor.java index 6218f331d..f06b45803 100644 --- a/src/java/main/com/topcoder/direct/services/view/processor/security/MockLoginProcessor.java +++ b/src/java/main/com/topcoder/direct/services/view/processor/security/MockLoginProcessor.java @@ -3,7 +3,7 @@ */ package com.topcoder.direct.services.view.processor.security; -import com.auth0.jwt.Algorithm; +import com.auth0.jwt.algorithms.Algorithm; import com.topcoder.direct.services.configs.ServerConfiguration; import com.topcoder.direct.services.view.action.LoginAction; import com.topcoder.direct.services.view.form.LoginForm; @@ -11,6 +11,7 @@ import com.topcoder.direct.services.view.util.DirectProperties; import com.topcoder.direct.services.view.util.DirectUtils; import com.topcoder.direct.services.view.util.jwt.DirectJWTSigner; +import com.topcoder.direct.services.view.util.jwt.JWTToken; import com.topcoder.security.RolePrincipal; import com.topcoder.security.TCPrincipal; import com.topcoder.security.TCSubject; @@ -99,7 +100,6 @@ public class MockLoginProcessor implements RequestProcessor { static { JWT_OPTIONS = new DirectJWTSigner.Options(); - JWT_OPTIONS.setAlgorithm(Algorithm.HS256); JWT_OPTIONS.setExpirySeconds(DirectProperties.JWT_EXPIRATION_SECONDS); JWT_OPTIONS.setIssuedAt(true); } @@ -210,12 +210,14 @@ public void processRequest(LoginAction action) { claims.put("aud", DirectProperties.CLIENT_ID_AUTH0); String sign = jwtSigner.sign(claims, JWT_OPTIONS); - + log.info("SIgned JWT: " + sign); // add session cookie, use -1 for expiration time DirectUtils.addDirectCookie(ServletActionContext.getResponse(), ServerConfiguration.JWT_COOOKIE_KEY, sign, -1); } catch (Exception e) { - log.error("User " + username + " could not set cookie"); + log.error("User " + username + " could not set cookie", e); + log.error(e.getMessage() + e.getCause()); + log.error(e.getStackTrace()); action.setResultCode(LoginAction.RC_INVALID_CREDENTIALS); } } diff --git a/src/java/main/com/topcoder/direct/services/view/util/DirectProperties.java b/src/java/main/com/topcoder/direct/services/view/util/DirectProperties.java index 16f5a0842..3c220d2dc 100644 --- a/src/java/main/com/topcoder/direct/services/view/util/DirectProperties.java +++ b/src/java/main/com/topcoder/direct/services/view/util/DirectProperties.java @@ -138,6 +138,11 @@ public final class DirectProperties { */ public static String USE_LOGIN_PROCESSOR; + /** + * List of known JWT issuers + */ + public static String JWT_VALID_ISSUERS; + /** *

* Initializes non-final static fields for this class with values for the same-named properties from the resource diff --git a/src/java/main/com/topcoder/direct/services/view/util/DirectUtils.java b/src/java/main/com/topcoder/direct/services/view/util/DirectUtils.java index 30108dfb4..1b96f0952 100644 --- a/src/java/main/com/topcoder/direct/services/view/util/DirectUtils.java +++ b/src/java/main/com/topcoder/direct/services/view/util/DirectUtils.java @@ -3787,7 +3787,7 @@ public static Set getGroupsFromApi(TCSubject tcSubject, JwtTokenUp HttpGet getRequest = new HttpGet(uri.build()); logger.info("Getting Group with thi uri: " + uri.build().toString()); - String v3Token = jwtTokenUpdater.check().getToken(); + String v3Token = jwtTokenUpdater.getV3Token(); getRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + v3Token); diff --git a/src/java/main/com/topcoder/direct/services/view/util/JwtTokenUpdater.java b/src/java/main/com/topcoder/direct/services/view/util/JwtTokenUpdater.java index d4e226dd5..7e6edd4cb 100644 --- a/src/java/main/com/topcoder/direct/services/view/util/JwtTokenUpdater.java +++ b/src/java/main/com/topcoder/direct/services/view/util/JwtTokenUpdater.java @@ -40,13 +40,6 @@ public class JwtTokenUpdater { */ private String authorizationURL; - /** - * v3 token - */ - private String token; - - private String v2Token = null; - /** * ssoLogin Url */ @@ -69,12 +62,12 @@ public JwtTokenUpdater() { } /** - * Check token from cookie + * Validate and get v3 token from cookies * - * @return this class instance + * @return v3 token * @throws Exception */ - public JwtTokenUpdater check() throws Exception { + public String getV3Token() throws Exception { Cookie jwtCookieV3 = DirectUtils.getCookieFromRequest(ServletActionContext.getRequest(), ServerConfiguration.JWT_V3_COOKIE_KEY); Cookie jwtCookieV2 = DirectUtils.getCookieFromRequest(ServletActionContext.getRequest(), @@ -84,9 +77,7 @@ public JwtTokenUpdater check() throws Exception { throw new JwtAuthenticationException("Please re-login"); } - validateCookieV2V3(jwtCookieV2,jwtCookieV3); - v2Token = jwtCookieV2.getValue(); - return this; + return validateCookieV2V3(jwtCookieV2,jwtCookieV3); } @@ -163,9 +154,10 @@ private String getValidJwtToken(String v3token, String v2token) throws JwtAuthen * * @param v2 cookie v2 * @param v3 cookie v3 + * @return valid v3 token * @throws Exception */ - private void validateCookieV2V3(Cookie v2, Cookie v3) throws Exception { + private String validateCookieV2V3(Cookie v2, Cookie v3) throws Exception { String validToken; String v3Token = null; if (v3 == null) { @@ -179,17 +171,9 @@ private void validateCookieV2V3(Cookie v2, Cookie v3) throws Exception { DirectUtils.addDirectCookie(ServletActionContext.getResponse(), ServerConfiguration.JWT_V3_COOKIE_KEY, validToken, -1); } - token = validToken; + return validToken; } - /** - * True if user has logge-in and has v2token - * Must be called after {@link #check()} - * @return - */ - public boolean isLoggedIn() { - return v2Token != null && !v2Token.isEmpty(); - } public String getAuthorizationURL() { return authorizationURL; @@ -206,13 +190,4 @@ public String getSsoLoginUrl() { public void setSsoLoginUrl(String ssoLoginUrl) { this.ssoLoginUrl = ssoLoginUrl; } - - /** - * Get v3 token - * Must be called after {@link #check()} - * @return - */ - public String getToken() { - return token; - } } diff --git a/src/java/main/com/topcoder/direct/services/view/util/jwt/DirectJWTSigner.java b/src/java/main/com/topcoder/direct/services/view/util/jwt/DirectJWTSigner.java index c37efa672..ad274cdd9 100644 --- a/src/java/main/com/topcoder/direct/services/view/util/jwt/DirectJWTSigner.java +++ b/src/java/main/com/topcoder/direct/services/view/util/jwt/DirectJWTSigner.java @@ -1,42 +1,65 @@ package com.topcoder.direct.services.view.util.jwt; -import com.auth0.jwt.Algorithm; -import com.auth0.jwt.internal.com.fasterxml.jackson.databind.ObjectMapper; -import com.auth0.jwt.internal.com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.auth0.jwt.internal.com.fasterxml.jackson.databind.node.ObjectNode; -import com.auth0.jwt.internal.org.apache.commons.codec.binary.Base64; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import javax.naming.OperationNotSupportedException; -import java.io.UnsupportedEncodingException; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTCreator; +import com.auth0.jwt.algorithms.Algorithm; +import org.apache.log4j.Logger; + import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; +import java.util.Date; +import java.util.Map; import java.util.HashMap; -import java.util.Iterator; import java.util.List; -import java.util.Map; +import java.util.Arrays; import java.util.UUID; +import java.util.Collection; +import java.util.Iterator; /** * JwtSigner implementation based on the Ruby implementation from http://jwt.io * No support for RSA encryption at present + * + * Change on version 2.0 + * - Use JWTCreator.Builder to create token + * + * @version 2.0 */ public class DirectJWTSigner { + /** + * Logging + */ + private static final Logger logger = Logger.getLogger(DirectJWTSigner.class); /** * The base64 encoded secret. */ private final String secret; /** - * Create the JWT signer with the base64 encoded secret. + * Secret encoder + */ + private JWTToken.SecretEncoder secretEncoder = new JWTToken.Base64SecretEncoder(); + + /** + * Create the JWT signer * - * @param secret the base64 encoded secret. + * @param secret secret. */ public DirectJWTSigner(String secret) { + this(secret, null); + } + + /** + * Create the JWT signer with specific encoder + * + * @param secret secret + * @param secretEncoder secret encoder + */ + public DirectJWTSigner(String secret, JWTToken.SecretEncoder secretEncoder) { this.secret = secret; + if (secretEncoder != null) { + this.secretEncoder = secretEncoder; + } } /** @@ -56,57 +79,32 @@ public DirectJWTSigner(String secret) { * the "options" parameter override claims in this map. * @param options Allow choosing the signing algorithm, and automatic setting of some registered claims. */ - public String sign(Map claims, Options options) { - Algorithm algorithm = Algorithm.HS256; - if (options != null && options.algorithm != null) + public String sign(Map claims, Options options) throws Exception{ + Algorithm algorithm = Algorithm.HMAC256(secretEncoder.encode(secret)); + if (options != null && options.algorithm != null) { algorithm = options.algorithm; - - List segments = new ArrayList(); - try { - segments.add(encodedHeader(algorithm)); - segments.add(encodedPayload(claims, options)); - segments.add(encodedSignature(join(segments, "."), algorithm)); - } catch (Exception e) { - throw (e instanceof RuntimeException) ? (RuntimeException) e : new RuntimeException(e); } - - return join(segments, "."); + JWTCreator.Builder builder = buildPayload(claims, options); + return builder.sign(algorithm); } /** - * Generate a JSON Web Token using the default algorithm HMAC SHA-256 ("HS256") - * and no claims automatically set. + * Create JWTCreator.Builder from claims Map * * @param claims Key to use in signing. Used as-is without Base64 encoding. *

* For details, see the two parameter variant of this method. */ - public String sign(Map claims) { + public String sign(Map claims) throws Exception{ return sign(claims, null); } /** - * Generate the header part of a JSON web token. - */ - private String encodedHeader(Algorithm algorithm) throws UnsupportedEncodingException { - if (algorithm == null) { // default the algorithm if not specified - algorithm = Algorithm.HS256; - } - - // create the header - ObjectNode header = JsonNodeFactory.instance.objectNode(); - header.put("type", "JWT"); - header.put("alg", algorithm.name()); - - return base64UrlEncode(header.toString().getBytes("UTF-8")); - } - - /** - * Generate the JSON web token payload string from the claims. + * Generate the JWTCreator.Builder payload string from the claims. * * @param options */ - private String encodedPayload(Map _claims, Options options) throws Exception { + private JWTCreator.Builder buildPayload(Map _claims, Options options) throws Exception { Map claims = new HashMap(_claims); enforceStringOrURI(claims, "iss"); enforceStringOrURI(claims, "sub"); @@ -119,8 +117,27 @@ private String encodedPayload(Map _claims, Options options) thro if (options != null) processPayloadOptions(claims, options); - String payload = new ObjectMapper().writeValueAsString(claims); - return base64UrlEncode(payload.getBytes("UTF-8")); + JWTCreator.Builder builder = JWT.create(); + String[] _dateTypeClaims = new String[]{"exp", "nbf", "iat"}; + List dateTypeClaims = Arrays.asList(_dateTypeClaims); + + logger.info("Build jwt claims"); + for (String key : claims.keySet()) { + if (!dateTypeClaims.contains(key)) { + builder.withClaim(key, (String)claims.get(key)); + } + } + + if (claims.get("exp") != null && (Long)claims.get("exp") > 0) + builder.withExpiresAt(new Date((Long)claims.get("exp") * 1000)); + + if (claims.get("nbf") != null && (Long)claims.get("nbf") > 0) + builder.withNotBefore(new Date((Long)claims.get("nbf") * 1000)); + + if (claims.get("iat") != null && (Long)claims.get("iat") > 0) + builder.withIssuedAt(new Date((Long)claims.get("iat") * 1000)); + + return builder; } private void processPayloadOptions(Map claims, Options options) { @@ -208,62 +225,6 @@ private String checkStringOrURI(Object value) { return null; } - /** - * Sign the header and payload - */ - private String encodedSignature(String signingInput, Algorithm algorithm) throws Exception { - byte[] signature = sign(algorithm, signingInput, secret); - return base64UrlEncode(signature); - } - - /** - * Safe URL encode a byte array to a String - */ - private String base64UrlEncode(byte[] str) { - return new String(Base64.encodeBase64URLSafe(str)); - } - - /** - * Switch the signing algorithm based on input, RSA not supported - */ - private static byte[] sign(Algorithm algorithm, String msg, String secret) throws Exception { - switch (algorithm) { - case HS256: - case HS384: - case HS512: - return signHmac(algorithm, msg, secret); - case RS256: - case RS384: - case RS512: - default: - throw new OperationNotSupportedException("Unsupported signing method"); - } - } - - /** - * Sign an input string using HMAC and return the encrypted bytes - */ - private static byte[] signHmac(Algorithm algorithm, String msg, String secret) throws Exception { - Mac mac = Mac.getInstance(algorithm.getValue()); - mac.init(new SecretKeySpec(Base64.decodeBase64(secret), algorithm.getValue())); - return mac.doFinal(msg.getBytes()); - } - - private String join(List input, String on) { - int size = input.size(); - int count = 1; - StringBuilder joined = new StringBuilder(); - for (String string : input) { - joined.append(string); - if (count < size) { - joined.append(on); - } - count++; - } - - return joined.toString(); - } - /** * An option object for JWT signing operation. Allow choosing the algorithm, and/or specifying * claims to be automatically set. diff --git a/src/java/main/com/topcoder/direct/services/view/util/jwt/InvalidTokenException.java b/src/java/main/com/topcoder/direct/services/view/util/jwt/InvalidTokenException.java new file mode 100644 index 000000000..7bb13fbcf --- /dev/null +++ b/src/java/main/com/topcoder/direct/services/view/util/jwt/InvalidTokenException.java @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2017 TopCoder Inc., All Rights Reserved. + */ +package com.topcoder.direct.services.view.util.jwt; + +/** + * Exception for invalid Jwt Token + */ +public class InvalidTokenException extends JWTException { + public InvalidTokenException(String message) { + super(message); + } + + public InvalidTokenException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/java/main/com/topcoder/direct/services/view/util/jwt/JWTException.java b/src/java/main/com/topcoder/direct/services/view/util/jwt/JWTException.java new file mode 100644 index 000000000..e5b36b867 --- /dev/null +++ b/src/java/main/com/topcoder/direct/services/view/util/jwt/JWTException.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2017 TopCoder Inc., All Rights Reserved. + */ +package com.topcoder.direct.services.view.util.jwt; + +import com.topcoder.util.errorhandling.BaseException; + +/** + * Base exception for Jwt token + */ +public class JWTException extends BaseException { + public JWTException(String message) { + super(message); + } + + public JWTException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/java/main/com/topcoder/direct/services/view/util/jwt/JWTToken.java b/src/java/main/com/topcoder/direct/services/view/util/jwt/JWTToken.java new file mode 100644 index 000000000..d97a647b6 --- /dev/null +++ b/src/java/main/com/topcoder/direct/services/view/util/jwt/JWTToken.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2017 TopCoder Inc., All Rights Reserved. + */ +package com.topcoder.direct.services.view.util.jwt; + +import java.security.interfaces.RSAPublicKey; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; + +import org.apache.commons.codec.binary.Base64; +import org.apache.log4j.Logger; + +import com.auth0.jwk.GuavaCachedJwkProvider; +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.UrlJwkProvider; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.Verification; + +/** + * Jwt token. Main purpose is to verify token. + * Verification based on token signature, issuers, and time validation + * + */ +public class JWTToken { + + private static final Logger logger = Logger.getLogger(JWTToken.class); + + public static final String CLAIM_USER_ID = "userId"; + public static final String CLAIM_HANDLE = "handle"; + public static final String CLAIM_EMAIL = "email"; + public static final String CLAIM_ROLES = "roles"; + public static final String CLAIM_SUBJECT = "sub"; + + public static final String CLAIM_ISSUER = "iss"; + public static final String CLAIM_ISSUED_TIME = "iat"; + public static final String CLAIM_EXPIRATION_TIME = "exp"; + + public static final int DEFAULT_EXP_SECONDS = 10 * 60; // 10 mins + + private String userId; + + private String handle; + + private String email; + + private String issuer; + + private String subject; + + private String token; + + private String secret; + + private List roles = new ArrayList(); + + private Integer expirySeconds = DEFAULT_EXP_SECONDS; + + private List knownIssuers = new ArrayList(); + + private String algorithmName = "HS256"; + + protected SecretEncoder encoder = new Base64SecretEncoder(); + + /** + * Constructor + * + * @param token token + * @param secret secret, if algorithm required it + * @param knownIssuers comma separate known issuers + * @param secretEncoder encoder of secret + * @throws JWTException + */ + public JWTToken(String token, String secret, String knownIssuers, SecretEncoder secretEncoder) throws JWTException{ + if (token == null) { + logger.error("token can not be null"); + throw new IllegalArgumentException("token can not be null"); + } + if (knownIssuers == null) { + logger.error("issuers can not be null"); + throw new IllegalArgumentException("issuers can not be null"); + } + + if (secretEncoder != null) + this.encoder = secretEncoder; + + for (String issuer : knownIssuers.split("\\s*,\\s*")) { + this.knownIssuers.add(issuer.trim()); + } + + + setTokenAndSecret(token, secret); + } + + /** + * Verify this JWT token + * + * @param algorithm algorithm to be used + * @throws JWTException + */ + protected void verify(Algorithm algorithm) throws JWTException { + try { + Verification verification = JWT.require(algorithm); + + JWTVerifier verifier = verification.build(); + DecodedJWT decodedJWT = verifier.verify(token); + + // Validate the issuer + if (decodedJWT.getIssuer() == null || !knownIssuers.contains(decodedJWT.getIssuer())) { + logger.error("Invalid issuer:" + decodedJWT.getIssuer() + ", token: " + token); + throw new InvalidTokenException("Invalid issuer: " + decodedJWT.getIssuer() + ", token: " + token); + } + logger.info("This JWT Token was issued by " + decodedJWT.getIssuer() + " is valid"); + } catch (com.auth0.jwt.exceptions.TokenExpiredException e) { + logger.error("Token is expired. " + token); + throw new TokenExpiredException(token + "Token is expired.", e); + } catch (SignatureVerificationException e) { + logger.error("Token is invalid. " + token); + throw new InvalidTokenException(token + "Token is invalid. " + e.getLocalizedMessage(), e); + } catch (IllegalStateException e) { + logger.error("Token is invalid. " + token); + throw new InvalidTokenException(token + "Token is invalid. " + e.getLocalizedMessage(), e); + } catch (Exception e) { + logger.error("Error occurred in verifying token. " + token); + throw new JWTException(token + "Error occurred in verifying token. " + e.getLocalizedMessage(), + e); + } + } + + /** + * Set this JWT class besed on token string + * + * @throws JWTException + */ + protected void apply() throws JWTException { + apply(this.encoder); + } + + /** + * Set this JWT class based on token string + * @param enc secret encoder + * @throws JWTException + */ + protected void apply(SecretEncoder enc) throws JWTException { + if (token == null) + throw new IllegalArgumentException("token must be specified."); + + DecodedJWT decodedJWT = null; + + // Decode only first to get the algorithm + try { + decodedJWT = JWT.decode(token); + logger.info("Jwt decoded payload: " + decodedJWT.getPayload()); + } catch (JWTDecodeException e) { + throw new InvalidTokenException("Error occurred in decoding token: " + token + " : " + e.getLocalizedMessage(), e); + } + + algorithmName = decodedJWT.getAlgorithm(); + + // Create the algorithm + logger.info("using algorithm: " + algorithmName); + + Algorithm algorithm; + + if ("RS256".equals(algorithmName)) { + // Create the JWK provider with caching + JwkProvider urlJwkProvider = new UrlJwkProvider(decodedJWT.getIssuer()); + JwkProvider jwkProvider = new GuavaCachedJwkProvider(urlJwkProvider); + + // Get the public key and create the algorithm + try { + logger.info("Getting pub key from " + decodedJWT.getIssuer()); + Jwk jwk = jwkProvider.get(decodedJWT.getKeyId()); + RSAPublicKey publicKey = (RSAPublicKey) jwk.getPublicKey(); + logger.info("Pubkey: " + new String(publicKey.getEncoded())); + + algorithm = Algorithm.RSA256(publicKey, null); + } catch (Exception e) { + logger.error("Error occurred in creating algorithm. token: " + token); + throw new JWTException("Error occurred in creating algorithm. token: " + token + " . " + e.getLocalizedMessage(), e); + } + } else if ("HS256".equals(algorithmName)) { + if (secret == null || secret.length() == 0) { + logger.error("secret must be specified."); + throw new IllegalArgumentException("secret must be specified."); + } + if (enc == null) + enc = new SecretEncoder(); + + // Create the algorithm + try { + algorithm = Algorithm.HMAC256(enc.encode(secret)); + } catch (Exception e) { + throw new JWTException("Error occurred in creating algorithm. token: " + token + " . " + e.getLocalizedMessage(), e); + } + } else { + throw new JWTException("Algorithm not supported: " + algorithmName); + } + verify(algorithm); + decodedJWTApply(decodedJWT); + } + + /** + * Set up fields from decodedJWT + * + * @param decodedJWT + */ + @SuppressWarnings("unchecked") + protected void decodedJWTApply(DecodedJWT decodedJWT) { + // Search for the custom claims + for (Entry entry : decodedJWT.getClaims().entrySet()) { + String key = entry.getKey(); + Claim claim = entry.getValue(); + + if (key.contains(CLAIM_USER_ID)) { + setUserId(claim.asString()); + } else if (key.contains(CLAIM_EMAIL)) { + setEmail(claim.asString()); + } else if (key.contains(CLAIM_HANDLE)) { + setHandle(claim.asString()); + } else if (key.contains(CLAIM_ROLES)) { + setRoles(claim.as(List.class)); + } else if (key.contains(CLAIM_SUBJECT)) { + setSubject(claim.asString()); + } + } + + setIssuer(decodedJWT.getClaim(CLAIM_ISSUER).asString()); + Integer iat = decodedJWT.getClaim(CLAIM_ISSUED_TIME).asInt(); + Integer exp = decodedJWT.getClaim(CLAIM_EXPIRATION_TIME).asInt(); + if (exp != null) { + setExpirySeconds(calcExpirySeconds(exp, iat)); + } + } + + /** + * Calculate expired time + * + * @param exp + * @param iat + * @return + */ + protected Integer calcExpirySeconds(Integer exp, Integer iat) { + if (exp == null) + return null; + int issuedAt = iat != null ? iat : (int) (System.currentTimeMillis() / 1000L); + return exp - issuedAt; + } + + public String getUserId() { + return userId; + } + + /** + * Set JWT claim "userId" (private). + */ + public void setUserId(String userId) { + this.userId = userId; + } + + public String getHandle() { + return handle; + } + + /** + * Set JWT claim "handle" (private). + */ + public void setHandle(String handle) { + this.handle = handle; + } + + public String getEmail() { + return email; + } + + /** + * Set JWT claim "email" (private). + */ + public void setEmail(String email) { + this.email = email; + } + + public String getIssuer() { + return issuer; + } + + /** + * Set JWT claim "roles" (private). + * + * @param roles + */ + public void setRoles(List roles) { + this.roles = roles; + } + + public List getRoles() { + return roles; + } + + /** + * Set JWT claim "iss". + */ + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public Integer getExpirySeconds() { + return expirySeconds; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + /** + * Set JWT claim "exp" to current timestamp plus this value. + */ + public void setExpirySeconds(Integer expirySeconds) { + this.expirySeconds = expirySeconds; + } + + public String getAlgorithm() { + return algorithmName; + } + + /** + * Set algorithm (default: HS256 [HMAC SHA-256]) + */ + public void setAlgorithm(String algorithm) { + this.algorithmName = algorithm; + } + + public String getToken() { + return token; + } + + /** + * Set new token + * + * @param token + * @param secret + * @throws JWTException + */ + public void setTokenAndSecret(String token, String secret) throws JWTException { + if (token == null) { + throw new IllegalArgumentException("token can not be null"); + } + this.token = token; + this.secret = secret; + apply(); + } + + public static class SecretEncoder { + public byte[] encode(String secret) { + return secret != null ? secret.getBytes() : null; + } + } + + public static class Base64SecretEncoder extends SecretEncoder { + public byte[] encode(String secret) { + return secret != null ? Base64.decodeBase64(secret) : null; + } + } +} \ No newline at end of file diff --git a/src/java/main/com/topcoder/direct/services/view/util/jwt/TokenExpiredException.java b/src/java/main/com/topcoder/direct/services/view/util/jwt/TokenExpiredException.java new file mode 100644 index 000000000..cf1d0623f --- /dev/null +++ b/src/java/main/com/topcoder/direct/services/view/util/jwt/TokenExpiredException.java @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2017 TopCoder Inc., All Rights Reserved. + */ +package com.topcoder.direct.services.view.util.jwt; + +/** + * Exception for expired token + */ +public class TokenExpiredException extends JWTException { + public TokenExpiredException(String message) { + super(message); + } + + public TokenExpiredException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/token.properties.docker b/token.properties.docker index d2930e9f5..dff2d3332 100644 --- a/token.properties.docker +++ b/token.properties.docker @@ -326,7 +326,7 @@ @CLIENT_SECRET_AUTH0@ = ZEEIRf_aLhvbYymAMTFefoEJ_8y7ELrUaboMTmE5fQoJXEo7sxxyg8IW6gtbyKuT @REG_SERVER_NAME@= tc.cloud.topcoder.com @LDAP_AUTH0_CONNECTION_NAME@=vm-ldap-connection - +@JWT_VALID_ISSUERS@ = https://sma.auth0.com, https://newtc.auth0.com @ApplicationServer.SSO_COOKIE_KEY@=tcsso_vm @ApplicationServer.SSO_HASH_SECRET@=GKDKJF80dbdc541fe829898aa01d9e30118bab5d6b9fe94fd052a40069385f5628 diff --git a/token.properties.example b/token.properties.example index a2e9b4c1c..9f5824dea 100644 --- a/token.properties.example +++ b/token.properties.example @@ -394,6 +394,7 @@ @REG_SERVER_NAME@=tc.cloud.topcoder.com @LDAP_AUTH0_CONNECTION_NAME@=vm-ldap-connection @member.profile.url.base@=http://tc.cloud.topcoder.com +@JWT_VALID_ISSUERS@ = https://sma.auth0.com, https://newtc.auth0.com @memberSearchApiUrl@=https://tc-api.cloud.topcoder.com:8443/v3/members/_suggest/ @groupMemberSearchApiUrl@=https://cockpit.cloud.topcoder.com/direct/group/member?handle=