diff --git a/pom.xml b/pom.xml index 3cfcf35477..2559828f32 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,9 @@ vertx-web-validation vertx-web-openapi-router vertx-web-proxy + vertx-web-auth-common + vertx-web-auth-jwt + vertx-web-auth-oauth2 diff --git a/vertx-web-api-service/src/test/java/io/vertx/ext/web/api/service/RouteToEBServiceHandlerTest.java b/vertx-web-api-service/src/test/java/io/vertx/ext/web/api/service/RouteToEBServiceHandlerTest.java index ec7b4e0dfc..cd7a365a9d 100644 --- a/vertx-web-api-service/src/test/java/io/vertx/ext/web/api/service/RouteToEBServiceHandlerTest.java +++ b/vertx-web-api-service/src/test/java/io/vertx/ext/web/api/service/RouteToEBServiceHandlerTest.java @@ -10,7 +10,7 @@ import io.vertx.core.json.pointer.JsonPointer; import io.vertx.ext.auth.User; import io.vertx.ext.web.handler.BodyHandler; -import io.vertx.ext.web.impl.UserContextInternal; +import io.vertx.ext.auth.common.UserContextInternal; import io.vertx.ext.web.validation.BaseValidationHandlerTest; import io.vertx.ext.web.validation.builder.ValidationHandlerBuilder; import io.vertx.json.schema.JsonSchema; diff --git a/vertx-web-auth-common/pom.xml b/vertx-web-auth-common/pom.xml new file mode 100644 index 0000000000..30b0276c40 --- /dev/null +++ b/vertx-web-auth-common/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + vertx-web-parent + io.vertx + 5.0.0-SNAPSHOT + + + vertx-web-auth-common + + + true + + + + + io.vertx + vertx-auth-common + + + diff --git a/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AbstractUserContext.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AbstractUserContext.java new file mode 100644 index 0000000000..6ef4271c2a --- /dev/null +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AbstractUserContext.java @@ -0,0 +1,261 @@ +package io.vertx.ext.auth.common; + +import java.util.Objects; + +import io.vertx.core.Future; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.ext.auth.User; +import io.vertx.ext.web.handler.HttpException; + +public abstract class AbstractUserContext implements UserContextInternal { + + public static final String USER_SWITCH_KEY = "__vertx.user-switch-ref"; + + private static final Logger LOG = LoggerFactory.getLogger(AbstractUserContext.class); + + protected AuthenticationContext ctx; + protected User user; + + public AbstractUserContext(AuthenticationContext ctx) { + this.ctx = ctx; + } + + @Override + public void setUser(User user) { + this.user = user; + } + + @Override + public User get() { + return user; + } + + @Override + public UserContext loginHint(String loginHint) { + final Session session = ctx.session(); + + if (session == null) { + if (loginHint == null) { + // Fine, we don't need a session + return this; + } + // we always need a session, otherwise we can't track the state of the previous user + throw new IllegalStateException("SessionHandler not seen in the route. Sessions are required to keep the state"); + } + + if (loginHint == null) { + // we're removing the hint if present + session.remove("login_hint"); + } else { + session + .put("login_hint", loginHint); + } + + return this; + } + + @Override + public Future refresh() { + if (!ctx.request().method().equals(HttpMethod.GET)) { + // we can't automate a redirect to a non-GET request + return Future.failedFuture(new HttpException(405, "Method not allowed")); + } + return refresh(ctx.request().absoluteURI()); + } + + @Override + public Future refresh(String redirectUri) { + Objects.requireNonNull(redirectUri, "redirectUri cannot be null"); + + if (user == null) { + // we need to ensure that we already had a user, otherwise we can't switch + LOG.debug("Impersonation can only occur after a complete authn flow."); + return Future.failedFuture(new HttpException(401)); + } + + final Session session = ctx.session(); + + if (session != null) { + // From now on, we're changing the state + session + // force a session id regeneration to protect against replay attacks + .regenerateId(); + } + + // remove user from the context + this.user = null; + + // we should redirect the UA so this link becomes invalid + return ctx.response() + // disable all caching + .putHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") + .putHeader("Pragma", "no-cache") + .putHeader(HttpHeaders.EXPIRES, "0") + // redirect (when there is no state, redirect to home + .putHeader(HttpHeaders.LOCATION, redirectUri) + .setStatusCode(302) + .end("Redirecting to " + redirectUri + "."); + } + + @Override + public Future impersonate() { + if (!ctx.request().method().equals(HttpMethod.GET)) { + // we can't automate a redirect to a non-GET request + return Future.failedFuture(new HttpException(405, "Method not allowed")); + } + return impersonate(ctx.request().absoluteURI()); + } + + @Override + public Future impersonate(String redirectUri) { + Objects.requireNonNull(redirectUri, "redirectUri cannot be null"); + + if (user == null) { + // we need to ensure that we already had a user, otherwise we can't switch + LOG.debug("Impersonation can only occur after a complete authn flow."); + return Future.failedFuture(new HttpException(401)); + } + + final Session session = ctx.session(); + + if (session == null) { + // we always need a session, otherwise we can't track the state of the previous user + LOG.debug("SessionHandler not seen in the route. Sessions are required to keep the state"); + return Future.failedFuture(new HttpException(500)); + } + + if (session.get(USER_SWITCH_KEY) != null) { + // we always need a session, otherwise we can't track the state of the previous user + LOG.debug("Impersonation already in place"); + return Future.failedFuture(new HttpException(400)); + } + + // From now on, we're changing the state + session + // move the user out of the context (yet keep it in the session, so we can roll back + .put(USER_SWITCH_KEY, user) + // force a session id regeneration to protect against replay attacks + .regenerateId(); + + // remove the current user from the context to avoid any further access + this.user = null; + + // we should redirect the UA so this link becomes invalid + return ctx.response() + // disable all caching + .putHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") + .putHeader("Pragma", "no-cache") + .putHeader(HttpHeaders.EXPIRES, "0") + // redirect (when there is no state, redirect to home + .putHeader(HttpHeaders.LOCATION, redirectUri) + .setStatusCode(302) + .end("Redirecting to " + redirectUri + "."); + } + + @Override + public Future restore() { + if (!ctx.request().method().equals(HttpMethod.GET)) { + // we can't automate a redirect to a non-GET request + return Future.failedFuture(new HttpException(405, "Method not allowed")); + } + return restore(ctx.request().absoluteURI()); + } + + @Override + public Future restore(String redirectUri) { + Objects.requireNonNull(redirectUri, "redirectUri cannot be null"); + + if (user == null) { + // we need to ensure that we already had a user, otherwise we can't switch + LOG.debug("Impersonation can only occur after a complete authn flow."); + return Future.failedFuture(new HttpException(401)); + } + + final Session session = ctx.session(); + + if (session == null) { + // we always need a session, otherwise we can't track the state of the previous user + LOG.debug("SessionHandler not seen in the route. Sessions are required to keep the state"); + return Future.failedFuture(new HttpException(500)); + } + + if (session.get(USER_SWITCH_KEY) == null) { + // we always need a session, otherwise we can't track the state of the previous user + LOG.debug("No previous impersonation in place"); + return Future.failedFuture(new HttpException(400)); + } + + // From now on, we're changing the state + User previousUser = session.get(USER_SWITCH_KEY); + + session + // move the user out of the context (yet keep it in the session, so we can rollback + .remove(USER_SWITCH_KEY); + // remove the previous hint + session + .remove("login_hint"); + + session + // force a session id regeneration to protect against replay attacks + .regenerateId(); + + // restore it to the context + this.user = previousUser; + + // we should redirect the UA so this link becomes invalid + return ctx.response() + // disable all caching + .putHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") + .putHeader("Pragma", "no-cache") + .putHeader(HttpHeaders.EXPIRES, "0") + // redirect (when there is no state, redirect to home + .putHeader(HttpHeaders.LOCATION, redirectUri) + .setStatusCode(302) + .end("Redirecting to " + redirectUri + "."); + } + + @Override + public Future logout() { + return logout("/"); + } + + @Override + public Future logout(String redirectUri) { + Objects.requireNonNull(redirectUri, "redirectUri cannot be null"); + + final Session session = ctx.session(); + // clear the session + if (session != null) { + session.destroy(); + } + + // clear the user + user = null; + + // we should redirect the UA so this link becomes invalid + return ctx.response() + // disable all caching + .putHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") + .putHeader("Pragma", "no-cache") + .putHeader(HttpHeaders.EXPIRES, "0") + // redirect (when there is no state, redirect to home + .putHeader(HttpHeaders.LOCATION, redirectUri) + .setStatusCode(302) + .end("Redirecting to " + redirectUri + "."); + } + + @Override + public void clear() { + final Session session = ctx.session(); + // clear the session + if (session != null) { + session.destroy(); + } + + // clear the user + user = null; + } +} diff --git a/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AuthenticationContext.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AuthenticationContext.java new file mode 100644 index 0000000000..32e3a154b6 --- /dev/null +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AuthenticationContext.java @@ -0,0 +1,42 @@ +package io.vertx.ext.auth.common; + +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; + +/** + * Context that is being accepted by various authentication handlers. + *

+ * The context allows access to the HTTP request and response to verify provided authentication information + *

+ * The {@link UserContext} provides access to the authenticated user. + * + */ +public interface AuthenticationContext { + + /** + * @return the HTTP request object + */ + HttpServerRequest request(); + + /** + * @return the HTTP response object + */ + HttpServerResponse response(); + + /** + * Control the user associated with this request. The user context allows accessing the security user object as well as perform authentication refreshes, + * logout and other operations. + * + * @return the user context + */ + UserContext user(); + + String normalizedPath(); + + default void onContinue() { + // NOOP + } + + Session session(); + +} diff --git a/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AuthenticationContextInternal.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AuthenticationContextInternal.java new file mode 100644 index 0000000000..c2778f326b --- /dev/null +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AuthenticationContextInternal.java @@ -0,0 +1,17 @@ +package io.vertx.ext.auth.common; + +import io.vertx.ext.auth.audit.SecurityAudit; + +public interface AuthenticationContextInternal { + + /** + * Get or Default the security audit object. + */ + SecurityAudit securityAudit(); + + /** + * Get or Default the security audit object. + */ + void setSecurityAudit(SecurityAudit securityAudit); + +} diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/AuthenticationHandler.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AuthenticationHandler.java similarity index 63% rename from vertx-web/src/main/java/io/vertx/ext/web/handler/AuthenticationHandler.java rename to vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AuthenticationHandler.java index 4fa8a1e720..75de609417 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/AuthenticationHandler.java +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/AuthenticationHandler.java @@ -14,22 +14,16 @@ * You may elect to redistribute this code under either of these licenses. */ -package io.vertx.ext.web.handler; +package io.vertx.ext.auth.common; -import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.Handler; -import io.vertx.ext.web.RoutingContext; /** * Base interface for auth handlers. - *

- * An auth handler allows your application to provide authentication support. - *

- * An Auth handler may require a {@link SessionHandler} to be on the routing chain before it. * * @author Tim Fox * @author Paulo Lopes */ -@VertxGen(concrete = false) -public interface AuthenticationHandler extends Handler { +public interface AuthenticationHandler extends Handler { + } diff --git a/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/ScopedAuthentication.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/ScopedAuthentication.java new file mode 100644 index 0000000000..2960379c2b --- /dev/null +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/ScopedAuthentication.java @@ -0,0 +1,37 @@ +package io.vertx.ext.auth.common; + +import java.util.List; + +/** + * Internal interface for scope aware Authentication handlers. + * + * @param + * @author Paulo Lopes + */ +public interface ScopedAuthentication> { + + /** + * Return a new instance with the internal state copied from the caller but the scopes to be requested during a token request are unique to the instance. + * + * @param scope + * scope. + * @return new instance of this interface. + */ + SELF withScope(String scope); + + /** + * Return a new instance with the internal state copied from the caller but the scopes to be requested during a token request are unique to the instance. + * + * @param scopes + * scopes. + * @return new instance of this interface. + */ + SELF withScopes(List scopes); + + /** + * Return the list of scopes provided as the 1st argument, unless the list is empty. In this case, the list of scopes is obtained from the routing context + * metadata if possible. In case the metadata is not available, the list of scopes is always an empty list. + */ + List getScopesOrSearchMetadata(List scopes, C ctx); + +} diff --git a/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/Session.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/Session.java new file mode 100644 index 0000000000..cd5d44b536 --- /dev/null +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/Session.java @@ -0,0 +1,140 @@ +package io.vertx.ext.auth.common; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.GenIgnore; +import io.vertx.codegen.annotations.VertxGen; + +import java.util.Map; +import java.util.function.Function; + +/** + * Represents a browser session. + *

+ * Sessions persist between HTTP requests for a single browser session. They are deleted when the browser is closed, or + * they time-out. Session cookies are used to maintain sessions using a secure UUID. + *

+ * Sessions can be used to maintain data for a browser session, e.g. a shopping basket. + *

+ * The context must have first been routed to a {@link io.vertx.ext.web.handler.SessionHandler} + * for sessions to be available. + * + * @author Tim Fox + */ +@VertxGen +public interface Session { + + /** + * @return The new unique ID of the session. + */ + Session regenerateId(); + + /** + * @return The unique ID of the session. This is generated using a random secure UUID. + */ + String id(); + + /** + * Put some data in a session + * + * @param key the key for the data + * @param obj the data + * @return a reference to this, so the API can be used fluently + */ + @Fluent + Session put(String key, Object obj); + + /** + * Put some data in a session if absent + * + * @param key the key for the data + * @param obj the data + * @return a reference to this, so the API can be used fluently + */ + @Fluent + Session putIfAbsent(String key, Object obj); + + /** + * Put some data in a session if absent. + * + * If the specified key is not already associated with a value (or is mapped + * to {@code null}), attempts to compute its value using the given mapping + * function and enters it into this map unless {@code null}. + * + * @param key the key for the data + * @param mappingFunction a mapping function + * @return a reference to this, so the API can be used fluently + */ + @Fluent + Session computeIfAbsent(String key, Function mappingFunction); + + /** + * Get some data from the session + * + * @param key the key of the data + * @return the data + */ + T get(String key); + + /** + * Remove some data from the session + * + * @param key the key of the data + * @return the data that was there or null if none there + */ + T remove(String key); + + /** + * @return the session data as a map + */ + @GenIgnore(GenIgnore.PERMITTED_TYPE) + Map data(); + + /** + * @return true if the session has data + */ + boolean isEmpty(); + + /** + * @return the time the session was last accessed + */ + long lastAccessed(); + + /** + * Destroy the session + */ + void destroy(); + + /** + * @return has the session been destroyed? + */ + boolean isDestroyed(); + + /** + * @return has the session been renewed? + */ + boolean isRegenerated(); + + /** + * @return old ID if renewed + */ + String oldId(); + + /** + * @return the amount of time in ms, after which the session will expire, if not accessed. + */ + long timeout(); + + /** + * Mark the session as being accessed. + */ + void setAccessed(); + + /** + * The short representation of the session to be added to the session cookie. By default is the session id. + * + * @return short representation string. + */ + default String value() { + return id(); + } +} diff --git a/vertx-web/src/main/java/io/vertx/ext/web/UserContext.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/UserContext.java similarity index 99% rename from vertx-web/src/main/java/io/vertx/ext/web/UserContext.java rename to vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/UserContext.java index ad729d3893..5694598602 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/UserContext.java +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/UserContext.java @@ -13,7 +13,7 @@ * * You may elect to redistribute this code under either of these licenses. */ -package io.vertx.ext.web; +package io.vertx.ext.auth.common; import io.vertx.codegen.annotations.Fluent; import io.vertx.codegen.annotations.Nullable; diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/UserContextInternal.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/UserContextInternal.java similarity index 80% rename from vertx-web/src/main/java/io/vertx/ext/web/impl/UserContextInternal.java rename to vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/UserContextInternal.java index 391f3dbb7e..98f0dd701f 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/UserContextInternal.java +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/UserContextInternal.java @@ -1,7 +1,6 @@ -package io.vertx.ext.web.impl; +package io.vertx.ext.auth.common; import io.vertx.ext.auth.User; -import io.vertx.ext.web.UserContext; public interface UserContextInternal extends UserContext { diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/AuthenticationHandlerInternal.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/handler/AuthenticationHandlerInternal.java similarity index 68% rename from vertx-web/src/main/java/io/vertx/ext/web/handler/impl/AuthenticationHandlerInternal.java rename to vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/handler/AuthenticationHandlerInternal.java index 435d2ed9f4..7f1a2915ef 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/AuthenticationHandlerInternal.java +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/handler/AuthenticationHandlerInternal.java @@ -1,21 +1,21 @@ -package io.vertx.ext.web.handler.impl; +package io.vertx.ext.auth.common.handler; import io.vertx.core.Future; import io.vertx.ext.auth.User; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.AuthenticationHandler; +import io.vertx.ext.auth.common.AuthenticationContext; +import io.vertx.ext.auth.common.AuthenticationHandler; -public interface AuthenticationHandlerInternal extends AuthenticationHandler { +public interface AuthenticationHandlerInternal extends AuthenticationHandler { /** * Parses the credentials from the request into a JsonObject. The implementation should * be able to extract the required info for the auth provider in the format the provider * expects. * - * @param context the routing context + * @param context the authentication context * @return future user to be called once the information is available. */ - Future authenticate(RoutingContext context); + Future authenticate(C context); /** * Applies a {@code WWW-Authenticate} Response Header. @@ -24,10 +24,10 @@ public interface AuthenticationHandlerInternal extends AuthenticationHandler { * acceptable Authorization header is not sent, the server responds with * a "401 Unauthorized" status code, and a WWW-Authenticate header. * - * @param context the routing context + * @param context the authentication context * @return the {@code true} if a header was added. */ - default boolean setAuthenticateHeader(RoutingContext context) { + default boolean setAuthenticateHeader(C context) { return false; } @@ -36,10 +36,11 @@ default boolean setAuthenticateHeader(RoutingContext context) { * Overrides must call {@link RoutingContext#next()} on success. Implementations must call this handler * at the end of the authentication process. * - * @param ctx the routing context + * @param ctx the authentication context + * @param authenticated the authenticated user */ - default void postAuthentication(RoutingContext ctx) { - ctx.next(); + default void postAuthentication(C ctx, User authenticated) { + ctx.onContinue(); } /** diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/AuthenticationHandlerImpl.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/handler/impl/AuthenticationHandlerImpl.java similarity index 65% rename from vertx-web/src/main/java/io/vertx/ext/web/handler/impl/AuthenticationHandlerImpl.java rename to vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/handler/impl/AuthenticationHandlerImpl.java index a6570e71fb..c4923aaa25 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/AuthenticationHandlerImpl.java +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/handler/impl/AuthenticationHandlerImpl.java @@ -14,26 +14,22 @@ * You may elect to redistribute this code under either of these licenses. */ -package io.vertx.ext.web.handler.impl; +package io.vertx.ext.auth.common.handler.impl; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerRequest; import io.vertx.ext.auth.User; import io.vertx.ext.auth.authentication.AuthenticationProvider; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.Session; -import io.vertx.ext.web.handler.HttpException; -import io.vertx.ext.web.impl.UserContextInternal; +import io.vertx.ext.auth.common.AuthenticationContext; +import io.vertx.ext.auth.common.Session; +import io.vertx.ext.auth.common.UserContextInternal; +import io.vertx.ext.auth.common.handler.AuthenticationHandlerInternal; /** * @author Tim Fox */ -public abstract class AuthenticationHandlerImpl implements AuthenticationHandlerInternal { - - static final HttpException UNAUTHORIZED = new HttpException(401); - static final HttpException BAD_REQUEST = new HttpException(400); - static final HttpException BAD_METHOD = new HttpException(405); +public abstract class AuthenticationHandlerImpl implements AuthenticationHandlerInternal { protected final T authProvider; // signal the kind of Multi-Factor Authentication used by the handler @@ -48,8 +44,7 @@ public AuthenticationHandlerImpl(T authProvider, String mfa) { this.mfa = mfa; } - @Override - public void handle(RoutingContext ctx) { + public void handle(C ctx) { if (handlePreflight(ctx)) { return; @@ -70,7 +65,7 @@ public void handle(RoutingContext ctx) { if (!ctx.request().isEnded()) { ctx.request().resume(); } - postAuthentication(ctx); + postAuthentication(ctx, user); return; } } else { @@ -78,7 +73,7 @@ public void handle(RoutingContext ctx) { if (!ctx.request().isEnded()) { ctx.request().resume(); } - postAuthentication(ctx); + postAuthentication(ctx, user); return; } } @@ -97,7 +92,7 @@ public void handle(RoutingContext ctx) { if (!ctx.request().isEnded()) { ctx.request().resume(); } - postAuthentication(ctx); + postAuthentication(ctx, authenticated); }) .onFailure(cause -> { // to allow further processing if needed @@ -108,41 +103,9 @@ public void handle(RoutingContext ctx) { }); } - /** - * This method is protected so custom auth handlers can override the default - * error handling - */ - protected void processException(RoutingContext ctx, Throwable exception) { - if (exception != null) { - if (exception instanceof HttpException) { - final int statusCode = ((HttpException) exception).getStatusCode(); - final String payload = ((HttpException) exception).getPayload(); - - switch (statusCode) { - case 302: - ctx.response() - .putHeader(HttpHeaders.LOCATION, payload) - .setStatusCode(302) - .end("Redirecting to " + payload + "."); - return; - case 401: - if (!"XMLHttpRequest".equals(ctx.request().getHeader("X-Requested-With"))) { - setAuthenticateHeader(ctx); - } - ctx.fail(401, exception); - return; - default: - ctx.fail(statusCode, exception); - return; - } - } - } - - // fallback 500 - ctx.fail(exception); - } + protected abstract void processException(C ctx, Throwable cause); - private boolean handlePreflight(RoutingContext ctx) { + private boolean handlePreflight(C ctx) { final HttpServerRequest request = ctx.request(); // See: https://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0 // Preflight requests should not be subject to security due to the reason UAs will remove the Authorization header @@ -154,7 +117,7 @@ private boolean handlePreflight(RoutingContext ctx) { for (String ctrlReq : accessControlRequestHeader.split(",")) { if (ctrlReq.equalsIgnoreCase("Authorization")) { // this request has auth in access control, so we can allow preflighs without authentication - ctx.next(); + ctx.onContinue(); return true; } } diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/HTTPAuthorizationHandler.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/handler/impl/HTTPAuthorizationHandler.java similarity index 85% rename from vertx-web/src/main/java/io/vertx/ext/web/handler/impl/HTTPAuthorizationHandler.java rename to vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/handler/impl/HTTPAuthorizationHandler.java index 199719380b..617844f78e 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/HTTPAuthorizationHandler.java +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/auth/common/handler/impl/HTTPAuthorizationHandler.java @@ -13,20 +13,22 @@ * * You may elect to redistribute this code under either of these licenses. */ -package io.vertx.ext.web.handler.impl; +package io.vertx.ext.auth.common.handler.impl; import io.vertx.core.Future; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; import io.vertx.ext.auth.authentication.AuthenticationProvider; -import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.auth.common.AuthenticationContext; + +import static io.vertx.ext.web.handler.HttpException.*; /** * This a common handler for auth handler that use the `Authorization` HTTP header. * * @author Paulo Lopes */ -public abstract class HTTPAuthorizationHandler extends AuthenticationHandlerImpl { +public abstract class HTTPAuthorizationHandler extends AuthenticationHandlerImpl { // this should match the IANA registry: https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml public enum Type { @@ -73,11 +75,11 @@ public HTTPAuthorizationHandler(T authProvider, Type type, String realm) { } } - protected final Future parseAuthorization(RoutingContext ctx) { + protected final Future parseAuthorization(C ctx) { return parseAuthorization(ctx, false); } - protected final Future parseAuthorization(RoutingContext ctx, boolean optional) { + protected final Future parseAuthorization(C ctx, boolean optional) { final HttpServerRequest request = ctx.request(); final String authorization = request.headers().get(HttpHeaders.AUTHORIZATION); @@ -109,7 +111,7 @@ protected final Future parseAuthorization(RoutingContext ctx, boolean op } @Override - public boolean setAuthenticateHeader(RoutingContext context) { + public boolean setAuthenticateHeader(C context) { if (realm != null && realm.length() > 0) { context.response() .headers() diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/HttpException.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/web/common/HttpException.java similarity index 95% rename from vertx-web/src/main/java/io/vertx/ext/web/handler/HttpException.java rename to vertx-web-auth-common/src/main/java/io/vertx/ext/web/common/HttpException.java index 562dea19d0..1dcb6fa537 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/HttpException.java +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/web/common/HttpException.java @@ -13,7 +13,7 @@ * * You may elect to redistribute this code under either of these licenses. */ -package io.vertx.ext.web.handler; +package io.vertx.ext.web.common; import io.netty.handler.codec.http.HttpResponseStatus; @@ -28,7 +28,7 @@ * * @author Paulo Lopes */ -public final class HttpException extends RuntimeException { +public abstract class HttpException extends RuntimeException { private final int statusCode; private final String payload; diff --git a/vertx-web-auth-common/src/main/java/io/vertx/ext/web/handler/HttpException.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/web/handler/HttpException.java new file mode 100644 index 0000000000..b80ac25ca1 --- /dev/null +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/web/handler/HttpException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 Red Hat, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +package io.vertx.ext.web.handler; + +/** + * @see io.vertx.ext.web.common.HttpException + */ +public final class HttpException extends io.vertx.ext.web.common.HttpException { + + public static final HttpException UNAUTHORIZED = new HttpException(401); + public static final HttpException BAD_REQUEST = new HttpException(400); + public static final HttpException BAD_METHOD = new HttpException(405); + + public HttpException() { + super(); + } + + public HttpException(int statusCode) { + super(statusCode); + } + + public HttpException(int statusCode, Throwable cause) { + super(statusCode, cause); + } + + public HttpException(int statusCode, String payload) { + super(statusCode, payload); + } + + public HttpException(int statusCode, String payload, Throwable cause) { + super(statusCode, payload, cause); + } + +} diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/Origin.java b/vertx-web-auth-common/src/main/java/io/vertx/ext/web/impl/Origin.java similarity index 98% rename from vertx-web/src/main/java/io/vertx/ext/web/impl/Origin.java rename to vertx-web-auth-common/src/main/java/io/vertx/ext/web/impl/Origin.java index c03fb6ba07..1256cc3572 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/Origin.java +++ b/vertx-web-auth-common/src/main/java/io/vertx/ext/web/impl/Origin.java @@ -19,7 +19,7 @@ import io.vertx.core.http.HttpHeaders; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; -import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.auth.common.AuthenticationContext; /** * An origin follows rfc6454#section-7 @@ -422,7 +422,7 @@ private static boolean isBlank(String s) { return s == null || s.trim().isEmpty(); } - public static boolean check(Origin origin, RoutingContext ctx) { + public static boolean check(Origin origin, AuthenticationContext ctx) { /* Verifying Same Origin with Standard Headers */ if (origin != null) { //Try to get the source from the "Origin" header diff --git a/vertx-web-auth-common/src/main/resources/META-INF/MANIFEST.MF b/vertx-web-auth-common/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..1eae9090c8 --- /dev/null +++ b/vertx-web-auth-common/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Automatic-Module-Name: io.vertx.web.auth-common + diff --git a/vertx-web-auth-jwt/pom.xml b/vertx-web-auth-jwt/pom.xml new file mode 100644 index 0000000000..c433fa0fd1 --- /dev/null +++ b/vertx-web-auth-jwt/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + vertx-web-parent + io.vertx + 5.0.0-SNAPSHOT + + + vertx-web-auth-jwt + + + true + + + + + vertx-web-auth-common + io.vertx + 5.0.0-SNAPSHOT + + + + vertx-auth-jwt + io.vertx + 5.0.0-SNAPSHOT + + + diff --git a/vertx-web-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/AbstractJWTHandler.java b/vertx-web-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/AbstractJWTHandler.java new file mode 100644 index 0000000000..1aa23a8a14 --- /dev/null +++ b/vertx-web-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/AbstractJWTHandler.java @@ -0,0 +1,122 @@ +package io.vertx.ext.auth.jwt; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; +import io.vertx.ext.auth.audit.Marker; +import io.vertx.ext.auth.audit.SecurityAudit; +import io.vertx.ext.auth.authentication.TokenCredentials; +import io.vertx.ext.auth.common.AuthenticationContext; +import io.vertx.ext.auth.common.AuthenticationContextInternal; +import io.vertx.ext.auth.common.handler.impl.HTTPAuthorizationHandler; +import io.vertx.ext.web.handler.HttpException; + +public abstract class AbstractJWTHandler extends HTTPAuthorizationHandler implements JWTAuthHandler, io.vertx.ext.auth.common.ScopedAuthentication> { + + protected final List scopes; + protected String delimiter; + + public AbstractJWTHandler(JWTAuth authProvider, Type bearer, String realm) { + super(authProvider, bearer, realm); + scopes = Collections.emptyList(); + this.delimiter = " "; + } + + public AbstractJWTHandler(JWTAuth authProvider, List scopes, + String delimiter, String realm) { + super(authProvider, Type.BEARER, realm); + Objects.requireNonNull(scopes, "scopes cannot be null"); + this.scopes = scopes; + Objects.requireNonNull(delimiter, "delimiter cannot be null"); + this.delimiter = delimiter; + } + + @Override + public Future authenticate(C context) { + + return parseAuthorization(context) + .compose(token -> { + int segments = 0; + for (int i = 0; i < token.length(); i++) { + char c = token.charAt(i); + if (c == '.') { + if (++segments == 3) { + return Future.failedFuture(new HttpException(400, "Too many segments in token")); + } + continue; + } + if (Character.isLetterOrDigit(c) || c == '-' || c == '_') { + continue; + } + // invalid character + return Future.failedFuture(new HttpException(400, "Invalid character in token: " + (int) c)); + } + + final TokenCredentials credentials = new TokenCredentials(token); + final SecurityAudit audit = ((AuthenticationContextInternal) context).securityAudit(); + audit.credentials(credentials); + + return + authProvider + .authenticate(new TokenCredentials(token)) + .andThen(op -> audit.audit(Marker.AUTHENTICATION, op.succeeded())) + .recover(err -> Future.failedFuture(new HttpException(401, err))); + }); + } + + /** + * The default behavior for post-authentication + */ + @Override + public void postAuthentication(C ctx, User authenticated) { + final User user = ctx.user().get(); + if (user == null) { + // bad state + fail(ctx, 403, "no user in the context"); + return; + } + // the user is authenticated, however the user may not have all the required scopes + final List scopes = getScopesOrSearchMetadata(this.scopes, ctx); + + if (scopes.size() > 0) { + final JsonObject jwt = user.get("accessToken"); + if (jwt == null) { + fail(ctx, 403, "Invalid JWT: null"); + return; + } + + if (jwt.getValue("scope") == null) { + fail(ctx, 403, "Invalid JWT: scope claim is required"); + return; + } + + List target; + if (jwt.getValue("scope") instanceof String) { + target = + Stream.of(jwt.getString("scope") + .split(delimiter)) + .collect(Collectors.toList()); + } else { + target = jwt.getJsonArray("scope").getList(); + } + + if (target != null) { + for (String scope : scopes) { + if (!target.contains(scope)) { + fail(ctx, 403, "JWT scopes != handler scopes"); + return; + } + } + } + } + ctx.onContinue(); + } + + abstract protected void fail(C ctx, int code, String msg); +} diff --git a/vertx-web-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/JWTAuthHandler.java b/vertx-web-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/JWTAuthHandler.java new file mode 100644 index 0000000000..22f7e4b6f2 --- /dev/null +++ b/vertx-web-auth-jwt/src/main/java/io/vertx/ext/auth/jwt/JWTAuthHandler.java @@ -0,0 +1,40 @@ +package io.vertx.ext.auth.jwt; + +import java.util.List; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.ext.auth.common.AuthenticationContext; +import io.vertx.ext.auth.common.AuthenticationHandler; + +public interface JWTAuthHandler extends AuthenticationHandler { + + /** + * Set the scope delimiter. By default this is a space character. + * + * @param delimiter + * scope delimiter. + * @return fluent self. + */ + @Fluent + JWTAuthHandler scopeDelimiter(String delimiter); + + /** + * Return a new instance with the internal state copied from the caller but the scopes to be requested during a token request are unique to the instance. When + * scopes are applied to the handler, the default scopes from the route metadata will be ignored. + * + * @param scope + * scope. + * @return new instance of this interface. + */ + JWTAuthHandler withScope(String scope); + + /** + * Return a new instance with the internal state copied from the caller but the scopes to be requested during a token request are unique to the instance. When + * scopes are applied to the handler, the default scopes from the route metadata will be ignored. + * + * @param scopes + * scopes. + * @return new instance of this interface. + */ + JWTAuthHandler withScopes(List scopes); +} diff --git a/vertx-web-auth-jwt/src/main/resources/META-INF/MANIFEST.MF b/vertx-web-auth-jwt/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..85f2995cd1 --- /dev/null +++ b/vertx-web-auth-jwt/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1 @@ +Automatic-Module-Name: io.vertx.web.auth-jwt diff --git a/vertx-web-auth-oauth2/pom.xml b/vertx-web-auth-oauth2/pom.xml new file mode 100644 index 0000000000..95accfcf5a --- /dev/null +++ b/vertx-web-auth-oauth2/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + vertx-web-parent + io.vertx + 5.0.0-SNAPSHOT + + + vertx-web-auth-oauth2 + + + true + + + + + vertx-web-auth-common + io.vertx + 5.0.0-SNAPSHOT + + + + vertx-auth-oauth2 + io.vertx + 5.0.0-SNAPSHOT + + + diff --git a/vertx-web-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/AbstractOAuth2Handler.java b/vertx-web-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/AbstractOAuth2Handler.java new file mode 100644 index 0000000000..550ee65752 --- /dev/null +++ b/vertx-web-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/AbstractOAuth2Handler.java @@ -0,0 +1,324 @@ +package io.vertx.ext.auth.oauth2; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; +import io.vertx.ext.auth.VertxContextPRNG; +import io.vertx.ext.auth.audit.Marker; +import io.vertx.ext.auth.audit.SecurityAudit; +import io.vertx.ext.auth.authentication.Credentials; +import io.vertx.ext.auth.authentication.TokenCredentials; +import io.vertx.ext.auth.common.AuthenticationContext; +import io.vertx.ext.auth.common.AuthenticationContextInternal; +import io.vertx.ext.auth.common.Session; +import io.vertx.ext.auth.common.handler.impl.HTTPAuthorizationHandler; +import io.vertx.ext.auth.impl.Codec; +import io.vertx.ext.web.handler.HttpException; +import io.vertx.ext.web.impl.Origin; + +public abstract class AbstractOAuth2Handler extends HTTPAuthorizationHandler implements OAuth2AuthHandler, io.vertx.ext.auth.common.ScopedAuthentication> { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractOAuth2Handler.class); + + protected final VertxContextPRNG prng; + protected final Origin callbackURL; + protected final MessageDigest sha256; + + protected final List scopes; + protected JsonObject extraParams; + protected String prompt; + protected int pkce = -1; + // explicit signal that tokens are handled as bearer only (meaning, no backend server known) + protected boolean bearerOnly = true; + + public AbstractOAuth2Handler(Vertx vertx, OAuth2Auth authProvider, String callbackURL, String realm) { + super(authProvider, Type.BEARER, realm); + + // get a reference to the prng + this.prng = VertxContextPRNG.current(vertx); + // get a reference to the sha-256 digest + try { + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Cannot get instance of SHA-256 MessageDigest", e); + } + // process callback + if (callbackURL != null) { + this.callbackURL = Origin.parse(callbackURL); + } else { + this.callbackURL = null; + } + // scopes are empty by default + this.scopes = Collections.emptyList(); + } + + protected AbstractOAuth2Handler(AbstractOAuth2Handler base, List scopes) { + super(base.authProvider, Type.BEARER, base.realm); + this.prng = base.prng; + this.callbackURL = base.callbackURL; + this.prompt = base.prompt; + this.pkce = base.pkce; + this.bearerOnly = base.bearerOnly; + + // get a new reference to the sha-256 digest + try { + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Cannot get instance of SHA-256 MessageDigest", e); + } + // state copy + if (base.extraParams != null) { + extraParams = base.extraParams.copy(); + } + + // apply the new scopes + Objects.requireNonNull(scopes, "scopes cannot be null"); + this.scopes = scopes; + } + + @Override + public Future authenticate(C context) { + // when the handler is working as bearer only, then the `Authorization` header is required + return parseAuthorization(context, !bearerOnly) + .compose(token -> { + // Authorization header can be null when in not in bearerOnly mode + if (token == null) { + // redirect request to the oauth2 server as we know nothing about this request + if (bearerOnly) { + // it's a failure both cases but the cause is not the same + return Future.failedFuture("callback route is not configured."); + } + // when this handle is mounted as a catch all, the callback route must be configured before, + // as it would shade the callback route. When a request matches the callback path and has the + // method GET the exceptional case should not redirect to the oauth2 server as it would become + // an infinite redirect loop. In this case an exception must be raised. + if (context.request().method() == HttpMethod.GET && context.normalizedPath().equals(callbackURL.resource())) { + LOG.warn("The callback route is shaded by the OAuth2AuthHandler, ensure the callback route is added BEFORE the OAuth2AuthHandler route!"); + return Future.failedFuture(new HttpException(500, "Infinite redirect loop [oauth2 callback]")); + } else { + if (context.request().method() != HttpMethod.GET) { + // we can only redirect GET requests + LOG.error("OAuth2 redirect attempt to non GET resource"); + return Future.failedFuture(new HttpException(405, new IllegalStateException("OAuth2 redirect attempt to non GET resource"))); + } + + // the redirect is processed as a failure to abort the chain + String redirectUri = context.request().uri(); + try { + return Future.failedFuture(new HttpException(302, authURI(context, redirectUri))); + } catch (IllegalStateException e) { + return Future.failedFuture(e); + } + } + } else { + // continue + final List scopes = getScopesOrSearchMetadata(this.scopes, context); + + final Credentials credentials = + scopes.size() > 0 ? new TokenCredentials(token).setScopes(scopes) : new TokenCredentials(token); + + final SecurityAudit audit = ((AuthenticationContextInternal) context).securityAudit(); + audit.credentials(credentials); + + return authProvider.authenticate(credentials) + .andThen(op -> audit.audit(Marker.AUTHENTICATION, op.succeeded())) + .recover(err -> Future.failedFuture(new HttpException(401, err))); + } + }); + } + + private String authURI(C context, String redirectURL) { + + String state = null; + String codeVerifier = null; + String loginHint = null; + + final Session session = context.session(); + + if (session == null) { + if (pkce > 0) { + // we can only handle PKCE with a session + throw new IllegalStateException("OAuth2 PKCE requires a session to be present"); + } + } else { + // there's a session we can make this request comply to the Oauth2 spec and add an opaque state + + loginHint = session.get("login_hint"); + // hint will be considered at least once + session.remove("login_hint"); + + session + .put("redirect_uri", redirectURL); + + // create a state value to mitigate replay attacks + state = prng.nextString(6); + // store the state in the session + session + .put("state", state); + + if (pkce > 0) { + codeVerifier = prng.nextString(pkce); + // store the code verifier in the session + session + .put("pkce", codeVerifier); + } + } + + final OAuth2AuthorizationURL config = new OAuth2AuthorizationURL(); + + if (extraParams != null) { + for (Map.Entry entry : extraParams) { + if (entry.getValue() != null) { + config.putAdditionalParameter(entry.getKey(), entry.getValue().toString()); + } + } + } + + config + .setState(state != null ? state : redirectURL) + .setLoginHint(loginHint) + .setPrompt(prompt); + + if (callbackURL != null) { + config.setRedirectUri(callbackURL.href()); + } + + final List scopes = getScopesOrSearchMetadata(this.scopes, context); + + if (scopes.size() > 0) { + config.setScopes(scopes); + } + + if (codeVerifier != null) { + synchronized (sha256) { + sha256.update(codeVerifier.getBytes(StandardCharsets.US_ASCII)); + config + .setCodeChallenge(Codec.base64UrlEncode(sha256.digest())) + .setCodeChallengeMethod("S256"); + } + } + + return authProvider.authorizeURL(new OAuth2AuthorizationURL(config)); + } + + @Override + public OAuth2AuthHandler extraParams(JsonObject extraParams) { + this.extraParams = extraParams; + return this; + } + + @Override + public OAuth2AuthHandler prompt(String prompt) { + this.prompt = prompt; + return this; + } + + @Override + public OAuth2AuthHandler pkceVerifierLength(int length) { + if (length >= 0) { + // requires verification + if (length < 43 || length > 128) { + throw new IllegalArgumentException("Length must be between 34 and 128"); + } + } + this.pkce = length; + return this; + } + + + + private static final Set OPENID_SCOPES = new HashSet<>(); + + static { + OPENID_SCOPES.add("openid"); + OPENID_SCOPES.add("profile"); + OPENID_SCOPES.add("email"); + OPENID_SCOPES.add("phone"); + OPENID_SCOPES.add("offline"); + } + + /** + * The default behavior for post-authentication + */ + @Override + public void postAuthentication(C ctx, User authenticatedUser) { + // the user is authenticated, however the user may not have all the required scopes + final List scopes = getScopesOrSearchMetadata(this.scopes, ctx); + + if (scopes.size() > 0) { + final User user = ctx.user().get(); + if (user == null) { + // bad state + fail(ctx, 403, "no user in the context"); + return; + } + + if (user.principal().containsKey("scope")) { + final String userScopes = user.principal().getString("scope"); + if (userScopes != null) { + // user principal contains scope, a basic assertion is required to ensure that + // the scopes present match the required ones + + // check if openid is active + final boolean openId = userScopes.contains("openid"); + + for (String scope : scopes) { + // do not assert openid scopes if openid is active + if (openId && OPENID_SCOPES.contains(scope)) { + continue; + } + + int idx = userScopes.indexOf(scope); + if (idx != -1) { + // match, but is it valid? + if ( + (idx != 0 && userScopes.charAt(idx -1) != ' ') || + (idx + scope.length() != userScopes.length() && userScopes.charAt(idx + scope.length()) != ' ')) { + // invalid scope assignment + fail(ctx, 403, "principal scope != handler scopes"); + return; + } + } else { + // invalid scope assignment + fail(ctx, 403, "principal scope != handler scopes"); + return; + } + } + } + } + } + ctx.onContinue(); + } + + @Override + public boolean performsRedirect() { + // depending on the time this method is invoked + // we can deduct with more accuracy if a redirect is possible or not + if (!bearerOnly) { + // we know that a redirect is definitely possible + // as the callback handler has been created + return true; + } else { + // the callback hasn't been mounted so we need to assume + // that if no callbackURL is provided, then there isn't + // a redirect happening in this application + return callbackURL != null; + } + } + + abstract protected void fail(C ctx, int code, String msg); +} diff --git a/vertx-web-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/OAuth2AuthHandler.java b/vertx-web-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/OAuth2AuthHandler.java new file mode 100644 index 0000000000..f55127e84a --- /dev/null +++ b/vertx-web-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/OAuth2AuthHandler.java @@ -0,0 +1,80 @@ +package io.vertx.ext.auth.oauth2; + +import java.util.List; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.common.AuthenticationContext; +import io.vertx.ext.auth.common.AuthenticationHandler; + +public interface OAuth2AuthHandler extends AuthenticationHandler { + + /** + * Extra parameters needed to be passed while requesting a token. + * + * @param extraParams extra optional parameters. + * @return self + */ + @Fluent + OAuth2AuthHandler extraParams(JsonObject extraParams); + + /** + * Return a new instance with the internal state copied from the caller but the scopes to be requested during + * a token request are unique to the instance. When scopes are applied to the handler, the default scopes from the + * route metadata will be ignored. + * + * @param scope scope. + * @return new instance of this interface. + */ + @Fluent + OAuth2AuthHandler withScope(String scope); + + /** + * Return a new instance with the internal state copied from the caller but the scopes to be requested during + * a token request are unique to the instance. When scopes are applied to the handler, the default scopes from the + * route metadata will be ignored. + * + * @param scopes scopes. + * @return new instance of this interface. + */ + @Fluent + OAuth2AuthHandler withScopes(List scopes); + + /** + * Indicates the type of user interaction that is required. Not all providers support this or the full list. + * + * Well known values are: + * + *

    + *
  • login will force the user to enter their credentials on that request, negating single-sign on.
  • + *
  • none is the opposite - it will ensure that the user isn't presented with any interactive prompt + * whatsoever. If the request can't be completed silently via single-sign on, the Microsoft identity platform + * endpoint will return an interaction_required error.
  • + *
  • consent will trigger the OAuth consent dialog after the user signs in, asking the user to grant + * permissions to the app.
  • + *
  • select_account will interrupt single sign-on providing account selection experience listing all the + * accounts either in session or any remembered account or an option to choose to use a different account + * altogether.
  • + *
  • + *
+ * + * @param prompt the prompt choice. + * @return self + */ + @Fluent + OAuth2AuthHandler prompt(String prompt); + + /** + * PKCE (RFC 7636) is an extension to the Authorization Code flow to prevent several attacks and to be able to + * securely perform the OAuth exchange from public clients. + * + * It was originally designed to protect mobile apps, but its ability to prevent authorization code injection + * makes it useful for every OAuth client, even web apps that use a client secret. + * + * @param length A number between 43 and 128. Or -1 to disable. + * @return self + */ + @Fluent + OAuth2AuthHandler pkceVerifierLength(int length); + +} diff --git a/vertx-web-graphql/src/main/java/examples/GraphQLExamples.java b/vertx-web-graphql/src/main/java/examples/GraphQLExamples.java index e01b7ee269..6414208b1e 100644 --- a/vertx-web-graphql/src/main/java/examples/GraphQLExamples.java +++ b/vertx-web-graphql/src/main/java/examples/GraphQLExamples.java @@ -29,7 +29,7 @@ import io.vertx.ext.web.FileUpload; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.UserContext; +import io.vertx.ext.auth.common.UserContext; import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.graphql.*; import io.vertx.ext.web.handler.graphql.instrumentation.JsonObjectAdapter; diff --git a/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/RouterBuilder.java b/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/RouterBuilder.java index 4f5ca8a36c..5565e1fd5c 100644 --- a/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/RouterBuilder.java +++ b/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/RouterBuilder.java @@ -19,7 +19,6 @@ import io.vertx.core.Vertx; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.AuthenticationHandler; import io.vertx.openapi.contract.OpenAPIContract; import io.vertx.ext.web.openapi.router.impl.RouterBuilderImpl; import io.vertx.openapi.validation.RequestUtils; diff --git a/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/AuthenticationHandlers.java b/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/AuthenticationHandlers.java index 69ba9d71ab..2a69286343 100644 --- a/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/AuthenticationHandlers.java +++ b/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/AuthenticationHandlers.java @@ -3,7 +3,8 @@ import io.vertx.core.Future; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.AuthenticationHandler; +import io.vertx.ext.auth.common.AuthenticationHandler; +import io.vertx.ext.web.handler.WebAuthenticationHandler; import io.vertx.ext.web.handler.ChainAuthHandler; import io.vertx.ext.web.handler.OAuth2AuthHandler; import io.vertx.ext.web.handler.SimpleAuthenticationHandler; @@ -20,12 +21,12 @@ */ class AuthenticationHandlers { - private static final AuthenticationHandler ANONYMOUS_SUCCESS_AUTH_HANDLER = + private static final WebAuthenticationHandler ANONYMOUS_SUCCESS_AUTH_HANDLER = SimpleAuthenticationHandler .create() .authenticate(ctx -> Future.succeededFuture()); - private final Map> securityHandlers; + private final Map> securityHandlers; private final Map callbackHandlers; AuthenticationHandlers() { @@ -33,7 +34,7 @@ class AuthenticationHandlers { this.callbackHandlers = new HashMap<>(); } - protected void addRequirement(String name, AuthenticationHandler handler, String callback) { + protected void addRequirement(String name, WebAuthenticationHandler handler, String callback) { securityHandlers .computeIfAbsent(name, k -> new ArrayList<>()) .add(handler); @@ -53,16 +54,16 @@ protected void addRequirement(String name, AuthenticationHandler handler, String * The input array is an OR of different AND security requirements */ protected void solve(Operation operation, Route route, boolean failOnNotFound) { - AuthenticationHandler authn = or(route, operation.getSecurityRequirements(), failOnNotFound); + WebAuthenticationHandler authn = or(route, operation.getSecurityRequirements(), failOnNotFound); if (authn != null) { route.handler(authn); } } - private List resolveHandlers(Route route, String name, List scopes, + private List resolveHandlers(Route route, String name, List scopes, boolean failOnNotFound) { - List authenticationHandlers; + List authenticationHandlers; if (failOnNotFound) { authenticationHandlers = Optional .ofNullable(this.securityHandlers.get(name)) @@ -81,8 +82,10 @@ private List resolveHandlers(Route route, String name, Li authenticationHandlers = authenticationHandlers .stream() .map(authHandler -> { - if (authHandler instanceof ScopedAuthentication) { - return ((ScopedAuthentication) authHandler).withScopes(scopes); + if (authHandler instanceof ScopedAuthentication) { + AuthenticationHandler scopedHandler = ((ScopedAuthentication) authHandler).withScopes(scopes); + WebAuthenticationHandler webAuthHandler = (WebAuthenticationHandler)scopedHandler; + return webAuthHandler; } else { return authHandler; } @@ -93,8 +96,8 @@ private List resolveHandlers(Route route, String name, Li return authenticationHandlers; } - private AuthenticationHandler and(Route route, SecurityRequirement securityRequirement, boolean failOnNotFound) { - List handlers = securityRequirement.getNames() + private WebAuthenticationHandler and(Route route, SecurityRequirement securityRequirement, boolean failOnNotFound) { + List handlers = securityRequirement.getNames() .stream() .flatMap(name -> resolveHandlers(route, name, securityRequirement.getScopes(name), failOnNotFound).stream()) .collect(Collectors.toList()); @@ -113,7 +116,7 @@ private AuthenticationHandler and(Route route, SecurityRequirement securityRequi return authHandler; } - private AuthenticationHandler or(Route route, List securityRequirements, + private WebAuthenticationHandler or(Route route, List securityRequirements, boolean failOnNotFound) { if (securityRequirements == null || securityRequirements.isEmpty()) { return null; diff --git a/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/RouterBuilderImpl.java b/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/RouterBuilderImpl.java index e892ba33d8..37a27a8815 100644 --- a/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/RouterBuilderImpl.java +++ b/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/RouterBuilderImpl.java @@ -20,7 +20,7 @@ import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.AuthenticationHandler; +import io.vertx.ext.web.handler.WebAuthenticationHandler; import io.vertx.ext.web.handler.InputTrustHandler; import io.vertx.ext.web.openapi.router.OpenAPIRoute; import io.vertx.ext.web.openapi.router.RequestExtractor; @@ -88,7 +88,7 @@ public RouterBuilder rootHandler(Handler rootHandler) { } @Override - public RouterBuilder security(String securitySchemeName, AuthenticationHandler authenticationHandler, + public RouterBuilder security(String securitySchemeName, WebAuthenticationHandler authenticationHandler, String callback) { securityHandlers.addRequirement(securitySchemeName, authenticationHandler, callback); return this; diff --git a/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/RouterBuilderInternal.java b/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/RouterBuilderInternal.java index 43e76602f5..dd943fa46c 100644 --- a/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/RouterBuilderInternal.java +++ b/vertx-web-openapi-router/src/main/java/io/vertx/ext/web/openapi/router/impl/RouterBuilderInternal.java @@ -16,7 +16,7 @@ package io.vertx.ext.web.openapi.router.impl; import io.vertx.codegen.annotations.Fluent; -import io.vertx.ext.web.handler.AuthenticationHandler; +import io.vertx.ext.web.handler.WebAuthenticationHandler; import io.vertx.ext.web.openapi.router.RouterBuilder; interface RouterBuilderInternal extends RouterBuilder { @@ -33,5 +33,5 @@ interface RouterBuilderInternal extends RouterBuilder { * @return self */ @Fluent - RouterBuilder security(String securitySchemeName, AuthenticationHandler handler, String callback); + RouterBuilder security(String securitySchemeName, WebAuthenticationHandler handler, String callback); } diff --git a/vertx-web/pom.xml b/vertx-web/pom.xml index 263fb04b97..78ab5c2a6a 100644 --- a/vertx-web/pom.xml +++ b/vertx-web/pom.xml @@ -39,6 +39,12 @@ io.vertx vertx-auth-common + + + io.vertx + vertx-web-auth-common + ${project.version} + io.vertx vertx-bridge-common @@ -51,11 +57,24 @@ vertx-auth-jwt true + + + io.vertx + vertx-web-auth-jwt + ${project.version} + true + io.vertx vertx-auth-oauth2 true + + + io.vertx + vertx-web-auth-oauth2 + ${project.version} + io.vertx vertx-auth-htdigest @@ -109,6 +128,11 @@ jackson-databind test + + io.vertx + vertx-auth-jwt-grpc + 5.0.0-SNAPSHOT + diff --git a/vertx-web/src/main/asciidoc/index.adoc b/vertx-web/src/main/asciidoc/index.adoc index e5c07c626a..c3309ac768 100644 --- a/vertx-web/src/main/asciidoc/index.adoc +++ b/vertx-web/src/main/asciidoc/index.adoc @@ -1199,7 +1199,7 @@ make sure your authentication handler is before your application handlers on tho ---- If the authentication handler has successfully authenticated the user it will inject a {@link io.vertx.ext.auth.User} -object into the {@link io.vertx.ext.web.UserContext} so it's available in your handlers from the routing context: +object into the {@link io.vertx.ext.auth.common.UserContext} so it's available in your handlers from the routing context: {@link io.vertx.ext.web.RoutingContext#user()}. If you want your User object to be stored in the session so it's available between requests so you don't have to @@ -1207,7 +1207,7 @@ authenticate on each request, then you should make sure you have a session handl Once you have your user object you can also programmatically use the methods on it to authorize the user. -If you want to cause the user to be logged out you can call {@link io.vertx.ext.web.UserContext#logout()} +If you want to cause the user to be logged out you can call {@link io.vertx.ext.auth.common.UserContext#logout()} on the routing context `user` getter. The logout will remove the user from the session if there is one and perform a redirect to an optional uri or `/` by default. === HTTP Basic Authentication diff --git a/vertx-web/src/main/java/examples/WebExamples.java b/vertx-web/src/main/java/examples/WebExamples.java index 9fc55f9488..f8ab1f216d 100644 --- a/vertx-web/src/main/java/examples/WebExamples.java +++ b/vertx-web/src/main/java/examples/WebExamples.java @@ -808,14 +808,14 @@ public void example37(Vertx vertx, AuthenticationProvider authProvider, Router r router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx))); - AuthenticationHandler basicAuthHandler = BasicAuthHandler.create(authProvider); + WebAuthenticationHandler basicAuthHandler = BasicAuthHandler.create(authProvider); } public void example38(Vertx vertx, AuthenticationProvider authProvider, Router router) { router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx))); - AuthenticationHandler basicAuthHandler = BasicAuthHandler.create(authProvider); + WebAuthenticationHandler basicAuthHandler = BasicAuthHandler.create(authProvider); // All requests to paths starting with '/private/' will be protected router.route("/private/*").handler(basicAuthHandler); @@ -1102,7 +1102,7 @@ public void example48(Vertx vertx, AuthenticationProvider authProvider) { router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx))); - AuthenticationHandler basicAuthHandler = BasicAuthHandler.create(authProvider); + WebAuthenticationHandler basicAuthHandler = BasicAuthHandler.create(authProvider); router.route("/eventbus/*").handler(basicAuthHandler); @@ -1813,7 +1813,7 @@ public void example77(Vertx vertx, Router router) { router.allowForward(AllowForwardHeaders.NONE); } - public void example78(Router router, AuthenticationHandler authNHandlerA, AuthenticationHandler authNHandlerB, AuthenticationHandler authNHandlerC) { + public void example78(Router router, WebAuthenticationHandler authNHandlerA, WebAuthenticationHandler authNHandlerB, WebAuthenticationHandler authNHandlerC) { // Chain will verify (A Or (B And C)) ChainAuthHandler chain = diff --git a/vertx-web/src/main/java/io/vertx/ext/web/RoutingContext.java b/vertx-web/src/main/java/io/vertx/ext/web/RoutingContext.java index f029f2fc76..d860ac2fa7 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/RoutingContext.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/RoutingContext.java @@ -24,6 +24,8 @@ import io.vertx.core.impl.ContextInternal; import io.vertx.core.json.EncodeException; import io.vertx.core.json.Json; +import io.vertx.ext.auth.common.AuthenticationContext; +import io.vertx.ext.auth.common.UserContext; import io.vertx.ext.web.impl.ParsableMIMEValue; import io.vertx.ext.web.impl.Utils; @@ -55,7 +57,7 @@ * @author Tim Fox */ @VertxGen -public interface RoutingContext { +public interface RoutingContext extends AuthenticationContext { /** * @return the HTTP request object diff --git a/vertx-web/src/main/java/io/vertx/ext/web/Session.java b/vertx-web/src/main/java/io/vertx/ext/web/Session.java index 6eecd038c1..498f2265e5 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/Session.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/Session.java @@ -16,141 +16,12 @@ package io.vertx.ext.web; -import io.vertx.codegen.annotations.Fluent; -import io.vertx.codegen.annotations.GenIgnore; import io.vertx.codegen.annotations.VertxGen; -import java.util.Map; -import java.util.function.Function; - /** - * Represents a browser session. - *

- * Sessions persist between HTTP requests for a single browser session. They are deleted when the browser is closed, or - * they time-out. Session cookies are used to maintain sessions using a secure UUID. - *

- * Sessions can be used to maintain data for a browser session, e.g. a shopping basket. - *

- * The context must have first been routed to a {@link io.vertx.ext.web.handler.SessionHandler} - * for sessions to be available. - * - * @author Tim Fox + * @see io.vertx.ext.auth.common.Session */ @VertxGen -public interface Session { - - /** - * @return The new unique ID of the session. - */ - Session regenerateId(); - - /** - * @return The unique ID of the session. This is generated using a random secure UUID. - */ - String id(); - - /** - * Put some data in a session - * - * @param key the key for the data - * @param obj the data - * @return a reference to this, so the API can be used fluently - */ - @Fluent - Session put(String key, Object obj); - - /** - * Put some data in a session if absent - * - * @param key the key for the data - * @param obj the data - * @return a reference to this, so the API can be used fluently - */ - @Fluent - Session putIfAbsent(String key, Object obj); - - /** - * Put some data in a session if absent. - * - * If the specified key is not already associated with a value (or is mapped - * to {@code null}), attempts to compute its value using the given mapping - * function and enters it into this map unless {@code null}. - * - * @param key the key for the data - * @param mappingFunction a mapping function - * @return a reference to this, so the API can be used fluently - */ - @Fluent - Session computeIfAbsent(String key, Function mappingFunction); - - /** - * Get some data from the session - * - * @param key the key of the data - * @return the data - */ - T get(String key); - - /** - * Remove some data from the session - * - * @param key the key of the data - * @return the data that was there or null if none there - */ - T remove(String key); - - /** - * @return the session data as a map - */ - @GenIgnore(GenIgnore.PERMITTED_TYPE) - Map data(); - - /** - * @return true if the session has data - */ - boolean isEmpty(); - - /** - * @return the time the session was last accessed - */ - long lastAccessed(); - - /** - * Destroy the session - */ - void destroy(); - - /** - * @return has the session been destroyed? - */ - boolean isDestroyed(); - - /** - * @return has the session been renewed? - */ - boolean isRegenerated(); - - /** - * @return old ID if renewed - */ - String oldId(); - - /** - * @return the amount of time in ms, after which the session will expire, if not accessed. - */ - long timeout(); - - /** - * Mark the session as being accessed. - */ - void setAccessed(); +public interface Session extends io.vertx.ext.auth.common.Session { - /** - * The short representation of the session to be added to the session cookie. By default is the session id. - * - * @return short representation string. - */ - default String value() { - return id(); - } } diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/APIKeyHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/APIKeyHandler.java index f3ddb93411..1c29957383 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/APIKeyHandler.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/APIKeyHandler.java @@ -33,7 +33,7 @@ * @author Paulo Lopes */ @VertxGen -public interface APIKeyHandler extends AuthenticationHandler { +public interface APIKeyHandler extends WebAuthenticationHandler { /** * Create an API Key authentication handler diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/BasicAuthHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/BasicAuthHandler.java index 93e09d5ab2..44e8ccece5 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/BasicAuthHandler.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/BasicAuthHandler.java @@ -26,7 +26,7 @@ * @author Tim Fox */ @VertxGen -public interface BasicAuthHandler extends AuthenticationHandler { +public interface BasicAuthHandler extends WebAuthenticationHandler { /** * The default realm to use diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/ChainAuthHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/ChainAuthHandler.java index a9f0c2b017..8422800839 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/ChainAuthHandler.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/ChainAuthHandler.java @@ -25,7 +25,7 @@ * @author Paulo Lopes */ @VertxGen -public interface ChainAuthHandler extends AuthenticationHandler { +public interface ChainAuthHandler extends WebAuthenticationHandler { /** * Create a chain authentication handler that will assert that all handlers pass the verification. @@ -51,5 +51,5 @@ static ChainAuthHandler any() { * */ @Fluent - ChainAuthHandler add(AuthenticationHandler other); + ChainAuthHandler add(WebAuthenticationHandler other); } diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/DigestAuthHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/DigestAuthHandler.java index 5a7625e349..8d354f5453 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/DigestAuthHandler.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/DigestAuthHandler.java @@ -27,7 +27,7 @@ * @author Paulo Lopes */ @VertxGen -public interface DigestAuthHandler extends AuthenticationHandler { +public interface DigestAuthHandler extends WebAuthenticationHandler { /** * The default nonce expire timeout to use in milliseconds. diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/FormLoginHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/FormLoginHandler.java index 1a2b97bb3c..7250368f58 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/FormLoginHandler.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/FormLoginHandler.java @@ -29,7 +29,7 @@ * @author Tim Fox */ @VertxGen -public interface FormLoginHandler extends AuthenticationHandler { +public interface FormLoginHandler extends WebAuthenticationHandler { /** * The default value of the form attribute which will contain the username diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/JWTAuthHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/JWTAuthHandler.java index 70cc1172c5..bb880fe639 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/JWTAuthHandler.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/JWTAuthHandler.java @@ -16,26 +16,25 @@ package io.vertx.ext.web.handler; -import io.vertx.codegen.annotations.Fluent; import io.vertx.codegen.annotations.VertxGen; import io.vertx.ext.auth.jwt.JWTAuth; +import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.impl.JWTAuthHandlerImpl; -import java.util.List; - /** * An auth handler that provides JWT Authentication support. * * @author Paulo Lopes */ @VertxGen -public interface JWTAuthHandler extends AuthenticationHandler { +// TODO this will alter the signature of the interface. Originally only the impl did implement ScopedAuthentication - now we also expose it via the interface. +public interface JWTAuthHandler extends WebAuthenticationHandler, io.vertx.ext.auth.jwt.JWTAuthHandler, io.vertx.ext.web.handler.impl.ScopedAuthentication> { /** - * Create a JWT auth handler. When no scopes are explicit declared, the default scopes will be looked up from the - * route metadata. + * Create a JWT auth handler. When no scopes are explicit declared, the default scopes will be looked up from the route metadata. * - * @param authProvider the auth provider to use + * @param authProvider + * the auth provider to use * @return the auth handler */ static JWTAuthHandler create(JWTAuth authProvider) { @@ -43,42 +42,14 @@ static JWTAuthHandler create(JWTAuth authProvider) { } /** - * Create a JWT auth handler. When no scopes are explicit declared, the default scopes will be looked up from the - * route metadata. + * Create a JWT auth handler. When no scopes are explicit declared, the default scopes will be looked up from the route metadata. * - * @param authProvider the auth provider to use + * @param authProvider + * the auth provider to use * @return the auth handler */ static JWTAuthHandler create(JWTAuth authProvider, String realm) { return new JWTAuthHandlerImpl(authProvider, realm); } - /** - * Set the scope delimiter. By default this is a space character. - * - * @param delimiter scope delimiter. - * @return fluent self. - */ - @Fluent - JWTAuthHandler scopeDelimiter(String delimiter); - - /** - * Return a new instance with the internal state copied from the caller but the scopes to be requested during a token - * request are unique to the instance. When scopes are applied to the handler, the default scopes from the route - * metadata will be ignored. - * - * @param scope scope. - * @return new instance of this interface. - */ - JWTAuthHandler withScope(String scope); - - /** - * Return a new instance with the internal state copied from the caller but the scopes to be requested during a token - * request are unique to the instance. When scopes are applied to the handler, the default scopes from the route - * metadata will be ignored. - * - * @param scopes scopes. - * @return new instance of this interface. - */ - JWTAuthHandler withScopes(List scopes); } diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/OAuth2AuthHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/OAuth2AuthHandler.java index 2f84394442..f9a8b2ebca 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/OAuth2AuthHandler.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/OAuth2AuthHandler.java @@ -19,12 +19,11 @@ import io.vertx.codegen.annotations.Fluent; import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; import io.vertx.ext.auth.oauth2.OAuth2Auth; import io.vertx.ext.web.Route; +import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.impl.OAuth2AuthHandlerImpl; - -import java.util.List; +import io.vertx.ext.web.impl.OrderListener; /** * An auth handler that provides OAuth2 Authentication support. This handler is suitable for AuthCode flows. @@ -32,7 +31,7 @@ * @author Paulo Lopes */ @VertxGen -public interface OAuth2AuthHandler extends AuthenticationHandler { +public interface OAuth2AuthHandler extends WebAuthenticationHandler, io.vertx.ext.auth.oauth2.OAuth2AuthHandler, io.vertx.ext.web.handler.impl.ScopedAuthentication>, OrderListener { /** * Create a OAuth2 auth handler with host pinning. When no scopes are explicit declared, the default scopes will be @@ -66,74 +65,6 @@ static OAuth2AuthHandler create(Vertx vertx, OAuth2Auth authProvider) { return new OAuth2AuthHandlerImpl(vertx, authProvider, null); } - /** - * Extra parameters needed to be passed while requesting a token. - * - * @param extraParams extra optional parameters. - * @return self - */ - @Fluent - OAuth2AuthHandler extraParams(JsonObject extraParams); - - /** - * Return a new instance with the internal state copied from the caller but the scopes to be requested during - * a token request are unique to the instance. When scopes are applied to the handler, the default scopes from the - * route metadata will be ignored. - * - * @param scope scope. - * @return new instance of this interface. - */ - @Fluent - OAuth2AuthHandler withScope(String scope); - - /** - * Return a new instance with the internal state copied from the caller but the scopes to be requested during - * a token request are unique to the instance. When scopes are applied to the handler, the default scopes from the - * route metadata will be ignored. - * - * @param scopes scopes. - * @return new instance of this interface. - */ - @Fluent - OAuth2AuthHandler withScopes(List scopes); - - /** - * Indicates the type of user interaction that is required. Not all providers support this or the full list. - * - * Well known values are: - * - *

    - *
  • login will force the user to enter their credentials on that request, negating single-sign on.
  • - *
  • none is the opposite - it will ensure that the user isn't presented with any interactive prompt - * whatsoever. If the request can't be completed silently via single-sign on, the Microsoft identity platform - * endpoint will return an interaction_required error.
  • - *
  • consent will trigger the OAuth consent dialog after the user signs in, asking the user to grant - * permissions to the app.
  • - *
  • select_account will interrupt single sign-on providing account selection experience listing all the - * accounts either in session or any remembered account or an option to choose to use a different account - * altogether.
  • - *
  • - *
- * - * @param prompt the prompt choice. - * @return self - */ - @Fluent - OAuth2AuthHandler prompt(String prompt); - - /** - * PKCE (RFC 7636) is an extension to the Authorization Code flow to prevent several attacks and to be able to - * securely perform the OAuth exchange from public clients. - * - * It was originally designed to protect mobile apps, but its ability to prevent authorization code injection - * makes it useful for every OAuth client, even web apps that use a client secret. - * - * @param length A number between 43 and 128. Or -1 to disable. - * @return self - */ - @Fluent - OAuth2AuthHandler pkceVerifierLength(int length); - /** * add the callback handler to a given route. * @param route a given route e.g.: {@code /callback} @@ -141,4 +72,5 @@ static OAuth2AuthHandler create(Vertx vertx, OAuth2Auth authProvider) { */ @Fluent OAuth2AuthHandler setupCallback(Route route); + } diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/OtpAuthHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/OtpAuthHandler.java index ac0c4f4753..8f8917ddfa 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/OtpAuthHandler.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/OtpAuthHandler.java @@ -22,7 +22,6 @@ import io.vertx.ext.auth.otp.totp.TotpAuth; import io.vertx.ext.web.Route; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.UserContext; import io.vertx.ext.web.handler.impl.HotpAuthHandlerImpl; import io.vertx.ext.web.handler.impl.TotpAuthHandlerImpl; @@ -32,7 +31,7 @@ * @author Paulo Lopes */ @VertxGen -public interface OtpAuthHandler extends AuthenticationHandler { +public interface OtpAuthHandler extends WebAuthenticationHandler { /** * Create a new instance of this handler using a time based one time password authentication provider. diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/RedirectAuthHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/RedirectAuthHandler.java index 9b2b4f0278..f92192a6b4 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/RedirectAuthHandler.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/RedirectAuthHandler.java @@ -26,7 +26,7 @@ * @author Tim Fox */ @VertxGen -public interface RedirectAuthHandler extends AuthenticationHandler { +public interface RedirectAuthHandler extends WebAuthenticationHandler { /** * Default path the user will be redirected to diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/SimpleAuthenticationHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/SimpleAuthenticationHandler.java index 32b5b31312..10c57a915e 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/SimpleAuthenticationHandler.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/SimpleAuthenticationHandler.java @@ -37,7 +37,7 @@ * @author Paulo Lopes */ @VertxGen -public interface SimpleAuthenticationHandler extends AuthenticationHandler { +public interface SimpleAuthenticationHandler extends WebAuthenticationHandler { /** * Creates a new instance of the simple authentication handler. diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/WebAuthenticationHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/WebAuthenticationHandler.java new file mode 100644 index 0000000000..bc392465e9 --- /dev/null +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/WebAuthenticationHandler.java @@ -0,0 +1,20 @@ +package io.vertx.ext.web.handler; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.ext.auth.common.AuthenticationHandler; +import io.vertx.ext.web.RoutingContext; + +/** + * Base interface for Vert.x Web authentication handlers. + *

+ * An auth handler allows your application to provide authentication support. + *

+ * An Auth handler may require a {@link SessionHandler} to be on the routing chain before it. + * + * @author Tim Fox + * @author Paulo Lopes + */ +@VertxGen(concrete = false) +public interface WebAuthenticationHandler extends AuthenticationHandler { + +} diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/WebAuthnHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/WebAuthnHandler.java index 82b869399c..f8dc090a1d 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/WebAuthnHandler.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/WebAuthnHandler.java @@ -27,7 +27,7 @@ * @author Paulo Lopes */ @VertxGen -public interface WebAuthnHandler extends AuthenticationHandler { +public interface WebAuthnHandler extends WebAuthenticationHandler { /** * Create a WebAuthN auth handler. This handler expects at least the response callback to be installed. diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/APIKeyHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/APIKeyHandlerImpl.java index 65cb6d4147..e408b1370f 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/APIKeyHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/APIKeyHandlerImpl.java @@ -30,10 +30,12 @@ import java.util.function.Function; +import static io.vertx.ext.web.handler.HttpException.UNAUTHORIZED; + /** * @author Paulo Lopes */ -public class APIKeyHandlerImpl extends AuthenticationHandlerImpl implements APIKeyHandler { +public class APIKeyHandlerImpl extends WebAuthenticationHandlerImpl implements APIKeyHandler { enum Type { HEADER, diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/BasicAuthHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/BasicAuthHandlerImpl.java index 0aa184899e..894806d9b1 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/BasicAuthHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/BasicAuthHandlerImpl.java @@ -35,7 +35,7 @@ * @author Paulo Lopes * @author Tim Fox */ -public class BasicAuthHandlerImpl extends HTTPAuthorizationHandler implements BasicAuthHandler { +public class BasicAuthHandlerImpl extends WebHTTPAuthorizationHandler implements BasicAuthHandler { public BasicAuthHandlerImpl(AuthenticationProvider authProvider, String realm) { super(authProvider, Type.BASIC, realm); diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/ChainAuthHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/ChainAuthHandlerImpl.java index bb85a93303..db49bfb04d 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/ChainAuthHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/ChainAuthHandlerImpl.java @@ -8,17 +8,19 @@ import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.ext.auth.User; import io.vertx.ext.auth.authentication.AuthenticationProvider; +import io.vertx.ext.auth.common.handler.AuthenticationHandlerInternal; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; import io.vertx.ext.web.handler.*; import java.util.ArrayList; import java.util.List; -public class ChainAuthHandlerImpl extends AuthenticationHandlerImpl implements ChainAuthHandler { +public class ChainAuthHandlerImpl extends WebAuthenticationHandlerImpl implements ChainAuthHandler { private static final Logger LOG = LoggerFactory.getLogger(ChainAuthHandler.class); - private final List handlers = new ArrayList<>(); + private final List> handlers = new ArrayList<>(); private final boolean all; private int willRedirect = -1; @@ -34,11 +36,11 @@ public boolean performsRedirect() { } @Override - public synchronized ChainAuthHandler add(AuthenticationHandler other) { + public synchronized ChainAuthHandler add(WebAuthenticationHandler other) { if (performsRedirect()) { throw new IllegalStateException("Cannot add a handler after a handler known to perform a HTTP redirect: " + handlers.get(willRedirect)); } - final AuthenticationHandlerInternal otherInternal = (AuthenticationHandlerInternal) other; + final AuthenticationHandlerInternal otherInternal = (AuthenticationHandlerInternal) other; // control if we should not allow more handlers due to the possibility of a redirect to happen if (otherInternal.performsRedirect()) { willRedirect = handlers.size(); @@ -77,7 +79,7 @@ private void iterate(final int idx, final RoutingContext ctx, User result, Throw } // parse the request in order to extract the credentials object - final AuthenticationHandlerInternal authHandler = handlers.get(idx); + final AuthenticationHandlerInternal authHandler = handlers.get(idx); authHandler .authenticate(ctx) @@ -120,7 +122,7 @@ private void iterate(final int idx, final RoutingContext ctx, User result, Throw @Override public boolean setAuthenticateHeader(RoutingContext ctx) { boolean added = false; - for (AuthenticationHandlerInternal authHandler : handlers) { + for (AuthenticationHandlerInternal authHandler : handlers) { if (all && added) { // we can only allow 1 header in this case, // otherwise we tell the user agent to pick the strongest, @@ -132,4 +134,5 @@ public boolean setAuthenticateHeader(RoutingContext ctx) { } return added; } + } diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/DigestAuthHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/DigestAuthHandlerImpl.java index 8dc9ee3e70..b743763a20 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/DigestAuthHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/DigestAuthHandlerImpl.java @@ -26,6 +26,7 @@ import io.vertx.ext.auth.VertxContextPRNG; import io.vertx.ext.auth.audit.Marker; import io.vertx.ext.auth.audit.SecurityAudit; +import io.vertx.ext.auth.common.handler.impl.HTTPAuthorizationHandler; import io.vertx.ext.auth.htdigest.HtdigestAuth; import io.vertx.ext.auth.htdigest.HtdigestCredentials; import io.vertx.ext.web.RoutingContext; @@ -42,11 +43,12 @@ import java.util.regex.Pattern; import static io.vertx.ext.auth.impl.Codec.base16Encode; +import static io.vertx.ext.web.handler.HttpException.UNAUTHORIZED; /** * @author Paulo Lopes */ -public class DigestAuthHandlerImpl extends HTTPAuthorizationHandler implements DigestAuthHandler { +public class DigestAuthHandlerImpl extends WebHTTPAuthorizationHandler implements DigestAuthHandler { private final static Logger LOG = LoggerFactory.getLogger(HTTPAuthorizationHandler.class); diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/FormLoginHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/FormLoginHandlerImpl.java index 428df66c1c..48d7f15fee 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/FormLoginHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/FormLoginHandlerImpl.java @@ -32,10 +32,13 @@ import io.vertx.ext.web.handler.HttpException; import io.vertx.ext.web.impl.RoutingContextInternal; +import static io.vertx.ext.web.handler.HttpException.BAD_METHOD; +import static io.vertx.ext.web.handler.HttpException.BAD_REQUEST; + /** * @author Tim Fox */ -public class FormLoginHandlerImpl extends AuthenticationHandlerImpl implements FormLoginHandler { +public class FormLoginHandlerImpl extends WebAuthenticationHandlerImpl implements FormLoginHandler { private String usernameParam; private String passwordParam; @@ -104,7 +107,7 @@ public Future authenticate(RoutingContext context) { } @Override - public void postAuthentication(RoutingContext ctx) { + public void postAuthentication(RoutingContext ctx, User user) { HttpServerRequest req = ctx.request(); Session session = ctx.session(); if (session != null) { diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/HotpAuthHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/HotpAuthHandlerImpl.java index 215c96ea09..e40c3915cb 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/HotpAuthHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/HotpAuthHandlerImpl.java @@ -36,7 +36,7 @@ /** * @author Paulo Lopes */ -public class HotpAuthHandlerImpl extends AuthenticationHandlerImpl implements OtpAuthHandler, OrderListener { +public class HotpAuthHandlerImpl extends WebAuthenticationHandlerImpl implements OtpAuthHandler, OrderListener { private final OtpKeyGenerator otpKeyGen; diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/JWTAuthHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/JWTAuthHandlerImpl.java index 5550c85015..67c9318a8a 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/JWTAuthHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/JWTAuthHandlerImpl.java @@ -16,78 +16,29 @@ package io.vertx.ext.web.handler.impl; -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.auth.User; -import io.vertx.ext.auth.audit.Marker; -import io.vertx.ext.auth.audit.SecurityAudit; -import io.vertx.ext.auth.authentication.TokenCredentials; +import java.util.ArrayList; + +import java.util.List; +import java.util.Objects; + +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.auth.jwt.AbstractJWTHandler; import io.vertx.ext.auth.jwt.JWTAuth; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.HttpException; import io.vertx.ext.web.handler.JWTAuthHandler; -import io.vertx.ext.web.impl.RoutingContextInternal; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; /** * @author Paulo Lopes */ -public class JWTAuthHandlerImpl extends HTTPAuthorizationHandler implements JWTAuthHandler, ScopedAuthentication { - - private final List scopes; - private String delimiter; +public class JWTAuthHandlerImpl extends AbstractJWTHandler implements JWTAuthHandler { public JWTAuthHandlerImpl(JWTAuth authProvider, String realm) { super(authProvider, Type.BEARER, realm); - scopes = Collections.emptyList(); - this.delimiter = " "; } private JWTAuthHandlerImpl(JWTAuthHandlerImpl base, List scopes, String delimiter) { - super(base.authProvider, Type.BEARER, base.realm); - Objects.requireNonNull(scopes, "scopes cannot be null"); - this.scopes = scopes; - Objects.requireNonNull(delimiter, "delimiter cannot be null"); - this.delimiter = delimiter; - } - - @Override - public Future authenticate(RoutingContext context) { - - return parseAuthorization(context) - .compose(token -> { - int segments = 0; - for (int i = 0; i < token.length(); i++) { - char c = token.charAt(i); - if (c == '.') { - if (++segments == 3) { - return Future.failedFuture(new HttpException(400, "Too many segments in token")); - } - continue; - } - if (Character.isLetterOrDigit(c) || c == '-' || c == '_') { - continue; - } - // invalid character - return Future.failedFuture(new HttpException(400, "Invalid character in token: " + (int) c)); - } - - final TokenCredentials credentials = new TokenCredentials(token); - final SecurityAudit audit = ((RoutingContextInternal) context).securityAudit(); - audit.credentials(credentials); - - return - authProvider - .authenticate(new TokenCredentials(token)) - .andThen(op -> audit.audit(Marker.AUTHENTICATION, op.succeeded())) - .recover(err -> Future.failedFuture(new HttpException(401, err))); - }); + super(base.authProvider, scopes, delimiter, base.realm); } @Override @@ -111,51 +62,41 @@ public JWTAuthHandler scopeDelimiter(String delimiter) { return this; } + // TODO remove duplicated code from WebAuthenticationHandlerImpl /** - * The default behavior for post-authentication + * This method is protected so custom auth handlers can override the default error handling */ - @Override - public void postAuthentication(RoutingContext ctx) { - final User user = ctx.user().get(); - if (user == null) { - // bad state - ctx.fail(403, new IllegalStateException("no user in the context")); - return; - } - // the user is authenticated, however the user may not have all the required scopes - final List scopes = getScopesOrSearchMetadata(this.scopes, ctx); - - if (scopes.size() > 0) { - final JsonObject jwt = user.get("accessToken"); - if (jwt == null) { - ctx.fail(403, new IllegalStateException("Invalid JWT: null")); - return; - } - - if (jwt.getValue("scope") == null) { - ctx.fail(403, new IllegalStateException("Invalid JWT: scope claim is required")); - return; - } - - List target; - if (jwt.getValue("scope") instanceof String) { - target = - Stream.of(jwt.getString("scope") - .split(delimiter)) - .collect(Collectors.toList()); - } else { - target = jwt.getJsonArray("scope").getList(); - } - - if (target != null) { - for (String scope : scopes) { - if (!target.contains(scope)) { - ctx.fail(403, new IllegalStateException("JWT scopes != handler scopes")); - return; + protected void processException(RoutingContext ctx, Throwable exception) { + if (exception != null) { + if (exception instanceof HttpException) { + final int statusCode = ((HttpException) exception).getStatusCode(); + final String payload = ((HttpException) exception).getPayload(); + + switch (statusCode) { + case 302: + ctx.response() + .putHeader(HttpHeaders.LOCATION, payload) + .setStatusCode(302) + .end("Redirecting to " + payload + "."); + return; + case 401: + if (!"XMLHttpRequest".equals(ctx.request().getHeader("X-Requested-With"))) { + setAuthenticateHeader(ctx); } + ctx.fail(401, exception); + return; + default: + ctx.fail(statusCode, exception); + return; } } } - ctx.next(); + + // fallback 500 + ctx.fail(exception); + } + + protected void fail(RoutingContext ctx, int code, String msg) { + ctx.fail(code, new IllegalStateException(msg)); } } diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/OAuth2AuthHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/OAuth2AuthHandlerImpl.java index 62c8bfc7f6..95b4a50a6e 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/OAuth2AuthHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/OAuth2AuthHandlerImpl.java @@ -16,57 +16,36 @@ package io.vertx.ext.web.handler.impl; -import io.vertx.core.Future; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.auth.User; -import io.vertx.ext.auth.VertxContextPRNG; import io.vertx.ext.auth.audit.Marker; import io.vertx.ext.auth.audit.SecurityAudit; -import io.vertx.ext.auth.authentication.Credentials; -import io.vertx.ext.auth.authentication.TokenCredentials; -import io.vertx.ext.auth.impl.Codec; +import io.vertx.ext.auth.common.AuthenticationContextInternal; +import io.vertx.ext.auth.common.Session; +import io.vertx.ext.auth.common.UserContextInternal; +import io.vertx.ext.auth.oauth2.AbstractOAuth2Handler; import io.vertx.ext.auth.oauth2.OAuth2Auth; -import io.vertx.ext.auth.oauth2.OAuth2AuthorizationURL; import io.vertx.ext.auth.oauth2.OAuth2FlowType; import io.vertx.ext.auth.oauth2.Oauth2Credentials; import io.vertx.ext.web.Route; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.Session; import io.vertx.ext.web.handler.HttpException; import io.vertx.ext.web.handler.OAuth2AuthHandler; -import io.vertx.ext.web.impl.OrderListener; -import io.vertx.ext.web.impl.Origin; -import io.vertx.ext.web.impl.RoutingContextInternal; -import io.vertx.ext.web.impl.UserContextInternal; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.*; /** * @author Paulo Lopes */ -public class OAuth2AuthHandlerImpl extends HTTPAuthorizationHandler implements OAuth2AuthHandler, ScopedAuthentication, OrderListener { +public class OAuth2AuthHandlerImpl extends AbstractOAuth2Handler implements OAuth2AuthHandler { private static final Logger LOG = LoggerFactory.getLogger(OAuth2AuthHandlerImpl.class); - private final VertxContextPRNG prng; - private final Origin callbackURL; - private final MessageDigest sha256; - - private final List scopes; - private JsonObject extraParams; - private String prompt; - private int pkce = -1; - // explicit signal that tokens are handled as bearer only (meaning, no backend server known) - private boolean bearerOnly = true; - private int order = -1; private Route callback; @@ -75,179 +54,13 @@ public OAuth2AuthHandlerImpl(Vertx vertx, OAuth2Auth authProvider, String callba } public OAuth2AuthHandlerImpl(Vertx vertx, OAuth2Auth authProvider, String callbackURL, String realm) { - super(authProvider, Type.BEARER, realm); - // get a reference to the prng - this.prng = VertxContextPRNG.current(vertx); - // get a reference to the sha-256 digest - try { - sha256 = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Cannot get instance of SHA-256 MessageDigest", e); - } - // process callback - if (callbackURL != null) { - this.callbackURL = Origin.parse(callbackURL); - } else { - this.callbackURL = null; - } - // scopes are empty by default - this.scopes = Collections.emptyList(); + super(vertx, authProvider, callbackURL, realm); } private OAuth2AuthHandlerImpl(OAuth2AuthHandlerImpl base, List scopes) { - super(base.authProvider, Type.BEARER, base.realm); - this.prng = base.prng; - this.callbackURL = base.callbackURL; - this.prompt = base.prompt; - this.pkce = base.pkce; - this.bearerOnly = base.bearerOnly; - - // get a new reference to the sha-256 digest - try { - sha256 = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Cannot get instance of SHA-256 MessageDigest", e); - } - // state copy - if (base.extraParams != null) { - extraParams = base.extraParams.copy(); - } + super(base, scopes); this.callback = base.callback; this.order = base.order; - // apply the new scopes - Objects.requireNonNull(scopes, "scopes cannot be null"); - this.scopes = scopes; - } - - @Override - public Future authenticate(RoutingContext context) { - // when the handler is working as bearer only, then the `Authorization` header is required - return parseAuthorization(context, !bearerOnly) - .compose(token -> { - // Authorization header can be null when in not in bearerOnly mode - if (token == null) { - // redirect request to the oauth2 server as we know nothing about this request - if (bearerOnly) { - // it's a failure both cases but the cause is not the same - return Future.failedFuture("callback route is not configured."); - } - // when this handle is mounted as a catch all, the callback route must be configured before, - // as it would shade the callback route. When a request matches the callback path and has the - // method GET the exceptional case should not redirect to the oauth2 server as it would become - // an infinite redirect loop. In this case an exception must be raised. - if (context.request().method() == HttpMethod.GET && context.normalizedPath().equals(callbackURL.resource())) { - LOG.warn("The callback route is shaded by the OAuth2AuthHandler, ensure the callback route is added BEFORE the OAuth2AuthHandler route!"); - return Future.failedFuture(new HttpException(500, "Infinite redirect loop [oauth2 callback]")); - } else { - if (context.request().method() != HttpMethod.GET) { - // we can only redirect GET requests - LOG.error("OAuth2 redirect attempt to non GET resource"); - return Future.failedFuture(new HttpException(405, new IllegalStateException("OAuth2 redirect attempt to non GET resource"))); - } - - // the redirect is processed as a failure to abort the chain - String redirectUri = context.request().uri(); - try { - return Future.failedFuture(new HttpException(302, authURI(context, redirectUri))); - } catch (IllegalStateException e) { - return Future.failedFuture(e); - } - } - } else { - // continue - final List scopes = getScopesOrSearchMetadata(this.scopes, context); - - final Credentials credentials = - scopes.size() > 0 ? new TokenCredentials(token).setScopes(scopes) : new TokenCredentials(token); - - final SecurityAudit audit = ((RoutingContextInternal) context).securityAudit(); - audit.credentials(credentials); - - return authProvider.authenticate(credentials) - .andThen(op -> audit.audit(Marker.AUTHENTICATION, op.succeeded())) - .recover(err -> Future.failedFuture(new HttpException(401, err))); - } - }); - } - - private String authURI(RoutingContext context, String redirectURL) { - - String state = null; - String codeVerifier = null; - String loginHint = null; - - final Session session = context.session(); - - if (session == null) { - if (pkce > 0) { - // we can only handle PKCE with a session - throw new IllegalStateException("OAuth2 PKCE requires a session to be present"); - } - } else { - // there's a session we can make this request comply to the Oauth2 spec and add an opaque state - - loginHint = session.get("login_hint"); - // hint will be considered at least once - session.remove("login_hint"); - - session - .put("redirect_uri", redirectURL); - - // create a state value to mitigate replay attacks - state = prng.nextString(6); - // store the state in the session - session - .put("state", state); - - if (pkce > 0) { - codeVerifier = prng.nextString(pkce); - // store the code verifier in the session - session - .put("pkce", codeVerifier); - } - } - - final OAuth2AuthorizationURL config = new OAuth2AuthorizationURL(); - - if (extraParams != null) { - for (Map.Entry entry : extraParams) { - if (entry.getValue() != null) { - config.putAdditionalParameter(entry.getKey(), entry.getValue().toString()); - } - } - } - - config - .setState(state != null ? state : redirectURL) - .setLoginHint(loginHint) - .setPrompt(prompt); - - if (callbackURL != null) { - config.setRedirectUri(callbackURL.href()); - } - - final List scopes = getScopesOrSearchMetadata(this.scopes, context); - - if (scopes.size() > 0) { - config.setScopes(scopes); - } - - if (codeVerifier != null) { - synchronized (sha256) { - sha256.update(codeVerifier.getBytes(StandardCharsets.US_ASCII)); - config - .setCodeChallenge(Codec.base64UrlEncode(sha256.digest())) - .setCodeChallengeMethod("S256"); - } - } - - return authProvider.authorizeURL(new OAuth2AuthorizationURL(config)); - } - - @Override - public OAuth2AuthHandler extraParams(JsonObject extraParams) { - this.extraParams = extraParams; - return this; } @Override @@ -264,25 +77,7 @@ public OAuth2AuthHandler withScopes(List scopes) { Objects.requireNonNull(scopes, "scopes cannot be null"); return new OAuth2AuthHandlerImpl(this, scopes); } - - @Override - public OAuth2AuthHandler prompt(String prompt) { - this.prompt = prompt; - return this; - } - - @Override - public OAuth2AuthHandler pkceVerifierLength(int length) { - if (length >= 0) { - // requires verification - if (length < 43 || length > 128) { - throw new IllegalArgumentException("Length must be between 34 and 128"); - } - } - this.pkce = length; - return this; - } - + @Override public OAuth2AuthHandler setupCallback(final Route route) { @@ -320,85 +115,6 @@ public OAuth2AuthHandler setupCallback(final Route route) { return this; } - private static final Set OPENID_SCOPES = new HashSet<>(); - - static { - OPENID_SCOPES.add("openid"); - OPENID_SCOPES.add("profile"); - OPENID_SCOPES.add("email"); - OPENID_SCOPES.add("phone"); - OPENID_SCOPES.add("offline"); - } - - /** - * The default behavior for post-authentication - */ - @Override - public void postAuthentication(RoutingContext ctx) { - // the user is authenticated, however the user may not have all the required scopes - final List scopes = getScopesOrSearchMetadata(this.scopes, ctx); - - if (scopes.size() > 0) { - final User user = ctx.user().get(); - if (user == null) { - // bad state - ctx.fail(403, new IllegalStateException("no user in the context")); - return; - } - - if (user.principal().containsKey("scope")) { - final String userScopes = user.principal().getString("scope"); - if (userScopes != null) { - // user principal contains scope, a basic assertion is required to ensure that - // the scopes present match the required ones - - // check if openid is active - final boolean openId = userScopes.contains("openid"); - - for (String scope : scopes) { - // do not assert openid scopes if openid is active - if (openId && OPENID_SCOPES.contains(scope)) { - continue; - } - - int idx = userScopes.indexOf(scope); - if (idx != -1) { - // match, but is it valid? - if ( - (idx != 0 && userScopes.charAt(idx -1) != ' ') || - (idx + scope.length() != userScopes.length() && userScopes.charAt(idx + scope.length()) != ' ')) { - // invalid scope assignment - ctx.fail(403, new IllegalStateException("principal scope != handler scopes")); - return; - } - } else { - // invalid scope assignment - ctx.fail(403, new IllegalStateException("principal scope != handler scopes")); - return; - } - } - } - } - } - ctx.next(); - } - - @Override - public boolean performsRedirect() { - // depending on the time this method is invoked - // we can deduct with more accuracy if a redirect is possible or not - if (!bearerOnly) { - // we know that a redirect is definitely possible - // as the callback handler has been created - return true; - } else { - // the callback hasn't been mounted so we need to assume - // that if no callbackURL is provided, then there isn't - // a redirect happening in this application - return callbackURL != null; - } - } - @Override public void onOrder(int order) { // order isn't known yet, we can attempt to mount @@ -411,14 +127,7 @@ public void onOrder(int order) { } } - private void mountCallback() { - - callback - .method(HttpMethod.GET) - // we want the callback before this handler - .order(order - 1); - - callback.handler(ctx -> { + private void callbackHandler(RoutingContext ctx) { // Some IdP's (e.g.: AWS Cognito) returns errors as query arguments String error = ctx.request().getParam("error"); @@ -440,9 +149,9 @@ private void mountCallback() { String errorDescription = ctx.request().getParam("error_description"); if (errorDescription != null) { - ctx.fail(errorCode, new IllegalStateException(error + ": " + errorDescription)); + fail(ctx, errorCode, error + ": " + errorDescription); } else { - ctx.fail(errorCode, new IllegalStateException(error)); + fail(ctx, errorCode, error); } return; } @@ -452,7 +161,7 @@ private void mountCallback() { // code is a require value if (code == null) { - ctx.fail(400, new IllegalStateException("Missing code parameter")); + fail(ctx, 400, "Missing code parameter"); return; } @@ -468,7 +177,7 @@ private void mountCallback() { // state is a required field if (state == null) { - ctx.fail(400, new IllegalStateException("Missing IdP state parameter to the callback endpoint")); + fail(ctx, 400, "Missing IdP state parameter to the callback endpoint"); return; } @@ -482,7 +191,7 @@ private void mountCallback() { // if there's a state in the context they must match if (!state.equals(ctxState)) { // forbidden, the state is not valid (this is a replay attack) - ctx.fail(401, new IllegalStateException("Invalid oauth2 state")); + fail(ctx, 401, "Invalid oauth2 state"); return; } @@ -500,7 +209,7 @@ private void mountCallback() { // This must exactly match the redirect_uri passed to the authorization URL in the previous step. credentials.setRedirectUri(callbackURL.href()); - final SecurityAudit audit = ((RoutingContextInternal) ctx).securityAudit(); + final SecurityAudit audit = ((AuthenticationContextInternal) ctx).securityAudit(); audit.credentials(credentials); authProvider @@ -536,6 +245,55 @@ private void mountCallback() { .setStatusCode(302) .end("Redirecting to " + location + "."); }); - }); } + + private void mountCallback() { + + callback + .method(HttpMethod.GET) + // we want the callback before this handler + .order(order - 1); + + callback.handler(this::callbackHandler); + } + + +//TODO remove duplicated code from WebAuthenticationHandlerImpl + /** + * This method is protected so custom auth handlers can override the default error handling + */ + protected void processException(RoutingContext ctx, Throwable exception) { + if (exception != null) { + if (exception instanceof HttpException) { + final int statusCode = ((HttpException) exception).getStatusCode(); + final String payload = ((HttpException) exception).getPayload(); + + switch (statusCode) { + case 302: + ctx.response() + .putHeader(HttpHeaders.LOCATION, payload) + .setStatusCode(302) + .end("Redirecting to " + payload + "."); + return; + case 401: + if (!"XMLHttpRequest".equals(ctx.request().getHeader("X-Requested-With"))) { + setAuthenticateHeader(ctx); + } + ctx.fail(401, exception); + return; + default: + ctx.fail(statusCode, exception); + return; + } + } + } + + // fallback 500 + ctx.fail(exception); + } + + protected void fail(RoutingContext ctx, int code, String msg) { + ctx.fail(code, new IllegalStateException(msg)); + } + } diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/RedirectAuthHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/RedirectAuthHandlerImpl.java index 8e7728320a..29e1c6b17c 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/RedirectAuthHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/RedirectAuthHandlerImpl.java @@ -28,7 +28,7 @@ * @author Tim Fox * @author Paulo Lopes */ -public class RedirectAuthHandlerImpl extends AuthenticationHandlerImpl implements RedirectAuthHandler { +public class RedirectAuthHandlerImpl extends WebAuthenticationHandlerImpl implements RedirectAuthHandler { private final String loginRedirectURL; private final String returnURLParam; diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/ScopedAuthentication.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/ScopedAuthentication.java index cf03a03eed..fd45b82069 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/ScopedAuthentication.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/ScopedAuthentication.java @@ -1,44 +1,20 @@ package io.vertx.ext.web.handler.impl; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.AuthenticationHandler; - import java.util.Collections; import java.util.List; -import java.util.Map; + +import io.vertx.ext.auth.common.AuthenticationHandler; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.RoutingContext; /** - * Internal interface for scope aware Authentication handlers. - * + * @see io.vertx.ext.auth.common.ScopedAuthentication * @param - * @author Paulo Lopes */ -public interface ScopedAuthentication { - - /** - * Return a new instance with the internal state copied from the caller but the scopes to be requested during a token - * request are unique to the instance. - * - * @param scope scope. - * @return new instance of this interface. - */ - SELF withScope(String scope); - - /** - * Return a new instance with the internal state copied from the caller but the scopes to be requested during a token - * request are unique to the instance. - * - * @param scopes scopes. - * @return new instance of this interface. - */ - SELF withScopes(List scopes); +public interface ScopedAuthentication> + extends io.vertx.ext.auth.common.ScopedAuthentication { - /** - * Return the list of scopes provided as the 1st argument, unless the list is empty. In this case, the list of scopes - * is obtained from the routing context metadata if possible. In case the metadata is not available, the list of - * scopes is always an empty list. - */ + @Override default List getScopesOrSearchMetadata(List scopes, RoutingContext ctx) { if (!scopes.isEmpty()) { return scopes; diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/SessionHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/SessionHandlerImpl.java index 165032c541..31f72a8e2a 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/SessionHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/SessionHandlerImpl.java @@ -29,7 +29,7 @@ import io.vertx.ext.web.handler.SessionHandler; import io.vertx.ext.web.impl.RoutingContextInternal; import io.vertx.ext.web.impl.Signature; -import io.vertx.ext.web.impl.UserContextInternal; +import io.vertx.ext.auth.common.UserContextInternal; import io.vertx.ext.web.sstore.SessionStore; import io.vertx.ext.web.sstore.impl.SessionInternal; diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/SimpleAuthenticationHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/SimpleAuthenticationHandlerImpl.java index 286a46778e..3146cef6b6 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/SimpleAuthenticationHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/SimpleAuthenticationHandlerImpl.java @@ -11,7 +11,7 @@ import java.util.function.Function; -public class SimpleAuthenticationHandlerImpl extends AuthenticationHandlerImpl implements SimpleAuthenticationHandler { +public class SimpleAuthenticationHandlerImpl extends WebAuthenticationHandlerImpl implements SimpleAuthenticationHandler { private Function> authn; diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/TotpAuthHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/TotpAuthHandlerImpl.java index 03ca23e059..f8eebebdf7 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/TotpAuthHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/TotpAuthHandlerImpl.java @@ -33,10 +33,12 @@ import io.vertx.ext.web.impl.OrderListener; import io.vertx.ext.web.impl.RoutingContextInternal; +import static io.vertx.ext.web.handler.HttpException.UNAUTHORIZED; + /** * @author Paulo Lopes */ -public class TotpAuthHandlerImpl extends AuthenticationHandlerImpl implements OtpAuthHandler, OrderListener { +public class TotpAuthHandlerImpl extends WebAuthenticationHandlerImpl implements OtpAuthHandler, OrderListener { private final OtpKeyGenerator otpKeyGen; @@ -63,7 +65,7 @@ public Future authenticate(RoutingContext ctx) { final User user = ctx.user().get(); if (user == null) { - return Future.failedFuture(new HttpException(401)); + return Future.failedFuture(UNAUTHORIZED); } else { Boolean userOtp = user.get("mfa"); // user hasn't 2fa yet? diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/UserHolder.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/UserHolder.java index c56d5f3c8d..be201e5403 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/UserHolder.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/UserHolder.java @@ -21,7 +21,7 @@ import io.vertx.core.shareddata.ClusterSerializable; import io.vertx.ext.auth.User; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.impl.UserContextInternal; +import io.vertx.ext.auth.common.UserContextInternal; import io.vertx.ext.web.impl.Utils; import java.nio.charset.StandardCharsets; diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/WebAuthenticationHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/WebAuthenticationHandlerImpl.java new file mode 100644 index 0000000000..629fbd9039 --- /dev/null +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/WebAuthenticationHandlerImpl.java @@ -0,0 +1,53 @@ +package io.vertx.ext.web.handler.impl; + +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.auth.authentication.AuthenticationProvider; +import io.vertx.ext.auth.common.handler.impl.AuthenticationHandlerImpl; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; + +public abstract class WebAuthenticationHandlerImpl extends AuthenticationHandlerImpl { + + public WebAuthenticationHandlerImpl(T authProvider) { + super(authProvider, null); + } + + public WebAuthenticationHandlerImpl(T authProvider, String mfa) { + super(authProvider, mfa); + } + + // TODO remove duplicated code from WebAuthenticationHandlerImpl + /** + * This method is protected so custom auth handlers can override the default error handling + */ + protected void processException(RoutingContext ctx, Throwable exception) { + if (exception != null) { + if (exception instanceof HttpException) { + final int statusCode = ((HttpException) exception).getStatusCode(); + final String payload = ((HttpException) exception).getPayload(); + + switch (statusCode) { + case 302: + ctx.response() + .putHeader(HttpHeaders.LOCATION, payload) + .setStatusCode(302) + .end("Redirecting to " + payload + "."); + return; + case 401: + if (!"XMLHttpRequest".equals(ctx.request().getHeader("X-Requested-With"))) { + setAuthenticateHeader(ctx); + } + ctx.fail(401, exception); + return; + default: + ctx.fail(statusCode, exception); + return; + } + } + } + + // fallback 500 + ctx.fail(exception); + } + +} diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/WebAuthnHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/WebAuthnHandlerImpl.java index 5dd9ee27a8..d233cbe3fd 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/WebAuthnHandlerImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/WebAuthnHandlerImpl.java @@ -31,10 +31,10 @@ import io.vertx.ext.web.handler.WebAuthnHandler; import io.vertx.ext.web.impl.OrderListener; import io.vertx.ext.web.impl.Origin; -import io.vertx.ext.web.impl.UserContextInternal; +import io.vertx.ext.auth.common.UserContextInternal; import io.vertx.ext.web.impl.RoutingContextInternal; -public class WebAuthnHandlerImpl extends AuthenticationHandlerImpl implements WebAuthnHandler, OrderListener { +public class WebAuthnHandlerImpl extends WebAuthenticationHandlerImpl implements WebAuthnHandler, OrderListener { private static final boolean CONFORMANCE = Boolean.getBoolean("io.vertx.ext.web.fido2.conformance.tests"); diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/WebHTTPAuthorizationHandler.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/WebHTTPAuthorizationHandler.java new file mode 100644 index 0000000000..84cee37f96 --- /dev/null +++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/WebHTTPAuthorizationHandler.java @@ -0,0 +1,50 @@ +package io.vertx.ext.web.handler.impl; + +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.auth.authentication.AuthenticationProvider; +import io.vertx.ext.auth.common.handler.impl.HTTPAuthorizationHandler; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; + +public abstract class WebHTTPAuthorizationHandler extends HTTPAuthorizationHandler { + + public WebHTTPAuthorizationHandler(T authProvider, Type type, String realm) { + super(authProvider, type, realm); + } + + // TODO remove duplicated code from WebAuthenticationHandlerImpl + /** + * This method is protected so custom auth handlers can override the default error handling + */ + protected void processException(RoutingContext ctx, Throwable exception) { + if (exception != null) { + if (exception instanceof HttpException) { + final int statusCode = ((HttpException) exception).getStatusCode(); + final String payload = ((HttpException) exception).getPayload(); + + switch (statusCode) { + case 302: + ctx.response() + .putHeader(HttpHeaders.LOCATION, payload) + .setStatusCode(302) + .end("Redirecting to " + payload + "."); + return; + case 401: + if (!"XMLHttpRequest".equals(ctx.request().getHeader("X-Requested-With"))) { + setAuthenticateHeader(ctx); + } + ctx.fail(401, exception); + return; + default: + ctx.fail(statusCode, exception); + return; + } + } + } + + // fallback 500 + ctx.fail(exception); + } + + +} diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RouteState.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RouteState.java index bd1db55aaf..fa1b0f8505 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RouteState.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RouteState.java @@ -22,6 +22,7 @@ import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.net.impl.URIDecoder; import io.vertx.core.net.HostAndPort; +import io.vertx.ext.auth.common.AuthenticationHandler; import io.vertx.ext.web.MIMEHeader; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.*; diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java index 11633d262d..0266f33596 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java @@ -11,6 +11,7 @@ import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.auth.User; import io.vertx.ext.auth.audit.SecurityAudit; +import io.vertx.ext.auth.common.UserContext; import io.vertx.ext.web.*; import java.nio.charset.Charset; diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java index bc5d7bb6e6..e6fdeb4ea7 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java @@ -25,6 +25,7 @@ import io.vertx.core.http.*; import io.vertx.core.http.impl.HttpUtils; import io.vertx.core.impl.ContextInternal; +import io.vertx.ext.auth.common.UserContext; import io.vertx.ext.web.*; import io.vertx.ext.web.handler.HttpException; import io.vertx.ext.web.handler.impl.UserHolder; @@ -142,6 +143,11 @@ public void next() { } } + @Override + public void onContinue() { + next(); + } + private void checkHandleNoMatch() { // Next called but no more matching routes if (failed()) { diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java index 77ed8d2227..7cc8a09e24 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java @@ -18,7 +18,7 @@ import io.vertx.codegen.annotations.CacheReturn; import io.vertx.codegen.annotations.Nullable; import io.vertx.core.buffer.Buffer; -import io.vertx.ext.auth.audit.SecurityAudit; +import io.vertx.ext.auth.common.AuthenticationContextInternal; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.Session; @@ -28,7 +28,7 @@ * * @author Paulo Lopes */ -public interface RoutingContextInternal extends RoutingContext { +public interface RoutingContextInternal extends RoutingContext, AuthenticationContextInternal { int BODY_HANDLER = 1 << 1; int CORS_HANDLER = 1 << 2; @@ -107,13 +107,4 @@ default String basePath() { } } - /** - * Get or Default the security audit object. - */ - SecurityAudit securityAudit(); - - /** - * Get or Default the security audit object. - */ - void setSecurityAudit(SecurityAudit securityAudit); } diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java index 1bfd657618..fc9f12a006 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java @@ -25,6 +25,7 @@ import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.auth.common.UserContext; import io.vertx.ext.web.*; import java.nio.charset.Charset; @@ -192,6 +193,11 @@ public void next() { } } + @Override + public void onContinue() { + next(); + } + @Override public boolean failed() { return inner.failed(); diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/UserContextImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/UserContextImpl.java index acdd9d92ac..3deb35223c 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/UserContextImpl.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/UserContextImpl.java @@ -1,263 +1,12 @@ package io.vertx.ext.web.impl; -import io.vertx.core.Future; -import io.vertx.core.http.HttpHeaders; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.impl.logging.Logger; -import io.vertx.core.impl.logging.LoggerFactory; -import io.vertx.ext.auth.User; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.Session; -import io.vertx.ext.web.UserContext; -import io.vertx.ext.web.handler.HttpException; +import io.vertx.ext.auth.common.AbstractUserContext; +import io.vertx.ext.auth.common.AuthenticationContext; -import java.util.Objects; +public class UserContextImpl extends AbstractUserContext { -public class UserContextImpl implements UserContextInternal { - - private static final String USER_SWITCH_KEY = "__vertx.user-switch-ref"; - private static final Logger LOG = LoggerFactory.getLogger(UserContext.class); - - private final RoutingContext ctx; - private User user; - - public UserContextImpl(RoutingContext ctx) { - this.ctx = ctx; - } - - @Override - public void setUser(User user) { - this.user = user; - } - - @Override - public User get() { - return user; - } - - @Override - public UserContext loginHint(String loginHint) { - final Session session = ctx.session(); - - if (session == null) { - if (loginHint == null) { - // Fine, we don't need a session - return this; - } - // we always need a session, otherwise we can't track the state of the previous user - throw new IllegalStateException("SessionHandler not seen in the route. Sessions are required to keep the state"); - } - - if (loginHint == null) { - // we're removing the hint if present - session.remove("login_hint"); - } else { - session - .put("login_hint", loginHint); - } - - return this; - } - - @Override - public Future refresh() { - if (!ctx.request().method().equals(HttpMethod.GET)) { - // we can't automate a redirect to a non-GET request - return Future.failedFuture(new HttpException(405, "Method not allowed")); - } - return refresh(ctx.request().absoluteURI()); - } - - @Override - public Future refresh(String redirectUri) { - Objects.requireNonNull(redirectUri, "redirectUri cannot be null"); - - if (user == null) { - // we need to ensure that we already had a user, otherwise we can't switch - LOG.debug("Impersonation can only occur after a complete authn flow."); - return Future.failedFuture(new HttpException(401)); - } - - final Session session = ctx.session(); - - if (session != null) { - // From now on, we're changing the state - session - // force a session id regeneration to protect against replay attacks - .regenerateId(); - } - - // remove user from the context - this.user = null; - - // we should redirect the UA so this link becomes invalid - return ctx.response() - // disable all caching - .putHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") - .putHeader("Pragma", "no-cache") - .putHeader(HttpHeaders.EXPIRES, "0") - // redirect (when there is no state, redirect to home - .putHeader(HttpHeaders.LOCATION, redirectUri) - .setStatusCode(302) - .end("Redirecting to " + redirectUri + "."); - } - - @Override - public Future impersonate() { - if (!ctx.request().method().equals(HttpMethod.GET)) { - // we can't automate a redirect to a non-GET request - return Future.failedFuture(new HttpException(405, "Method not allowed")); - } - return impersonate(ctx.request().absoluteURI()); - } - - @Override - public Future impersonate(String redirectUri) { - Objects.requireNonNull(redirectUri, "redirectUri cannot be null"); - - if (user == null) { - // we need to ensure that we already had a user, otherwise we can't switch - LOG.debug("Impersonation can only occur after a complete authn flow."); - return Future.failedFuture(new HttpException(401)); - } - - final Session session = ctx.session(); - - if (session == null) { - // we always need a session, otherwise we can't track the state of the previous user - LOG.debug("SessionHandler not seen in the route. Sessions are required to keep the state"); - return Future.failedFuture(new HttpException(500)); - } - - if (session.get(USER_SWITCH_KEY) != null) { - // we always need a session, otherwise we can't track the state of the previous user - LOG.debug("Impersonation already in place"); - return Future.failedFuture(new HttpException(400)); - } - - // From now on, we're changing the state - session - // move the user out of the context (yet keep it in the session, so we can roll back - .put(USER_SWITCH_KEY, user) - // force a session id regeneration to protect against replay attacks - .regenerateId(); - - // remove the current user from the context to avoid any further access - this.user = null; - - // we should redirect the UA so this link becomes invalid - return ctx.response() - // disable all caching - .putHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") - .putHeader("Pragma", "no-cache") - .putHeader(HttpHeaders.EXPIRES, "0") - // redirect (when there is no state, redirect to home - .putHeader(HttpHeaders.LOCATION, redirectUri) - .setStatusCode(302) - .end("Redirecting to " + redirectUri + "."); - } - - @Override - public Future restore() { - if (!ctx.request().method().equals(HttpMethod.GET)) { - // we can't automate a redirect to a non-GET request - return Future.failedFuture(new HttpException(405, "Method not allowed")); - } - return restore(ctx.request().absoluteURI()); + public UserContextImpl(AuthenticationContext ctx) { + super(ctx); } - @Override - public Future restore(String redirectUri) { - Objects.requireNonNull(redirectUri, "redirectUri cannot be null"); - - if (user == null) { - // we need to ensure that we already had a user, otherwise we can't switch - LOG.debug("Impersonation can only occur after a complete authn flow."); - return Future.failedFuture(new HttpException(401)); - } - - final Session session = ctx.session(); - - if (session == null) { - // we always need a session, otherwise we can't track the state of the previous user - LOG.debug("SessionHandler not seen in the route. Sessions are required to keep the state"); - return Future.failedFuture(new HttpException(500)); - } - - if (session.get(USER_SWITCH_KEY) == null) { - // we always need a session, otherwise we can't track the state of the previous user - LOG.debug("No previous impersonation in place"); - return Future.failedFuture(new HttpException(400)); - } - - // From now on, we're changing the state - User previousUser = session.get(USER_SWITCH_KEY); - - session - // move the user out of the context (yet keep it in the session, so we can rollback - .remove(USER_SWITCH_KEY); - // remove the previous hint - session - .remove("login_hint"); - - session - // force a session id regeneration to protect against replay attacks - .regenerateId(); - - // restore it to the context - this.user = previousUser; - - // we should redirect the UA so this link becomes invalid - return ctx.response() - // disable all caching - .putHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") - .putHeader("Pragma", "no-cache") - .putHeader(HttpHeaders.EXPIRES, "0") - // redirect (when there is no state, redirect to home - .putHeader(HttpHeaders.LOCATION, redirectUri) - .setStatusCode(302) - .end("Redirecting to " + redirectUri + "."); - } - - @Override - public Future logout() { - return logout("/"); - } - - @Override - public Future logout(String redirectUri) { - Objects.requireNonNull(redirectUri, "redirectUri cannot be null"); - - final Session session = ctx.session(); - // clear the session - if (session != null) { - session.destroy(); - } - - // clear the user - user = null; - - // we should redirect the UA so this link becomes invalid - return ctx.response() - // disable all caching - .putHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") - .putHeader("Pragma", "no-cache") - .putHeader(HttpHeaders.EXPIRES, "0") - // redirect (when there is no state, redirect to home - .putHeader(HttpHeaders.LOCATION, redirectUri) - .setStatusCode(302) - .end("Redirecting to " + redirectUri + "."); - } - - @Override - public void clear() { - final Session session = ctx.session(); - // clear the session - if (session != null) { - session.destroy(); - } - - // clear the user - user = null; - } } diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/AuthHandlerTestBase.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/AuthHandlerTestBase.java index c1b96ef8fa..b43a69286d 100644 --- a/vertx-web/src/test/java/io/vertx/ext/web/handler/AuthHandlerTestBase.java +++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/AuthHandlerTestBase.java @@ -25,7 +25,7 @@ import io.vertx.ext.auth.properties.PropertyFileAuthentication; import io.vertx.ext.auth.properties.PropertyFileAuthorization; import io.vertx.ext.web.WebTestBase; -import io.vertx.ext.web.impl.UserContextInternal; +import io.vertx.ext.auth.common.UserContextInternal; import io.vertx.ext.web.sstore.LocalSessionStore; import io.vertx.ext.web.sstore.SessionStore; import org.junit.AfterClass; @@ -53,7 +53,7 @@ public void testAuthAuthoritiesFail() throws Exception { testAuthorization("tim", true, PermissionBasedAuthorization.create("knitter")); } - protected abstract AuthenticationHandler createAuthHandler(AuthenticationProvider authProvider); + protected abstract WebAuthenticationHandler createAuthHandler(AuthenticationProvider authProvider); protected boolean requiresSession() { return false; @@ -72,7 +72,7 @@ protected void testAuthorization(String username, boolean fail, Authorization au AuthenticationProvider authNProvider = PropertyFileAuthentication.create(vertx, "login/loginusers.properties"); AuthorizationProvider authZProvider = PropertyFileAuthorization.create(vertx, "login/loginusers.properties"); - AuthenticationHandler authNHandler = createAuthHandler(authNProvider); + WebAuthenticationHandler authNHandler = createAuthHandler(authNProvider); router.route().handler(rc -> { // we need to be logged in if (!rc.user().authenticated()) { diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/AuthXRequestedWithTest.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/AuthXRequestedWithTest.java index 73e3c01443..7588f313ce 100644 --- a/vertx-web/src/test/java/io/vertx/ext/web/handler/AuthXRequestedWithTest.java +++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/AuthXRequestedWithTest.java @@ -59,7 +59,7 @@ public void testNoWwwAuthenticateForAjaxCalls() throws Exception { } @Override - protected AuthenticationHandler createAuthHandler(AuthenticationProvider authProvider) { + protected WebAuthenticationHandler createAuthHandler(AuthenticationProvider authProvider) { return BasicAuthHandler.create(authProvider); } diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/BasicAuthHandlerTest.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/BasicAuthHandlerTest.java index 3dde95ee13..fdf9480a76 100644 --- a/vertx-web/src/test/java/io/vertx/ext/web/handler/BasicAuthHandlerTest.java +++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/BasicAuthHandlerTest.java @@ -214,7 +214,7 @@ public void testLoginFailWithBadBase64() throws Exception { } @Override - protected AuthenticationHandler createAuthHandler(AuthenticationProvider authProvider) { + protected WebAuthenticationHandler createAuthHandler(AuthenticationProvider authProvider) { return BasicAuthHandler.create(authProvider); } diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthHandlerAndTest.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthHandlerAndTest.java index cae5c94e9f..876890703e 100644 --- a/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthHandlerAndTest.java +++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthHandlerAndTest.java @@ -17,7 +17,7 @@ public void setUp() throws Exception { super.setUp(); authProvider = PropertyFileAuthentication.create(vertx, "login/loginusers.properties"); - AuthenticationHandler redirectAuthHandler = RedirectAuthHandler.create(authProvider); + WebAuthenticationHandler redirectAuthHandler = RedirectAuthHandler.create(authProvider); // create a chain chain = ChainAuthHandler.all() diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthHandlerTest.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthHandlerTest.java index aa959fd66a..8aa9a33ae5 100644 --- a/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthHandlerTest.java +++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthHandlerTest.java @@ -20,7 +20,7 @@ public void setUp() throws Exception { super.setUp(); authProvider = PropertyFileAuthentication.create(vertx, "login/loginusers.properties"); - AuthenticationHandler redirectAuthHandler = RedirectAuthHandler.create(authProvider); + WebAuthenticationHandler redirectAuthHandler = RedirectAuthHandler.create(authProvider); // create a chain chain = ChainAuthHandler.any() diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthMixHandlerTest.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthMixHandlerTest.java index f11bd26844..a82a0058d0 100644 --- a/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthMixHandlerTest.java +++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/ChainAuthMixHandlerTest.java @@ -17,11 +17,11 @@ public class ChainAuthMixHandlerTest extends WebTestBase { private static final User USER = User.create(new JsonObject().put("id", "paulo")); - private final AuthenticationHandler success = SimpleAuthenticationHandler.create() + private final WebAuthenticationHandler success = SimpleAuthenticationHandler.create() .authenticate(ctx -> Future.succeededFuture(USER)); - private final AuthenticationHandler failure = SimpleAuthenticationHandler.create() + private final WebAuthenticationHandler failure = SimpleAuthenticationHandler.create() .authenticate(ctx -> Future.failedFuture(new HttpException(401))); @Test diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/CustomAuthHandlerTest.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/CustomAuthHandlerTest.java index ef85d71e67..ea4b4f5626 100644 --- a/vertx-web/src/test/java/io/vertx/ext/web/handler/CustomAuthHandlerTest.java +++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/CustomAuthHandlerTest.java @@ -31,11 +31,11 @@ public class CustomAuthHandlerTest extends AuthHandlerTestBase { @Override - protected AuthenticationHandler createAuthHandler(AuthenticationProvider authProvider) { + protected WebAuthenticationHandler createAuthHandler(AuthenticationProvider authProvider) { return newAuthHandler(authProvider, null); } - private AuthenticationHandler newAuthHandler(AuthenticationProvider authProvider, Handler exceptionProcessor) { + private WebAuthenticationHandler newAuthHandler(AuthenticationProvider authProvider, Handler exceptionProcessor) { return SimpleAuthenticationHandler.create() .authenticate(ctx -> { diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/EventbusBridgeTest.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/EventbusBridgeTest.java index 540aa27958..32344054f4 100644 --- a/vertx-web/src/test/java/io/vertx/ext/web/handler/EventbusBridgeTest.java +++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/EventbusBridgeTest.java @@ -1166,7 +1166,7 @@ public void testSendRequiresAuthorityHasnotAuthority() throws Exception { testError(new JsonObject().put("type", "send").put("address", addr).put("body", "foo"), "access_denied"); } - private AuthenticationHandler addLoginHandler(AuthenticationProvider authProvider) { + private WebAuthenticationHandler addLoginHandler(AuthenticationProvider authProvider) { return SimpleAuthenticationHandler.create() .authenticate(ctx -> { if (ctx.user().get() == null) { diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/OAuth2AuthHandlerTest.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/OAuth2AuthHandlerTest.java index 5db674edfb..1dc63f286a 100644 --- a/vertx-web/src/test/java/io/vertx/ext/web/handler/OAuth2AuthHandlerTest.java +++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/OAuth2AuthHandlerTest.java @@ -16,6 +16,16 @@ package io.vertx.ext.web.handler; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Ignore; +import org.junit.Test; + import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServer; import io.vertx.core.json.JsonObject; @@ -26,17 +36,10 @@ import io.vertx.ext.auth.oauth2.OAuth2Auth; import io.vertx.ext.auth.oauth2.OAuth2Options; import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.WebTestBase; +import io.vertx.ext.web.handler.impl.OAuth2AuthHandlerImpl; import io.vertx.ext.web.sstore.SessionStore; -import org.junit.Ignore; -import org.junit.Test; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; /** * @author Paulo Lopes @@ -151,9 +154,11 @@ public void testAuthCodeFlowWithScopes() throws Exception { // create a oauth2 handler on our domain to the callback: "http://localhost:8080/callback" OAuth2AuthHandler oauth2Handler = OAuth2AuthHandler - .create(vertx, oauth2, "http://localhost:8080/callback") - // require "read" scope - .withScope("read"); + .create(vertx, oauth2, "http://localhost:8080/callback"); + + //TODO fix issue with fluent API + // require "read" scope + oauth2Handler = (OAuth2AuthHandlerImpl)oauth2Handler.withScope("read"); // setup the callback handler for receiving the callback oauth2Handler.setupCallback(router.route("/callback")); @@ -213,9 +218,12 @@ public void testAuthCodeFlowWithScopesInvalid() throws Exception { // create a oauth2 handler on our domain to the callback: "http://localhost:8080/callback" OAuth2AuthHandler oauth2Handler = OAuth2AuthHandler - .create(vertx, oauth2, "http://localhost:8080/callback") - // require "rea" scope (will fail) - .withScope("rea"); + .create(vertx, oauth2, "http://localhost:8080/callback"); + + + //TODO fix issue with fluent API + // require "rea" scope (will fail) + oauth2Handler = (OAuth2AuthHandlerImpl)oauth2Handler.withScope("rea"); // setup the callback handler for receiving the callback oauth2Handler.setupCallback(router.route("/callback")); @@ -487,8 +495,11 @@ public void testAuthPKCECodeFlow() throws Exception { // create a oauth2 handler on our domain to the callback: "http://localhost:8080/callback" OAuth2AuthHandler oauth2Handler = OAuth2AuthHandler - .create(vertx, oauth2, "http://localhost:8080/callback") - .pkceVerifierLength(64); + .create(vertx, oauth2, "http://localhost:8080/callback"); + + //TODO fix issue with fluent API + oauth2Handler = (OAuth2AuthHandlerImpl)oauth2Handler.pkceVerifierLength(64); + // setup the callback handler for receiving the callback oauth2Handler.setupCallback(router.route("/callback")); // protect everything under /protected @@ -677,7 +688,7 @@ public void testPasswordFlow() throws Exception { latch.await(); - AuthenticationHandler oauth2Handler = BasicAuthHandler.create(oauth2); + WebAuthenticationHandler oauth2Handler = BasicAuthHandler.create(oauth2); // protect everything under /protected router.route("/protected/*").handler(oauth2Handler); diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/RedirectAuthHandlerTest.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/RedirectAuthHandlerTest.java index e25350ae8a..0f6332ba4e 100644 --- a/vertx-web/src/test/java/io/vertx/ext/web/handler/RedirectAuthHandlerTest.java +++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/RedirectAuthHandlerTest.java @@ -21,6 +21,7 @@ import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpMethod; import io.vertx.ext.auth.authentication.AuthenticationProvider; +import io.vertx.ext.auth.common.AuthenticationHandler; import io.vertx.ext.auth.properties.PropertyFileAuthentication; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.Session; @@ -229,7 +230,7 @@ public void testRedirectWithParams() throws Exception { } @Override - protected AuthenticationHandler createAuthHandler(AuthenticationProvider authProvider) { + protected WebAuthenticationHandler createAuthHandler(AuthenticationProvider authProvider) { return RedirectAuthHandler.create(authProvider); }