Skip to content

Commit

Permalink
fix(auth0): handle app_metadata and verification for new auth0 tenants
Browse files Browse the repository at this point in the history
New Auth0 tenants no longer have access to app_metadata or user_metadata on the user profile by
default. A custom rule must be set up in Auth0 to attach these objects to the idToken. This commit
handles remapping those values to the previous app- and user_metadata fields and attaches the entire
profile to the request object (for sending back to the requesting user).

refs catalogueglobal/datatools-ui#207
  • Loading branch information
landonreed committed Aug 30, 2018
1 parent 526ef4a commit 1281c6d
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 12 deletions.
5 changes: 4 additions & 1 deletion configurations/default/env.yml.tmp
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
AUTH0_CLIENT_ID: your-auth0-client-id
AUTH0_DOMAIN: your-auth0-domain
AUTH0_SECRET: your-auth0-secret
# Note: One of AUTH0_SECRET or AUTH0_PUBLIC_KEY should be used depending on the signing algorithm set on the client.
# It seems that newer Auth0 accounts (2017 and later) might default to RS256 (public key).
AUTH0_SECRET: your-auth0-secret # uses HS256 signing algorithm
# AUTH0_PUBLIC_KEY: /path/to/auth0.pem # uses RS256 signing algorithm
AUTH0_TOKEN: your-auth0-token
DISABLE_AUTH: false
OSM_VEX: http://localhost:1000
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>2.1.0</version>
<version>2.3.0</version>
</dependency>

</dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.conveyal.datatools.manager.auth;

import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.internal.org.apache.commons.codec.binary.Base64;
import com.auth0.jwt.JWTVerifyException;
import com.auth0.jwt.pem.PemReader;
import com.conveyal.datatools.common.utils.SparkUtils;
import com.conveyal.datatools.manager.DataManager;
import com.conveyal.datatools.manager.models.FeedSource;
Expand All @@ -15,22 +16,38 @@

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

import static com.conveyal.datatools.common.utils.SparkUtils.haltWithMessage;
import static com.conveyal.datatools.manager.DataManager.getConfigPropertyAsText;
import static com.conveyal.datatools.manager.DataManager.hasConfigProperty;
import static spark.Spark.halt;

/**
* This handles verifying the Auth0 token passed in the Auth header of Spark HTTP requests.
*
* Created by demory on 3/22/16.
*/

public class Auth0Connection {
public static final String APP_METADATA = "app_metadata";
public static final String USER_METADATA = "user_metadata";
public static final String SCOPE = "http://datatools";
public static final String SCOPED_APP_METADATA = String.join("/", SCOPE, APP_METADATA);
public static final String SCOPED_USER_METADATA = String.join("/", SCOPE, USER_METADATA);
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Logger LOG = LoggerFactory.getLogger(Auth0Connection.class);
private static final String BASE_URL = getConfigPropertyAsText("application.public_url");
private static final int DEFAULT_LINES_TO_PRINT = 10;
private static JWTVerifier verifier;

/**
* Check the incoming API request for the user token (and verify it) and assign as the "user" attribute on the
Expand Down Expand Up @@ -60,17 +77,64 @@ public static void checkUser(Request req) {
// Validate the JWT and cast into the user profile, which will be attached as an attribute on the request object
// for downstream controllers to check permissions.
try {
byte[] decodedSecret = new Base64().decode(getConfigPropertyAsText("AUTH0_SECRET"));
JWTVerifier verifier = new JWTVerifier(decodedSecret);
Map<String, Object> jwt = verifier.verify(token);
Map<String, Object> jwt = verifyToken(token);
remapTokenValues(jwt);
Auth0UserProfile profile = MAPPER.convertValue(jwt, Auth0UserProfile.class);
// The user attribute is used on the server side to check user permissions and does not have all of the
// fields that the raw Auth0 profile string does.
req.attribute("user", profile);
// The raw_user attribute is used to return the complete user profile string to the client upon request in
// the UserController. Previously the client sought the profile directly from Auth0 but the need to add
req.attribute("raw_user", MAPPER.writeValueAsString(jwt));
} catch (Exception e) {
LOG.warn("Login failed to verify with our authorization provider.", e);
haltWithMessage(401, "Could not verify user's token");
}
}

/**
* Choose the correct JWT verification algorithm (based on the values present in env.yml config) and verify the JWT
* token.
*/
private static Map<String, Object> verifyToken(String token) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, IOException, JWTVerifyException, InvalidKeyException, SignatureException {
if (verifier == null) {
if (hasConfigProperty("AUTH0_SECRET")) {
// Use HS256 algorithm to verify token (uses client secret).
byte[] decodedSecret = new org.apache.commons.codec.binary.Base64().decode(getConfigPropertyAsText("AUTH0_SECRET"));
verifier = new JWTVerifier(decodedSecret);
} else if (hasConfigProperty("AUTH0_PUBLIC_KEY")) {
// Use RS256 algorithm to verify token (uses public key/.pem file).
PublicKey publicKey = PemReader.readPublicKey(getConfigPropertyAsText("AUTH0_PUBLIC_KEY"));
verifier = new JWTVerifier(publicKey);
} else {
throw new RuntimeException("Server authentication provider not configured correctly.");
}
}
return verifier.verify(token);
}

/**
* Handle mapping token values to the expected keys. This accounts for app_metadata and user_metadata that have been
* scoped to conform with OIDC (i.e., how newer Auth0 accounts structure the user profile) as well as the user_id ->
* sub mapping.
*/
private static void remapTokenValues(Map<String, Object> jwt) {
// If token did not contain app_metadata or user_metadata, add the scoped values to the decoded token object.
if (!jwt.containsKey(APP_METADATA) && jwt.containsKey(SCOPED_APP_METADATA)) {
jwt.put(APP_METADATA, jwt.get(SCOPED_APP_METADATA));
}
if (!jwt.containsKey(USER_METADATA) && jwt.containsKey(SCOPED_USER_METADATA)) {
jwt.put(USER_METADATA, jwt.get(SCOPED_USER_METADATA));
}
// Do the same for user_id -> sub
if (!jwt.containsKey("user_id") && jwt.containsKey("sub")) {
jwt.put("user_id", jwt.get("sub"));
}
// Remove scoped metadata objects to clean up user profile object.
jwt.remove(SCOPED_APP_METADATA);
jwt.remove(SCOPED_USER_METADATA);
}

/**
* Check that the user has edit privileges for the feed ID specified. NOTE: the feed ID provided in the request will
* represent a feed source, not a specific SQL namespace that corresponds to a feed version or specific set of GTFS
Expand Down Expand Up @@ -175,12 +239,19 @@ public static void logRequestOrResponse(boolean logRequest, Request request, Res
private static String trimLines(String str) {
if (str == null) return "";
String[] lines = str.split("\n");
if (lines.length <= DEFAULT_LINES_TO_PRINT) return str;
return String.format(
"%s \n...and %d more lines",
String.join("\n", Arrays.copyOfRange(lines, 0, DEFAULT_LINES_TO_PRINT - 1)),
lines.length - DEFAULT_LINES_TO_PRINT
);
boolean linesExceedLimit = lines.length > DEFAULT_LINES_TO_PRINT;
// Gather lines to print in smaller array (so that filter below only has to be applied to a few lines).
String[] linesToPrint = linesExceedLimit
? Arrays.copyOfRange(lines, 0, DEFAULT_LINES_TO_PRINT - 1)
: lines;
String stringToPrint = Arrays.stream(linesToPrint)
// Filter out any password found in JSON (e.g., when creating an Auth0 user), so that sensitive information
// is not logged. NOTE: this is a pretty clumsy match, but it is probably better to err on the side of caution.
.map(line -> line.contains("password") ? " \"password\": \"xxxxxx\", (value filtered by logger)" : line)
.collect(Collectors.joining("\n"));
return linesExceedLimit
? String.format("%s \n...and %d more lines", stringToPrint, lines.length - DEFAULT_LINES_TO_PRINT)
: stringToPrint;
}

/**
Expand Down

0 comments on commit 1281c6d

Please sign in to comment.