Skip to content

Commit

Permalink
Merge pull request #427 from vert-x3/issues/405
Browse files Browse the repository at this point in the history
normalize the attributes layout and extract the userInfo specific att…
  • Loading branch information
pmlopes authored Oct 12, 2020
2 parents 43753a6 + 1d4ea37 commit 722c05d
Show file tree
Hide file tree
Showing 13 changed files with 275 additions and 128 deletions.
41 changes: 41 additions & 0 deletions vertx-auth-common/src/main/java/io/vertx/ext/auth/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,51 @@
@VertxGen
public interface User {

/**
* Factory for user instances that are single string. The credentials will be added to the principal
* of this instance. As nothing can be said about the credentials no validation will be done.
*
* Will create a principal with a property {@code "username"} with the name as value.
*
* @param username the value for this user
* @return user instance
*/
static User fromName(String username) {
return create(new JsonObject().put("username", username));
}

/**
* Factory for user instances that are single string. The credentials will be added to the principal
* of this instance. As nothing can be said about the credentials no validation will be done.
*
* Will create a principal with a property {@code "access_token"} with the name as value.
*
* @param token the value for this user
* @return user instance
*/
static User fromToken(String token) {
return create(new JsonObject().put("access_token", token));
}

/**
* Factory for user instances that are free form. The credentials will be added to the principal
* of this instance. As nothing can be said about the credentials no validation will be done.
*
* @param principal the free form json principal
* @return user instance
*/
static User create(JsonObject principal) {
return create(principal, new JsonObject());
}

/**
* Factory for user instances that are free form. The credentials will be added to the principal
* of this instance. As nothing can be said about the credentials no validation will be done.
*
* @param principal the free form json principal
* @param attributes the free form json attributes that further describe the principal
* @return user instance
*/
static User create(JsonObject principal, JsonObject attributes) {
return new UserImpl(principal, attributes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,51 +275,6 @@ public JsonObject decode(final String token) {
return payload;
}

public boolean isExpired(JsonObject jwt, JWTOptions options) {

if (jwt == null) {
return false;
}

// All dates in JWT are of type NumericDate
// a NumericDate is: numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until
// the specified UTC date/time, ignoring leap seconds
final long now = (System.currentTimeMillis() / 1000);

if (jwt.containsKey("exp") && !options.isIgnoreExpiration()) {
if (now - options.getLeeway() >= jwt.getLong("exp")) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Expired JWT token: exp[%d] <= (now[%d] - leeway[%d])", jwt.getLong("exp"), now, options.getLeeway()));
}
return true;
}
}

if (jwt.containsKey("iat")) {
Long iat = jwt.getLong("iat");
// issue at must be in the past
if (iat > now + options.getLeeway()) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Invalid JWT token: iat[%d] > now[%d] + leeway[%d]", iat, now, options.getLeeway()));
}
return true;
}
}

if (jwt.containsKey("nbf")) {
Long nbf = jwt.getLong("nbf");
// not before must be after now
if (nbf > now + options.getLeeway()) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Invalid JWT token: nbf[%d] > now[%d] + leeway[%d]", nbf, now, options.getLeeway()));
}
return true;
}
}

return false;
}

/**
* Scope claim are used to grant access to a specific resource.
* They are included into the JWT when the user consent access to the resource,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public void testMatch1(TestContext should) {

final HttpServer server = rule.vertx().createHttpServer();
server.requestHandler(request -> {
User user = User.create(new JsonObject().put("username", "dummy user"));
User user = User.fromName("dummy user");
user.authorizations().add("providerId", PermissionBasedAuthorization.create("p1").setResource("r1"));
AuthorizationContext context = new AuthorizationContextImpl(user, request.params());
should.assertEquals(true, PermissionBasedAuthorization.create("p1").setResource("{variable1}").match(context));
Expand All @@ -103,7 +103,7 @@ public void testMatch2(TestContext should) {

final HttpServer server = rule.vertx().createHttpServer();
server.requestHandler(request -> {
User user = User.create(new JsonObject().put("username", "dummy user"));
User user = User.fromName("dummy user");
user.authorizations().add("providerId", PermissionBasedAuthorization.create("p1").setResource("r1"));
AuthorizationContext context = new AuthorizationContextImpl(user, request.params());
should.assertEquals(false, PermissionBasedAuthorization.create("p1").setResource("{variable1}").match(context));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public void testMatch1(TestContext should) {

final HttpServer server = rule.vertx().createHttpServer();
server.requestHandler(request -> {
User user = User.create(new JsonObject().put("username", "dummy user"));
User user = User.fromName("dummy user");
user.authorizations().add("providerId", RoleBasedAuthorization.create("p1").setResource("r1"));
AuthorizationContext context = new AuthorizationContextImpl(user, request.params());
should.assertTrue(RoleBasedAuthorization.create("p1").setResource("{variable1}").match(context));
Expand All @@ -95,7 +95,7 @@ public void testMatch2(TestContext should) {

final HttpServer server = rule.vertx().createHttpServer();
server.requestHandler(request -> {
User user = User.create(new JsonObject().put("username", "dummy user"));
User user = User.fromName("dummy user");
user.authorizations().add("providerId", RoleBasedAuthorization.create("p1").setResource("r1"));
AuthorizationContext context = new AuthorizationContextImpl(user, request.params());
should.assertFalse(RoleBasedAuthorization.create("p1").setResource("{variable1}").match(context));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static <T> void testJsonCodec(T authorization, Function<T, JsonObject> to
}

public static AuthorizationContext getTestAuthorizationContext() {
return getTestAuthorizationContext(User.create(new JsonObject().put("username", "dummy user")));
return getTestAuthorizationContext(User.fromName("dummy user"));
}

public static AuthorizationContext getTestAuthorizationContext(User user) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public void testMatch1(TestContext should) {
final Async test = should.async();
final HttpServer server = rule.vertx().createHttpServer();
server.requestHandler(request -> {
User user = User.create(new JsonObject().put("username", "dummy user"));
User user = User.fromName("dummy user");
user.authorizations().add("providerId", WildcardPermissionBasedAuthorization.create("p1").setResource("r1"));
AuthorizationContext context = new AuthorizationContextImpl(user, request.params());
should.assertTrue(WildcardPermissionBasedAuthorization.create("p1").setResource("{variable1}").match(context));
Expand All @@ -126,7 +126,7 @@ public void testMatch2(TestContext should) {
final Async test = should.async();
final HttpServer server = rule.vertx().createHttpServer();
server.requestHandler(request -> {
User user = User.create(new JsonObject().put("username", "dummy user"));
User user = User.fromName("dummy user");
user.authorizations().add("providerId", WildcardPermissionBasedAuthorization.create("p1").setResource("r1"));
AuthorizationContext context = new AuthorizationContextImpl(user, request.params());
should.assertFalse(WildcardPermissionBasedAuthorization.create("p1").setResource("{variable1}").match(context));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,6 @@
*/
package io.vertx.ext.auth.jwt.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.List;

import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
Expand All @@ -34,17 +24,27 @@
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.JWTOptions;
import io.vertx.ext.auth.KeyStoreOptions;
import io.vertx.ext.auth.PubSecKeyOptions;
import io.vertx.ext.auth.User;
import io.vertx.ext.auth.authentication.Credentials;
import io.vertx.ext.auth.authentication.TokenCredentials;
import io.vertx.ext.auth.authorization.PermissionBasedAuthorization;
import io.vertx.ext.auth.PubSecKeyOptions;
import io.vertx.ext.auth.User;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.auth.impl.jose.JWK;
import io.vertx.ext.auth.impl.jose.JWT;
import io.vertx.ext.auth.JWTOptions;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.List;

/**
* @author Paulo Lopes
Expand Down Expand Up @@ -120,11 +120,6 @@ public void authenticate(Credentials credentials, Handler<AsyncResult<User>> res

final JsonObject payload = jwt.decode(authInfo.getToken());

if (jwt.isExpired(payload, jwtOptions)) {
resultHandler.handle(Future.failedFuture("Expired JWT token."));
return;
}

if (jwtOptions.getAudience() != null) {
JsonArray target;
if (payload.getValue("aud") instanceof String) {
Expand All @@ -134,7 +129,7 @@ public void authenticate(Credentials credentials, Handler<AsyncResult<User>> res
}

if (Collections.disjoint(jwtOptions.getAudience(), target.getList())) {
resultHandler.handle(Future.failedFuture("Invalid JWT audient. expected: " + Json.encode(jwtOptions.getAudience())));
resultHandler.handle(Future.failedFuture("Invalid JWT audience. expected: " + Json.encode(jwtOptions.getAudience())));
return;
}
}
Expand All @@ -146,12 +141,21 @@ public void authenticate(Credentials credentials, Handler<AsyncResult<User>> res
}
}

if(!jwt.isScopeGranted(payload, jwtOptions)) {
if (!jwt.isScopeGranted(payload, jwtOptions)) {
resultHandler.handle(Future.failedFuture("Invalid JWT token: missing required scopes."));
return;
}

resultHandler.handle(Future.succeededFuture(createUser(authInfo.getToken(), payload, permissionsClaimKey)));
final User user = createUser(authInfo.getToken(), payload, permissionsClaimKey);

if (user.expired(jwtOptions.getLeeway())) {
if (!jwtOptions.isIgnoreExpiration()) {
resultHandler.handle(Future.failedFuture("Invalid JWT token: missing required scopes."));
return;
}
}

resultHandler.handle(Future.succeededFuture(user));

} catch (RuntimeException e) {
resultHandler.handle(Future.failedFuture(e));
Expand Down Expand Up @@ -179,25 +183,14 @@ private static JsonArray getJsonPermissions(JsonObject jwtToken, String permissi

@Deprecated
private User createUser(String accessToken, JsonObject jwtToken, String permissionsClaimKey) {
User result = User.create(new JsonObject().put("access_token", accessToken));
User result = User.fromToken(accessToken);

// update the attributes
result.attributes()
.put("accessToken", jwtToken);

try {
// re-compute expires at if not present and access token has been successfully decoded from JWT
if (!result.attributes().containsKey("exp")) {
Long exp = jwtToken.getLong("exp");

if (exp != null) {
result.attributes()
.put("exp", exp);
}
}
} catch (ClassCastException e) {
// ignore
}
// copy the expiration check properties to the root + sub
copyProperties(jwtToken, result.attributes(), "exp", "iat", "nbf", "sub");

// root claim meta data for JWT AuthZ
result.attributes()
Expand All @@ -215,6 +208,16 @@ private User createUser(String accessToken, JsonObject jwtToken, String permissi
return result;
}

private static void copyProperties(JsonObject source, JsonObject target, String... keys) {
if (source != null && target != null) {
for (String key : keys) {
if (source.containsKey(key) && !target.containsKey(key)) {
target.put(key, source.getValue(key));
}
}
}
}

private static JsonArray getNestedJsonValue(JsonObject jwtToken, String permissionsClaimKey) {
String[] keys = permissionsClaimKey.split("/");
JsonObject obj = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public void authenticate(Credentials credentials, Handler<AsyncResult<io.vertx.e
String ldapPrincipal = getLdapPrincipal(authInfo.getUsername());
createLdapContext(ldapPrincipal, authInfo.getPassword(), contextResponse -> {
if (contextResponse.succeeded()) {
User user = User.create(new JsonObject().put("username", authInfo.getUsername()));
User user = User.fromName(authInfo.getUsername());
resultHandler.handle(Future.succeededFuture(user));
} else {
resultHandler.handle(Future.failedFuture(contextResponse.cause()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
import io.vertx.core.impl.logging.Logger;
import io.vertx.core.impl.logging.LoggerFactory;
import io.vertx.ext.auth.impl.http.SimpleHttpClient;
import io.vertx.ext.auth.impl.jose.JWT;
import io.vertx.ext.auth.oauth2.OAuth2FlowType;
import io.vertx.ext.auth.oauth2.OAuth2Options;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -390,7 +392,7 @@ public void tokenRevocation(String tokenType, String token, Handler<AsyncResult<
*
* see: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
*/
public void userInfo(String accessToken, Handler<AsyncResult<JsonObject>> handler) {
public void userInfo(String accessToken, JWT jwt, Handler<AsyncResult<JsonObject>> handler) {
final JsonObject headers = new JsonObject();
final JsonObject extraParams = config.getUserInfoParameters();
String path = config.getUserInfoPath();
Expand All @@ -406,7 +408,7 @@ public void userInfo(String accessToken, Handler<AsyncResult<JsonObject>> handle

headers.put("Authorization", "Bearer " + accessToken);
// specify preferred accepted accessToken type
headers.put("Accept", "application/json,application/x-www-form-urlencoded;q=0.9");
headers.put("Accept", "application/json,application/jwt,application/x-www-form-urlencoded;q=0.9");

fetch(
HttpMethod.GET,
Expand All @@ -431,6 +433,14 @@ public void userInfo(String accessToken, Handler<AsyncResult<JsonObject>> handle
handler.handle(Future.failedFuture(e));
return;
}
} else if (reply.is("application/jwt")) {
try {
// userInfo is expected to be a JWT
userInfo = jwt.decode(reply.body().toString(StandardCharsets.UTF_8));
} catch (RuntimeException e) {
handler.handle(Future.failedFuture(e));
return;
}
} else if (reply.is("application/x-www-form-urlencoded") || reply.is("text/plain")) {
try {
// attempt to convert url encoded string to json
Expand Down
Loading

0 comments on commit 722c05d

Please sign in to comment.