From be4f0dfaad80ba91997016a01ed91fe980b5b76a Mon Sep 17 00:00:00 2001 From: Paulo Lopes Date: Fri, 9 Oct 2020 11:07:33 +0200 Subject: [PATCH 1/2] normalize the attributes layout and extract the userInfo specific attributes from the json to the user object Signed-off-by: Paulo Lopes --- .../src/main/java/io/vertx/ext/auth/User.java | 33 +++++ .../java/io/vertx/ext/auth/impl/jose/JWT.java | 45 ------- .../PermissionBasedAuthorizationTest.java | 4 +- .../ext/auth/RoleBasedAuthorizationTest.java | 4 +- .../java/io/vertx/ext/auth/TestUtils.java | 2 +- ...dcardPermissionBasedAuthorizationTest.java | 4 +- .../auth/jwt/impl/JWTAuthProviderImpl.java | 77 +++++------ .../ldap/impl/LdapAuthenticationImpl.java | 2 +- .../vertx/ext/auth/oauth2/impl/OAuth2API.java | 14 +- .../oauth2/impl/OAuth2AuthProviderImpl.java | 83 +++++++----- .../oauth2/OAuth2UserInfoAzureJWTTest.java | 123 ++++++++++++++++++ .../impl/PropertyFileAuthenticationImpl.java | 2 +- .../sqlclient/impl/SqlAuthenticationImpl.java | 2 +- 13 files changed, 267 insertions(+), 128 deletions(-) create mode 100644 vertx-auth-oauth2/src/test/java/io/vertx/ext/auth/test/oauth2/OAuth2UserInfoAzureJWTTest.java diff --git a/vertx-auth-common/src/main/java/io/vertx/ext/auth/User.java b/vertx-auth-common/src/main/java/io/vertx/ext/auth/User.java index f2e0158c7..bf33c1c68 100644 --- a/vertx-auth-common/src/main/java/io/vertx/ext/auth/User.java +++ b/vertx-auth-common/src/main/java/io/vertx/ext/auth/User.java @@ -40,10 +40,43 @@ @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. + * + * The kind will be used a the property key name on the principal to store the value. For example: + * + * {@code create("access_token", jwt)} + * + * Will create a principal with a property {@code "access_token"} with the jwt as value. + * + * @param kind the credential kind e.g.: {@code access_token} or {@code username} + * @param value the value for this token + * @return user instance + */ + static User create(String kind, String value) { + return create(new JsonObject().put(kind, value)); + } + + /** + * 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); } diff --git a/vertx-auth-common/src/main/java/io/vertx/ext/auth/impl/jose/JWT.java b/vertx-auth-common/src/main/java/io/vertx/ext/auth/impl/jose/JWT.java index 9c5d9befc..04f8016cf 100644 --- a/vertx-auth-common/src/main/java/io/vertx/ext/auth/impl/jose/JWT.java +++ b/vertx-auth-common/src/main/java/io/vertx/ext/auth/impl/jose/JWT.java @@ -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, diff --git a/vertx-auth-common/src/test/java/io/vertx/ext/auth/PermissionBasedAuthorizationTest.java b/vertx-auth-common/src/test/java/io/vertx/ext/auth/PermissionBasedAuthorizationTest.java index 00622a171..c88cd8f71 100644 --- a/vertx-auth-common/src/test/java/io/vertx/ext/auth/PermissionBasedAuthorizationTest.java +++ b/vertx-auth-common/src/test/java/io/vertx/ext/auth/PermissionBasedAuthorizationTest.java @@ -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.create("username", "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)); @@ -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.create("username", "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)); diff --git a/vertx-auth-common/src/test/java/io/vertx/ext/auth/RoleBasedAuthorizationTest.java b/vertx-auth-common/src/test/java/io/vertx/ext/auth/RoleBasedAuthorizationTest.java index 0fca01b03..3a6f13d46 100644 --- a/vertx-auth-common/src/test/java/io/vertx/ext/auth/RoleBasedAuthorizationTest.java +++ b/vertx-auth-common/src/test/java/io/vertx/ext/auth/RoleBasedAuthorizationTest.java @@ -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.create("username", "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)); @@ -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.create("username", "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)); diff --git a/vertx-auth-common/src/test/java/io/vertx/ext/auth/TestUtils.java b/vertx-auth-common/src/test/java/io/vertx/ext/auth/TestUtils.java index 9e8409cc2..7d60ebdb4 100644 --- a/vertx-auth-common/src/test/java/io/vertx/ext/auth/TestUtils.java +++ b/vertx-auth-common/src/test/java/io/vertx/ext/auth/TestUtils.java @@ -30,7 +30,7 @@ public static void testJsonCodec(T authorization, Function to } public static AuthorizationContext getTestAuthorizationContext() { - return getTestAuthorizationContext(User.create(new JsonObject().put("username", "dummy user"))); + return getTestAuthorizationContext(User.create("username", "dummy user")); } public static AuthorizationContext getTestAuthorizationContext(User user) { diff --git a/vertx-auth-common/src/test/java/io/vertx/ext/auth/WildcardPermissionBasedAuthorizationTest.java b/vertx-auth-common/src/test/java/io/vertx/ext/auth/WildcardPermissionBasedAuthorizationTest.java index 823a1f7f2..795f29acb 100644 --- a/vertx-auth-common/src/test/java/io/vertx/ext/auth/WildcardPermissionBasedAuthorizationTest.java +++ b/vertx-auth-common/src/test/java/io/vertx/ext/auth/WildcardPermissionBasedAuthorizationTest.java @@ -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.create("username", "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)); @@ -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.create("username", "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)); diff --git a/vertx-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/impl/JWTAuthProviderImpl.java b/vertx-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/impl/JWTAuthProviderImpl.java index 966d5c07b..cd954d159 100644 --- a/vertx-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/impl/JWTAuthProviderImpl.java +++ b/vertx-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/impl/JWTAuthProviderImpl.java @@ -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; @@ -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 @@ -120,11 +120,6 @@ public void authenticate(Credentials credentials, Handler> 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) { @@ -134,7 +129,7 @@ public void authenticate(Credentials credentials, Handler> 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; } } @@ -146,12 +141,21 @@ public void authenticate(Credentials credentials, Handler> 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)); @@ -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.create("access_token", 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() @@ -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; diff --git a/vertx-auth-ldap/src/main/java/io/vertx/ext/auth/ldap/impl/LdapAuthenticationImpl.java b/vertx-auth-ldap/src/main/java/io/vertx/ext/auth/ldap/impl/LdapAuthenticationImpl.java index 5bd7c62e6..9239e77a5 100644 --- a/vertx-auth-ldap/src/main/java/io/vertx/ext/auth/ldap/impl/LdapAuthenticationImpl.java +++ b/vertx-auth-ldap/src/main/java/io/vertx/ext/auth/ldap/impl/LdapAuthenticationImpl.java @@ -61,7 +61,7 @@ public void authenticate(Credentials credentials, Handler { if (contextResponse.succeeded()) { - User user = User.create(new JsonObject().put("username", authInfo.getUsername())); + User user = User.create("username", authInfo.getUsername()); resultHandler.handle(Future.succeededFuture(user)); } else { resultHandler.handle(Future.failedFuture(contextResponse.cause())); diff --git a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/impl/OAuth2API.java b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/impl/OAuth2API.java index c984472be..1279d2c7e 100644 --- a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/impl/OAuth2API.java +++ b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/impl/OAuth2API.java @@ -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; @@ -390,7 +392,7 @@ public void tokenRevocation(String tokenType, String token, Handler> handler) { + public void userInfo(String accessToken, JWT jwt, Handler> handler) { final JsonObject headers = new JsonObject(); final JsonObject extraParams = config.getUserInfoParameters(); String path = config.getUserInfoPath(); @@ -406,7 +408,7 @@ public void userInfo(String accessToken, Handler> 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, @@ -431,6 +433,14 @@ public void userInfo(String accessToken, Handler> 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 diff --git a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/impl/OAuth2AuthProviderImpl.java b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/impl/OAuth2AuthProviderImpl.java index 65f9ca589..413c83957 100644 --- a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/impl/OAuth2AuthProviderImpl.java +++ b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/impl/OAuth2AuthProviderImpl.java @@ -21,21 +21,21 @@ import io.vertx.core.Vertx; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.DecodeException; import io.vertx.core.json.Json; import io.vertx.core.json.JsonArray; -import io.vertx.core.json.DecodeException; import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.JWTOptions; +import io.vertx.ext.auth.NoSuchKeyIdException; import io.vertx.ext.auth.PubSecKeyOptions; import io.vertx.ext.auth.User; import io.vertx.ext.auth.authentication.CredentialValidationException; import io.vertx.ext.auth.authentication.Credentials; import io.vertx.ext.auth.authentication.TokenCredentials; import io.vertx.ext.auth.authentication.UsernamePasswordCredentials; -import io.vertx.ext.auth.oauth2.*; 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.NoSuchKeyIdException; +import io.vertx.ext.auth.oauth2.*; import java.util.Collections; @@ -50,7 +50,9 @@ public class OAuth2AuthProviderImpl implements OAuth2Auth { private final OAuth2Options config; private final OAuth2API api; - private JWT jwt = new JWT(); + // avoid caching, as it may swap, + // old references are still valid though + private volatile JWT jwt = new JWT(); private long updateTimerId = -1; private Handler missingKeyHandler; @@ -344,7 +346,28 @@ public OAuth2Auth revoke(User user, String tokenType, Handler> @Override public OAuth2Auth userInfo(User user, Handler> handler) { - api.userInfo(user.principal().getString("access_token"), handler); + api.userInfo(user.principal().getString("access_token"), jwt, userInfo -> { + if (userInfo.succeeded()) { + JsonObject json = userInfo.result(); + // validation (the subject must match) + String userSub = user.principal().getString("sub", user.attributes().getString("sub")); + String userInfoSub = json.getString("sub"); + if (userSub != null || userInfoSub != null) { + if (userSub != null) { + if (userInfoSub != null) { + if (!userSub.equals(userInfoSub)) { + handler.handle(Future.failedFuture("Used 'sub' does not match UserInfo 'sub'.")); + return; + } + } + } + } + // copy basic properties to the attributes + copyProperties(json, user.attributes(), true, "sub", "name", "email", "picture"); + } + // complete + handler.handle(userInfo); + }); return this; } @@ -353,10 +376,6 @@ public String endSessionURL(User user, JsonObject params) { return api.endSessionURL(user.principal().getString("id_token"), params); } - OAuth2API api() { - return api; - } - /** * Create a User object with some initial validations related to JWT. */ @@ -391,16 +410,9 @@ private User createUser(JsonObject json, boolean skipMissingKeyNotify) { user.attributes() .put("accessToken", jwt.decode(json.getString("access_token"))); - // re-compute expires at if not present and access token has been successfully decoded from JWT - if (!user.attributes().containsKey("exp")) { - Long exp = user.attributes() - .getJsonObject("accessToken").getLong("exp"); - - if (exp != null) { - user.attributes() - .put("exp", exp); - } - } + // copy the expiration check properties to the root + // + sub + copyProperties(user.attributes().getJsonObject("accessToken"), user.attributes(), true, "exp", "iat", "nbf", "sub"); // root claim meta data for JWT AuthZ user.attributes() @@ -437,17 +449,8 @@ private User createUser(JsonObject json, boolean skipMissingKeyNotify) { try { user.attributes() .put("idToken", jwt.decode(json.getString("id_token"))); - - // re-compute expires at if not present and id token has been successfully decoded from JWT - if (!user.attributes().containsKey("exp")) { - Long exp = user.attributes() - .getJsonObject("idToken").getLong("exp"); - - if (exp != null) { - user.attributes() - .put("exp", exp); - } - } + // copy the userInfo basic properties to the root + copyProperties(user.attributes().getJsonObject("idToken"), user.attributes(), false,"sub", "name", "email", "picture"); } catch (NoSuchKeyIdException e) { if (!skipMissingKeyNotify) { // we haven't notified this id yet @@ -467,7 +470,7 @@ private User createUser(JsonObject json, boolean skipMissingKeyNotify) { } } } catch (DecodeException | IllegalStateException e) { - // explicity catch and log. The exception here is a valid case + // explicitly catch and log. The exception here is a valid case // the reason is that it can be for several factors, such as bad token // or invalid JWT key setup, in that case we fall back to opaque token // which is the default operational mode for OAuth2. @@ -610,7 +613,7 @@ private AccessToken createAccessToken(JsonObject json) { LOG.trace("Cannot decode access token:", e); } } catch (DecodeException | IllegalStateException e) { - // explicity catch and log as trace. exception here is a valid case + // explicitly catch and log as trace. exception here is a valid case // the reason is that it can be for several factors, such as bad token // or invalid JWT key setup, in that case we fall back to opaque token // which is the default operational mode for OAuth2. @@ -632,7 +635,7 @@ private AccessToken createAccessToken(JsonObject json) { LOG.trace("Cannot decode access token:", e); } } catch (DecodeException | IllegalStateException e) { - // explicity catch and log as trace. exception here is a valid case + // explicitly catch and log as trace. exception here is a valid case // the reason is that it can be for several factors, such as bad token // or invalid JWT key setup, in that case we fall back to opaque token // which is the default operational mode for OAuth2. @@ -642,4 +645,16 @@ private AccessToken createAccessToken(JsonObject json) { return user; } + + private static void copyProperties(JsonObject source, JsonObject target, boolean overwrite, String... keys) { + if (source != null && target != null) { + for (String key : keys) { + if (source.containsKey(key)) { + if (!target.containsKey(key) || overwrite) { + target.put(key, source.getValue(key)); + } + } + } + } + } } diff --git a/vertx-auth-oauth2/src/test/java/io/vertx/ext/auth/test/oauth2/OAuth2UserInfoAzureJWTTest.java b/vertx-auth-oauth2/src/test/java/io/vertx/ext/auth/test/oauth2/OAuth2UserInfoAzureJWTTest.java new file mode 100644 index 000000000..fc752310a --- /dev/null +++ b/vertx-auth-oauth2/src/test/java/io/vertx/ext/auth/test/oauth2/OAuth2UserInfoAzureJWTTest.java @@ -0,0 +1,123 @@ +package io.vertx.ext.auth.test.oauth2; + +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServer; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; +import io.vertx.ext.auth.impl.http.SimpleHttpClient; +import io.vertx.ext.auth.oauth2.OAuth2Auth; +import io.vertx.ext.auth.oauth2.OAuth2FlowType; +import io.vertx.ext.auth.oauth2.OAuth2Options; +import io.vertx.test.core.VertxTestBase; +import org.junit.Test; + +import java.io.UnsupportedEncodingException; +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.assertNotEquals; + +public class OAuth2UserInfoAzureJWTTest extends VertxTestBase { + + private static final String fixtureV1 = "eyJhbGciOiJub25lIn0.eyJhdWQiOiJiMTRhNzUwNS05NmU5LTQ5MjctOTFlOC0wNjAxZDBmYzljYWEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9mYTE1ZDY5Mi1lOWM3LTQ0NjAtYTc0My0yOWYyOTU2ZmQ0MjkvIiwiaWF0IjoxNTM2Mjc1MTI0LCJuYmYiOjE1MzYyNzUxMjQsImV4cCI6MTUzNjI3OTAyNCwiYWlvIjoiQVhRQWkvOElBQUFBcXhzdUIrUjREMnJGUXFPRVRPNFlkWGJMRDlrWjh4ZlhhZGVBTTBRMk5rTlQ1aXpmZzN1d2JXU1hodVNTajZVVDVoeTJENldxQXBCNWpLQTZaZ1o5ay9TVTI3dVY5Y2V0WGZMT3RwTnR0Z2s1RGNCdGsrTExzdHovSmcrZ1lSbXY5YlVVNFhscGhUYzZDODZKbWoxRkN3PT0iLCJhbXIiOlsicnNhIl0sImVtYWlsIjoiYWJlbGlAbWljcm9zb2Z0LmNvbSIsImZhbWlseV9uYW1lIjoiTGluY29sbiIsImdpdmVuX25hbWUiOiJBYmUiLCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaXBhZGRyIjoiMTMxLjEwNy4yMjIuMjIiLCJuYW1lIjoiYWJlbGkiLCJub25jZSI6IjEyMzUyMyIsIm9pZCI6IjA1ODMzYjZiLWFhMWQtNDJkNC05ZWMwLTFiMmJiOTE5NDQzOCIsInJoIjoiSSIsInN1YiI6IjVfSjlyU3NzOC1qdnRfSWN1NnVlUk5MOHhYYjhMRjRGc2dfS29vQzJSSlEiLCJ0aWQiOiJmYTE1ZDY5Mi1lOWM3LTQ0NjAtYTc0My0yOWYyOTU2ZmQ0MjkiLCJ1bmlxdWVfbmFtZSI6IkFiZUxpQG1pY3Jvc29mdC5jb20iLCJ1dGkiOiJMeGVfNDZHcVRrT3BHU2ZUbG40RUFBIiwidmVyIjoiMS4wIn0="; + private static final String fixtureV2 = "eyJhbGciOiJub25lIn0.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTEyMjA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsInRpZCI6IjkxMjIwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0"; + + private String fixture; + + private static final JsonObject azureParams = new JsonObject() + .put("alt", "json"); + + private OAuth2Auth oauth2; + private HttpServer server; + + private final OAuth2Options oauthConfig = new OAuth2Options() + .setFlow(OAuth2FlowType.AUTH_CODE) + .setClientID("client-id") + .setClientSecret("client-secret") + .setSite("http://localhost:8080") + .setUserInfoPath("/oauth/userinfo") + .setUserInfoParameters(azureParams); + + @Override + public void setUp() throws Exception { + super.setUp(); + oauth2 = OAuth2Auth.create(vertx, oauthConfig); + + final CountDownLatch latch = new CountDownLatch(1); + + server = vertx.createHttpServer().requestHandler(req -> { + if (req.method() == HttpMethod.GET && "/oauth/userinfo".equals(req.path())) { + assertTrue(req.getHeader("Authorization").contains("Bearer ")); + + try { + assertEquals(azureParams, SimpleHttpClient.queryToJson(Buffer.buffer(req.query()))); + } catch (UnsupportedEncodingException e) { + fail(e); + } + + req.response().putHeader("Content-Type", "application/jwt").end(fixture); + } else { + req.response().setStatusCode(400).end(); + } + }).listen(8080, ready -> { + if (ready.failed()) { + throw new RuntimeException(ready.cause()); + } + // ready + latch.countDown(); + }); + + latch.await(); + } + + @Override + public void tearDown() throws Exception { + server.close(); + super.tearDown(); + } + + @Test + public void getUserInfoV1() { + final User user = User.create(new JsonObject("{\"access_token\":\"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJhdXRob3JpemF0aW9uIjp7InBlcm1pc3Npb25zIjpbeyJyZXNvdXJjZV9zZXRfaWQiOiJkMmZlOTg0My02NDYyLTRiZmMtYmFiYS1iNTc4N2JiNmUwZTciLCJyZXNvdXJjZV9zZXRfbmFtZSI6IkhlbGxvIFdvcmxkIFJlc291cmNlIn1dfSwianRpIjoiZDYxMDlhMDktNzhmZC00OTk4LWJmODktOTU3MzBkZmQwODkyLTE0NjQ5MDY2Nzk0MDUiLCJleHAiOjk5OTk5OTk5OTksIm5iZiI6MCwiaWF0IjoxNDY0OTA2NjcxLCJzdWIiOiJmMTg4OGY0ZC01MTcyLTQzNTktYmUwYy1hZjMzODUwNWQ4NmMiLCJ0eXAiOiJrY19ldHQiLCJhenAiOiJoZWxsby13b3JsZC1hdXRoei1zZXJ2aWNlIn0\",\"active\":true,\"scope\":\"scopeA scopeB\",\"client_id\":\"client-id\",\"username\":\"username\",\"token_type\":\"bearer\",\"expires_at\":99999999999000}")); + + assertNotEquals("abeli", user.attributes().getString("name")); + assertNotEquals("abeli@microsoft.com", user.attributes().getString("email")); + + fixture = fixtureV1; + + oauth2.userInfo(user, userInfo -> { + if (userInfo.failed()) { + fail(userInfo.cause().getMessage()); + } else { + // assert that the user has now a few extra fields on the attributes + assertEquals("abeli", user.attributes().getString("name")); + assertEquals("", user.attributes().getString("picture", "")); + assertEquals("abeli@microsoft.com", user.attributes().getString("email")); + testComplete(); + } + }); + await(); + } + + @Test + public void getUserInfoV2() { + final User user = User.create(new JsonObject("{\"access_token\":\"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJhdXRob3JpemF0aW9uIjp7InBlcm1pc3Npb25zIjpbeyJyZXNvdXJjZV9zZXRfaWQiOiJkMmZlOTg0My02NDYyLTRiZmMtYmFiYS1iNTc4N2JiNmUwZTciLCJyZXNvdXJjZV9zZXRfbmFtZSI6IkhlbGxvIFdvcmxkIFJlc291cmNlIn1dfSwianRpIjoiZDYxMDlhMDktNzhmZC00OTk4LWJmODktOTU3MzBkZmQwODkyLTE0NjQ5MDY2Nzk0MDUiLCJleHAiOjk5OTk5OTk5OTksIm5iZiI6MCwiaWF0IjoxNDY0OTA2NjcxLCJzdWIiOiJmMTg4OGY0ZC01MTcyLTQzNTktYmUwYy1hZjMzODUwNWQ4NmMiLCJ0eXAiOiJrY19ldHQiLCJhenAiOiJoZWxsby13b3JsZC1hdXRoei1zZXJ2aWNlIn0\",\"active\":true,\"scope\":\"scopeA scopeB\",\"client_id\":\"client-id\",\"username\":\"username\",\"token_type\":\"bearer\",\"expires_at\":99999999999000}")); + + assertNotEquals("Abe Lincoln", user.attributes().getString("name")); + + fixture = fixtureV2; + + oauth2.userInfo(user, userInfo -> { + if (userInfo.failed()) { + fail(userInfo.cause().getMessage()); + } else { + // assert that the user has now a few extra fields on the attributes + assertEquals("Abe Lincoln", user.attributes().getString("name")); + assertEquals("", user.attributes().getString("picture", "")); + assertEquals("", user.attributes().getString("email", "")); + testComplete(); + } + }); + await(); + } +} diff --git a/vertx-auth-properties/src/main/java/io/vertx/ext/auth/properties/impl/PropertyFileAuthenticationImpl.java b/vertx-auth-properties/src/main/java/io/vertx/ext/auth/properties/impl/PropertyFileAuthenticationImpl.java index 7ad3c59af..a69f645fd 100644 --- a/vertx-auth-properties/src/main/java/io/vertx/ext/auth/properties/impl/PropertyFileAuthenticationImpl.java +++ b/vertx-auth-properties/src/main/java/io/vertx/ext/auth/properties/impl/PropertyFileAuthenticationImpl.java @@ -178,7 +178,7 @@ public void authenticate(Credentials credentials, Handler> res Row row = rows.iterator().next(); String hashedStoredPwd = row.getString(0); if (strategy.verify(hashedStoredPwd, authInfo.getPassword())) { - resultHandler.handle(Future.succeededFuture(User.create(new JsonObject().put("username", authInfo.getUsername())))); + resultHandler.handle(Future.succeededFuture(User.create("username", authInfo.getUsername()))); } else { resultHandler.handle(Future.failedFuture("Invalid username/password")); } From 1d4ea37fda695ed205b33ee8f2220cd14e1bd80e Mon Sep 17 00:00:00 2001 From: Paulo Lopes Date: Mon, 12 Oct 2020 16:10:20 +0200 Subject: [PATCH 2/2] rename factory Signed-off-by: Paulo Lopes --- .../src/main/java/io/vertx/ext/auth/User.java | 22 +++++++++++++------ .../PermissionBasedAuthorizationTest.java | 4 ++-- .../ext/auth/RoleBasedAuthorizationTest.java | 4 ++-- .../java/io/vertx/ext/auth/TestUtils.java | 2 +- ...dcardPermissionBasedAuthorizationTest.java | 4 ++-- .../auth/jwt/impl/JWTAuthProviderImpl.java | 2 +- .../ldap/impl/LdapAuthenticationImpl.java | 2 +- .../impl/PropertyFileAuthenticationImpl.java | 2 +- .../sqlclient/impl/SqlAuthenticationImpl.java | 2 +- 9 files changed, 26 insertions(+), 18 deletions(-) diff --git a/vertx-auth-common/src/main/java/io/vertx/ext/auth/User.java b/vertx-auth-common/src/main/java/io/vertx/ext/auth/User.java index bf33c1c68..9bfd3f81c 100644 --- a/vertx-auth-common/src/main/java/io/vertx/ext/auth/User.java +++ b/vertx-auth-common/src/main/java/io/vertx/ext/auth/User.java @@ -44,18 +44,26 @@ 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. * - * The kind will be used a the property key name on the principal to store the value. For example: + * Will create a principal with a property {@code "username"} with the name as value. * - * {@code create("access_token", jwt)} + * @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 jwt as value. + * Will create a principal with a property {@code "access_token"} with the name as value. * - * @param kind the credential kind e.g.: {@code access_token} or {@code username} - * @param value the value for this token + * @param token the value for this user * @return user instance */ - static User create(String kind, String value) { - return create(new JsonObject().put(kind, value)); + static User fromToken(String token) { + return create(new JsonObject().put("access_token", token)); } /** diff --git a/vertx-auth-common/src/test/java/io/vertx/ext/auth/PermissionBasedAuthorizationTest.java b/vertx-auth-common/src/test/java/io/vertx/ext/auth/PermissionBasedAuthorizationTest.java index c88cd8f71..06bd78667 100644 --- a/vertx-auth-common/src/test/java/io/vertx/ext/auth/PermissionBasedAuthorizationTest.java +++ b/vertx-auth-common/src/test/java/io/vertx/ext/auth/PermissionBasedAuthorizationTest.java @@ -83,7 +83,7 @@ public void testMatch1(TestContext should) { final HttpServer server = rule.vertx().createHttpServer(); server.requestHandler(request -> { - User user = User.create("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)); @@ -103,7 +103,7 @@ public void testMatch2(TestContext should) { final HttpServer server = rule.vertx().createHttpServer(); server.requestHandler(request -> { - User user = User.create("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)); diff --git a/vertx-auth-common/src/test/java/io/vertx/ext/auth/RoleBasedAuthorizationTest.java b/vertx-auth-common/src/test/java/io/vertx/ext/auth/RoleBasedAuthorizationTest.java index 3a6f13d46..46bae41ae 100644 --- a/vertx-auth-common/src/test/java/io/vertx/ext/auth/RoleBasedAuthorizationTest.java +++ b/vertx-auth-common/src/test/java/io/vertx/ext/auth/RoleBasedAuthorizationTest.java @@ -75,7 +75,7 @@ public void testMatch1(TestContext should) { final HttpServer server = rule.vertx().createHttpServer(); server.requestHandler(request -> { - User user = User.create("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)); @@ -95,7 +95,7 @@ public void testMatch2(TestContext should) { final HttpServer server = rule.vertx().createHttpServer(); server.requestHandler(request -> { - User user = User.create("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)); diff --git a/vertx-auth-common/src/test/java/io/vertx/ext/auth/TestUtils.java b/vertx-auth-common/src/test/java/io/vertx/ext/auth/TestUtils.java index 7d60ebdb4..5a918199b 100644 --- a/vertx-auth-common/src/test/java/io/vertx/ext/auth/TestUtils.java +++ b/vertx-auth-common/src/test/java/io/vertx/ext/auth/TestUtils.java @@ -30,7 +30,7 @@ public static void testJsonCodec(T authorization, Function to } public static AuthorizationContext getTestAuthorizationContext() { - return getTestAuthorizationContext(User.create("username", "dummy user")); + return getTestAuthorizationContext(User.fromName("dummy user")); } public static AuthorizationContext getTestAuthorizationContext(User user) { diff --git a/vertx-auth-common/src/test/java/io/vertx/ext/auth/WildcardPermissionBasedAuthorizationTest.java b/vertx-auth-common/src/test/java/io/vertx/ext/auth/WildcardPermissionBasedAuthorizationTest.java index 795f29acb..05bb0eeae 100644 --- a/vertx-auth-common/src/test/java/io/vertx/ext/auth/WildcardPermissionBasedAuthorizationTest.java +++ b/vertx-auth-common/src/test/java/io/vertx/ext/auth/WildcardPermissionBasedAuthorizationTest.java @@ -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("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)); @@ -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("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)); diff --git a/vertx-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/impl/JWTAuthProviderImpl.java b/vertx-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/impl/JWTAuthProviderImpl.java index cd954d159..a6704db6b 100644 --- a/vertx-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/impl/JWTAuthProviderImpl.java +++ b/vertx-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/impl/JWTAuthProviderImpl.java @@ -183,7 +183,7 @@ private static JsonArray getJsonPermissions(JsonObject jwtToken, String permissi @Deprecated private User createUser(String accessToken, JsonObject jwtToken, String permissionsClaimKey) { - User result = User.create("access_token", accessToken); + User result = User.fromToken(accessToken); // update the attributes result.attributes() diff --git a/vertx-auth-ldap/src/main/java/io/vertx/ext/auth/ldap/impl/LdapAuthenticationImpl.java b/vertx-auth-ldap/src/main/java/io/vertx/ext/auth/ldap/impl/LdapAuthenticationImpl.java index 9239e77a5..85b24dde6 100644 --- a/vertx-auth-ldap/src/main/java/io/vertx/ext/auth/ldap/impl/LdapAuthenticationImpl.java +++ b/vertx-auth-ldap/src/main/java/io/vertx/ext/auth/ldap/impl/LdapAuthenticationImpl.java @@ -61,7 +61,7 @@ public void authenticate(Credentials credentials, Handler { if (contextResponse.succeeded()) { - User user = User.create("username", authInfo.getUsername()); + User user = User.fromName(authInfo.getUsername()); resultHandler.handle(Future.succeededFuture(user)); } else { resultHandler.handle(Future.failedFuture(contextResponse.cause())); diff --git a/vertx-auth-properties/src/main/java/io/vertx/ext/auth/properties/impl/PropertyFileAuthenticationImpl.java b/vertx-auth-properties/src/main/java/io/vertx/ext/auth/properties/impl/PropertyFileAuthenticationImpl.java index a69f645fd..5546fcc0c 100644 --- a/vertx-auth-properties/src/main/java/io/vertx/ext/auth/properties/impl/PropertyFileAuthenticationImpl.java +++ b/vertx-auth-properties/src/main/java/io/vertx/ext/auth/properties/impl/PropertyFileAuthenticationImpl.java @@ -178,7 +178,7 @@ public void authenticate(Credentials credentials, Handler> res Row row = rows.iterator().next(); String hashedStoredPwd = row.getString(0); if (strategy.verify(hashedStoredPwd, authInfo.getPassword())) { - resultHandler.handle(Future.succeededFuture(User.create("username", authInfo.getUsername()))); + resultHandler.handle(Future.succeededFuture(User.fromName(authInfo.getUsername()))); } else { resultHandler.handle(Future.failedFuture("Invalid username/password")); }