Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@
public class AuthenticationOptions implements Option {

public static final String DEFAULT_ALGORITHM = "HS256";
public static final String DEFAULT_ISSUER = "Gentics Mesh";

public static final int DEFAULT_TOKEN_EXPIRATION_TIME = 60 * 60; // 1 hour
public static final int DEFAULT_LEEWAY = 0;

public static final boolean DEFAULT_IGNORE_EXPIRATION = false;

public static final String DEFAULT_KEYSTORE_PATH = CONFIG_FOLDERNAME + "/keystore.jceks";

Expand All @@ -37,6 +41,10 @@ public class AuthenticationOptions implements Option {
public static final String MESH_AUTH_KEYSTORE_PASS_ENV = "MESH_AUTH_KEYSTORE_PASS";
public static final String MESH_AUTH_KEYSTORE_PATH_ENV = "MESH_AUTH_KEYSTORE_PATH";
public static final String MESH_AUTH_JWT_ALGO_ENV = "MESH_AUTH_JWT_ALGO";
public static final String MESH_AUTH_JWT_LEEWAY_ENV = "MESH_AUTH_JWT_LEEWAY";
public static final String MESH_AUTH_JWT_AUDIENCE_ENV = "MESH_AUTH_JWT_AUDIENCE";
public static final String MESH_AUTH_JWT_ISSUER_ENV = "MESH_AUTH_JWT_ISSUER";
public static final String MESH_AUTH_JWT_IGNORE_EXPIRATION_ENV = "MESH_AUTH_JWT_IGNORE_EXPIRATION";
public static final String MESH_AUTH_ANONYMOUS_ENABLED_ENV = "MESH_AUTH_ANONYMOUS_ENABLED";
public static final String MESH_AUTH_PUBLIC_KEYS_PATH_ENV = "MESH_AUTH_PUBLIC_KEYS_PATH";

Expand All @@ -60,6 +68,26 @@ public class AuthenticationOptions implements Option {
@EnvironmentVariable(name = MESH_AUTH_JWT_ALGO_ENV, description = "Override the configured algorithm which is used to sign the JWT.")
private String algorithm = DEFAULT_ALGORITHM;

@JsonProperty(required = false)
@JsonPropertyDescription("Leeway (in seconds) of how long a JWT should still be considered valid.")
@EnvironmentVariable(name = MESH_AUTH_JWT_LEEWAY_ENV, description = "Override the configured Leeway (in seconds) of how long a JWT should still be considered valid.")
private int leeway = DEFAULT_LEEWAY;

@JsonProperty(required = false)
@JsonPropertyDescription("The issuer of the JWT which is also written into the token.")
@EnvironmentVariable(name = MESH_AUTH_JWT_ISSUER_ENV, description = "Override the configured issuer of the JWT.")
private String issuer = DEFAULT_ISSUER;

@JsonProperty(required = false)
@JsonPropertyDescription("The expected audience of the JWT.")
@EnvironmentVariable(name = MESH_AUTH_JWT_AUDIENCE_ENV, description = "Override the configured audience of the JWT.")
private List<String> audience = null;

@JsonProperty(required = false)
@JsonPropertyDescription("If expired JWT should still be accepted and processed.")
@EnvironmentVariable(name = MESH_AUTH_JWT_IGNORE_EXPIRATION_ENV, description = "Overrides if an expired JWT should still be accepted and processed.")
private boolean ignoreExpiration = DEFAULT_IGNORE_EXPIRATION;

@JsonProperty(required = false)
@JsonPropertyDescription("Flag which indicates whether anonymous access should be enabled.")
@EnvironmentVariable(name = MESH_AUTH_ANONYMOUS_ENABLED_ENV, description = "Override the configured anonymous enabled flag.")
Expand Down Expand Up @@ -140,6 +168,46 @@ public AuthenticationOptions setAlgorithm(String algorithm) {
return this;
}

public int getLeeway() {
return leeway;
}

@Setter
public AuthenticationOptions setLeeway(int leeway) {
this.leeway = leeway;
return this;
}

public String getIssuer() {
return issuer;
}

@Setter
public AuthenticationOptions setIssuer(String issuer) {
this.issuer = issuer;
return this;
}

public List<String> getAudience() {
return audience;
}

@Setter
public AuthenticationOptions setAudience(List<String> audience) {
this.audience = audience;
return this;
}

public boolean isIgnoreExpiration() {
return ignoreExpiration;
}

@Setter
public AuthenticationOptions setIgnoreExpiration(boolean ignoreExpiration) {
this.ignoreExpiration = ignoreExpiration;
return this;
}

public boolean isEnableAnonymousAccess() {
return enableAnonymousAccess;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Authentication: Added JWT options to configure token content (`issuer` - `"iss"`, `audience` - `"aud"`) and token verification (`leeway` and `ignoreExpiration`).
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.gentics.mesh.auth.AuthenticationResult;
Expand All @@ -22,15 +24,14 @@
import com.gentics.mesh.etc.config.AuthenticationOptions;
import com.gentics.mesh.etc.config.MeshOptions;
import com.gentics.mesh.shared.SharedKeys;
import com.gentics.mesh.util.JWTUtil;

import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.Cookie;
import io.vertx.core.json.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.vertx.ext.auth.JWTOptions;
import io.vertx.ext.auth.KeyStoreOptions;
import io.vertx.ext.auth.User;
Expand All @@ -50,24 +51,35 @@ public class MeshJWTAuthProvider implements AuthenticationProvider, JWTAuth {

private static final Logger log = LoggerFactory.getLogger(MeshJWTAuthProvider.class);

private JWTAuth jwtProvider;

private static final String USERID_FIELD_NAME = "userUuid";

private static final String API_KEY_TOKEN_CODE_FIELD_NAME = "jti";

protected final Vertx vertx;
protected final Database db;

private BCryptPasswordEncoder passwordEncoder;

private JWTAuth jwtProvider;
private final BCryptPasswordEncoder passwordEncoder;
private final MeshOptions meshOptions;

@Inject
public MeshJWTAuthProvider(Vertx vertx, MeshOptions meshOptions, BCryptPasswordEncoder passwordEncoder, Database database,
BootstrapInitializer boot) {
this.vertx = vertx;
this.meshOptions = meshOptions;
this.passwordEncoder = passwordEncoder;
this.db = database;
}

/**
* Delayed initialize function, which is only executed once this provider is actually needed
* ({@link #authenticateJWT(JsonObject, Handler) and {@link #generateToken(String, String, String)}}.
* This is done, to make testing possible, as otherwise the provider would be initialized with the
* settings, before the tests could change them (and therefore make it impossible to test different configurations).
*/
public void initialize() {
if (this.jwtProvider != null) {
return;
}

// Use the mesh JWT options in order to setup the JWTAuth provider
AuthenticationOptions options = meshOptions.getAuthenticationOptions();
Expand All @@ -79,8 +91,10 @@ public MeshJWTAuthProvider(Vertx vertx, MeshOptions meshOptions, BCryptPasswordE
String keyStorePath = options.getKeystorePath();
String type = "jceks";
JWTAuthOptions config = new JWTAuthOptions();
// Set JWT options from the config
config.setJWTOptions(JWTUtil.createJWTOptions(options));
config.setKeyStore(new KeyStoreOptions().setPath(keyStorePath).setPassword(keystorePassword).setType(type));
jwtProvider = JWTAuth.create(vertx, new JWTAuthOptions(config));
this.jwtProvider = JWTAuth.create(vertx, new JWTAuthOptions(config));
}

/**
Expand All @@ -92,6 +106,8 @@ public MeshJWTAuthProvider(Vertx vertx, MeshOptions meshOptions, BCryptPasswordE
* @param resultHandler
*/
public void authenticateJWT(JsonObject authInfo, Handler<AsyncResult<AuthenticationResult>> resultHandler) {
initialize();

// Decode and validate the JWT. A JWTUser will be returned which contains the decoded token.
// We will use this information to load the Mesh User from the graph.
jwtProvider.authenticate(authInfo, rh -> {
Expand All @@ -100,20 +116,21 @@ public void authenticateJWT(JsonObject authInfo, Handler<AsyncResult<Authenticat
log.debug("Could not authenticate token.", rh.cause());
}
resultHandler.handle(Future.failedFuture("Invalid Token"));
} else {
JsonObject decodedJwt = rh.result().principal();
try {
User user = loadUserByJWT(decodedJwt);
AuthenticationResult result = new AuthenticationResult(user);

// Check whether an api key was used to authenticate the user.
if (decodedJwt.containsKey(API_KEY_TOKEN_CODE_FIELD_NAME)) {
result.setUsingAPIKey(true);
}
resultHandler.handle(Future.succeededFuture(result));
} catch (Exception e) {
resultHandler.handle(Future.failedFuture(e));
return;
}

JsonObject decodedJwt = rh.result().principal();
try {
User user = loadUserByJWT(decodedJwt);
AuthenticationResult result = new AuthenticationResult(user);

// Check whether an api key was used to authenticate the user.
if (decodedJwt.containsKey(API_KEY_TOKEN_CODE_FIELD_NAME)) {
result.setUsingAPIKey(true);
}
resultHandler.handle(Future.succeededFuture(result));
} catch (Exception e) {
resultHandler.handle(Future.failedFuture(e));
}
});
}
Expand Down Expand Up @@ -143,11 +160,12 @@ public String generateToken(JsonObject jsonObject) {
* Password
*/
public String generateToken(String username, String password, String newPassword) {
initialize();

HibUser user = authenticate(username, password, newPassword);
String uuid = db.tx(user::getUuid);
JsonObject tokenData = new JsonObject().put(USERID_FIELD_NAME, uuid);
return jwtProvider.generateToken(tokenData, new JWTOptions()
.setExpiresInSeconds(meshOptions.getAuthenticationOptions().getTokenExpirationTime()));
return jwtProvider.generateToken(tokenData, JWTUtil.createJWTOptions(meshOptions.getAuthenticationOptions()));
}

/**
Expand All @@ -160,41 +178,41 @@ public String generateToken(String username, String password, String newPassword
*/
private HibUser authenticate(String username, String password, String newPassword) {
HibUser user = db.tx(tx -> { return tx.userDao().findByUsername(username); });
if (user != null) {
String accountPasswordHash = db.tx(user::getPasswordHash);
// TODO check if user is enabled
boolean hashMatches = false;
if (StringUtils.isEmpty(accountPasswordHash) && password != null) {
if (log.isDebugEnabled()) {
log.debug("The account password hash or token password string are invalid.");
}
throw error(UNAUTHORIZED, "auth_login_failed");
} else {
if (log.isDebugEnabled()) {
log.debug("Validating password using the bcrypt password encoder");
}
hashMatches = passwordEncoder.matches(password, accountPasswordHash);
if (user == null) {
if (log.isDebugEnabled()) {
log.debug("Could not load user with username {" + username + "}.");
}
if (hashMatches) {
boolean forcedPasswordChange = db.tx(user::isForcedPasswordChange);
if (forcedPasswordChange && newPassword == null) {
throw error(BAD_REQUEST, "auth_login_password_change_required");
} else if (!forcedPasswordChange && newPassword != null) {
throw error(BAD_REQUEST, "auth_login_newpassword_failed");
} else {
if (forcedPasswordChange) {
db.tx(tx -> { return tx.userDao().setPassword(user, newPassword); });
}
return user;
}
} else {
throw error(UNAUTHORIZED, "auth_login_failed");
// TODO Don't let the user know that we know that he did not exist?
throw error(UNAUTHORIZED, "auth_login_failed");
}

String accountPasswordHash = db.tx(user::getPasswordHash);
// TODO check if user is enabled
boolean hashMatches = false;
if (StringUtils.isEmpty(accountPasswordHash) && password != null) {
if (log.isDebugEnabled()) {
log.debug("The account password hash or token password string are invalid.");
}
throw error(UNAUTHORIZED, "auth_login_failed");
} else {
if (log.isDebugEnabled()) {
log.debug("Could not load user with username {" + username + "}.");
log.debug("Validating password using the bcrypt password encoder");
}
// TODO Don't let the user know that we know that he did not exist?
hashMatches = passwordEncoder.matches(password, accountPasswordHash);
}
if (hashMatches) {
boolean forcedPasswordChange = db.tx(user::isForcedPasswordChange);
if (forcedPasswordChange && newPassword == null) {
throw error(BAD_REQUEST, "auth_login_password_change_required");
} else if (!forcedPasswordChange && newPassword != null) {
throw error(BAD_REQUEST, "auth_login_newpassword_failed");
} else {
if (forcedPasswordChange) {
db.tx(tx -> { return tx.userDao().setPassword(user, newPassword); });
}
return user;
}
} else {
throw error(UNAUTHORIZED, "auth_login_failed");
}
}
Expand All @@ -207,18 +225,19 @@ private HibUser authenticate(String username, String password, String newPasswor
* @return The new token
*/
public String generateToken(User user) {
if (user instanceof MeshAuthUser) {
AuthenticationOptions options = meshOptions.getAuthenticationOptions();
JsonObject tokenData = new JsonObject();
String uuid = db.tx(((MeshAuthUser) user).getDelegate()::getUuid);
tokenData.put(USERID_FIELD_NAME, uuid);
JWTOptions jwtOptions = new JWTOptions().setAlgorithm(options.getAlgorithm())
.setExpiresInSeconds(options.getTokenExpirationTime());
return jwtProvider.generateToken(tokenData, jwtOptions);
} else {
initialize();

if (!(user instanceof MeshAuthUser)) {
log.error("Can't generate token for user of type {" + user.getClass().getName() + "}");
throw error(INTERNAL_SERVER_ERROR, "error_internal");
}

AuthenticationOptions options = meshOptions.getAuthenticationOptions();
JsonObject tokenData = new JsonObject();
String uuid = db.tx(((MeshAuthUser) user).getDelegate()::getUuid);
tokenData.put(USERID_FIELD_NAME, uuid);
JWTOptions jwtOptions = JWTUtil.createJWTOptions(options);
return jwtProvider.generateToken(tokenData, jwtOptions);
}

/**
Expand All @@ -231,11 +250,13 @@ public String generateToken(User user) {
* @return Generated API key
*/
public String generateAPIToken(HibUser user, String tokenCode, Integer expireDuration) {
initialize();

AuthenticationOptions options = meshOptions.getAuthenticationOptions();
JsonObject tokenData = new JsonObject()
.put(USERID_FIELD_NAME, user.getUuid())
.put(API_KEY_TOKEN_CODE_FIELD_NAME, tokenCode);
JWTOptions jwtOptions = new JWTOptions().setAlgorithm(options.getAlgorithm());
JWTOptions jwtOptions = JWTUtil.createJWTOptions(options);
if (expireDuration != null) {
jwtOptions.setExpiresInMinutes(expireDuration);
}
Expand Down Expand Up @@ -268,7 +289,7 @@ private User loadUserByJWT(JsonObject jwt) throws Exception {
// }

// Check whether the token might be an API key token
if (!jwt.containsKey("exp")) {
if (!jwt.containsKey(JWTUtil.JWT_FIELD_EXPIRATION)) {
String apiKeyToken = jwt.getString(API_KEY_TOKEN_CODE_FIELD_NAME);
// TODO: All tokens without exp must have a token code - See https://github.com/gentics/mesh/issues/412
if (apiKeyToken != null) {
Expand Down
Loading