Skip to content

Commit

Permalink
Port to 4.5.x the UserContext API.
Browse files Browse the repository at this point in the history
Motivation:

Vert.x 5 has a new UserContext API that encapsulate the API present on Routingcontext in addition of new identity related operations. This API can be back-ported to 4.5.x to ease migration to Vert.x 5 .

Changes:

Partial part of the UserContext API and documentation, as well as a couple of deprecations in favor of this new API.
  • Loading branch information
vietj committed Dec 16, 2024
1 parent 3eab0a1 commit 542dcf4
Show file tree
Hide file tree
Showing 10 changed files with 720 additions and 21 deletions.
14 changes: 14 additions & 0 deletions vertx-web/src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,20 @@ Complex chaining is also possible, for example, building logic sequences such as
{@link examples.WebExamples#exampleChainAuthHandler}
----

=== Impersonation
When using authentication handlers, it is possible that the identity of the user changes over time. For
example, a user may require to become `admin` for a specific period of time. This can be achieved by calling:
[source,$lang]
----
{@link examples.WebExamples#example89}
----
The operation can be reversed by calling:
[source,$lang]
----
{@link examples.WebExamples#example90}
----
The impersonation does not require calling other router endpoints to avoid being exploited from the outside.

== Serving static resources

Vert.x-Web comes with an out of the box handler for serving static web resources so you can write static web servers
Expand Down
22 changes: 22 additions & 0 deletions vertx-web/src/main/java/examples/WebExamples.java
Original file line number Diff line number Diff line change
Expand Up @@ -1971,4 +1971,26 @@ public void example87(Router router) {
ctx.end(value);
});
}

public void example89(Router router) {
router
.route("/high/security/route/check")
.handler(ctx -> {
// if the user isn't admin, we ask the user to login again as admin
ctx
.userContext()
.loginHint("admin")
.impersonate();
});
}

public void example90(Router router) {
router
.route("/high/security/route/back/to/me")
.handler(ctx -> {
ctx
.userContext()
.restore();
});
}
}
20 changes: 17 additions & 3 deletions vertx-web/src/main/java/io/vertx/ext/web/RoutingContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.User;
import io.vertx.ext.web.impl.ParsableMIMEValue;
import io.vertx.ext.web.impl.UserContextImpl;
import io.vertx.ext.web.impl.Utils;

import java.nio.charset.Charset;
Expand Down Expand Up @@ -407,7 +408,11 @@ default String normalisedPath() {
* Get the authenticated user (if any). This will usually be injected by an auth handler if authentication if successful.
* @return the user, or null if the current user is not authenticated.
*/
@Nullable User user();
default @Nullable User user() {
return userContext().get();
}

UserContext userContext();

/**
* If the context is being routed to failure handlers after a failure has been triggered by calling
Expand Down Expand Up @@ -551,14 +556,23 @@ default Future<Void> addEndHandler() {
* Set the user. Usually used by auth handlers to inject a User. You will not normally call this method.
*
* @param user the user
* @deprecated this method should not be called, application authentication should rely on {@link io.vertx.ext.web.handler.AuthenticationHandler} implementations.
*/
void setUser(User user);
@Deprecated
default void setUser(User user) {
((UserContextImpl) userContext()).setUser(user);
}

/**
* Clear the current user object in the context. This usually is used for implementing a log out feature, since the
* current user is unbounded from the routing context.
*
* @deprecated instead use {@link UserContext#logout()}
*/
void clearUser();
@Deprecated
default void clearUser() {
setUser(null);
}

/**
* Set the acceptable content type. Used by
Expand Down
147 changes: 147 additions & 0 deletions vertx-web/src/main/java/io/vertx/ext/web/UserContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package io.vertx.ext.web;

import io.vertx.codegen.annotations.Fluent;
import io.vertx.codegen.annotations.Nullable;
import io.vertx.codegen.annotations.VertxGen;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.ext.auth.User;

/**
* A web user is extended user coupled to the context and is used to perform verifications
* and actions on behalf of the user. Actions can be:
*
* <ul>
* <li>{@link #impersonate()} - Require a re-authentication to switch user identities</li>
* <li>{@link #restore()} - De-escalate a previous impersonate call</li>
* <li>{@link #logout()} - Logout the user from this application and redirect to a uri</li>
* <li>{@link #clear()} - Same as logout, without requirind a redirect</li>
* </ul>
*/
@VertxGen
public interface UserContext {

/**
* Get the authenticated user (if any). This will usually be injected by an auth handler if authentication if successful.
*
* @return the user, or null if the current user is not authenticated.
*/
@Nullable
User get();

default boolean authenticated() {
return get() != null;
}

/**
* When performing a web identity operation, hint if possible to the identity provider to use the given login.
*
* @param loginHint the desired login name, for example: {@code admin}.
* @return fluent self
*/
@Fluent
UserContext loginHint(String loginHint);

/**
* Impersonates a second identity. The user will be redirected to the same origin where this call was
* made. It is important to notice that the redirect will only allow sources originating from a HTTP GET request.
*
* @return future result of the operation.
*/
Future<Void> impersonate();

default void impersonate(Handler<AsyncResult<Void>> callback) {
Future<Void> fut = impersonate();
if (callback != null) {
fut.onComplete(callback);
}
}

/**
* Impersonates a second identity. The user will be redirected to the given uri. It is important to
* notice that the redirect will only allow targets using an HTTP GET request.
*
* @param redirectUri the uri to redirect the user to after the authentication.
* @return future result of the operation.
*/
Future<Void> impersonate(String redirectUri);

default void impersonate(String redirectUri, Handler<AsyncResult<Void>> callback) {
Future<Void> fut = impersonate(redirectUri);
if (callback != null) {
fut.onComplete(callback);
}
}

/**
* Undo a previous call to a impersonation. The user will be redirected to the same origin where this call was
* made. It is important to notice that the redirect will only allow sources originating from a HTTP GET request.
*
* @return future result of the operation.
*/
Future<Void> restore();

default void restore(Handler<AsyncResult<Void>> callback) {
Future<Void> fut = restore();
if (callback != null) {
fut.onComplete(callback);
}
}

/**
* Undo a previous call to an impersonation. The user will be redirected to the given uri. It is important to
* notice that the redirect will only allow targets using an HTTP GET request.
*
* @param redirectUri the uri to redirect the user to after the re-authentication.
* @return future result of the operation.
*/
Future<Void> restore(String redirectUri);

default void restore(String redirectUri, Handler<AsyncResult<Void>> callback) {
Future<Void> fut = restore(redirectUri);
if (callback != null) {
fut.onComplete(callback);
}
}

/**
* Logout can be called from any route handler which needs to terminate a login session. Invoking logout will remove
* the {@link io.vertx.ext.auth.User} and clear the {@link Session} (if any) in the current context. Followed by a
* redirect to the given uri.
*
* @param redirectUri the uri to redirect the user to after the logout.
* @return future result of the operation.
*/
Future<Void> logout(String redirectUri);

default void logout(String redirectUri, Handler<AsyncResult<Void>> callback) {
Future<Void> fut = logout(redirectUri);
if (callback != null) {
fut.onComplete(callback);
}
}

/**
* Logout can be called from any route handler which needs to terminate a login session. Invoking logout will remove
* the {@link io.vertx.ext.auth.User} and clear the {@link Session} (if any) in the current context. Followed by a
* redirect to {@code /}.
*
* @return future result of the operation.
*/
Future<Void> logout();

default void logout(Handler<AsyncResult<Void>> callback) {
Future<Void> fut = logout();
if (callback != null) {
fut.onComplete(callback);
}
}

/**
* Clear can be called from any route handler which needs to terminate a login session. Invoking logout will remove
* the {@link io.vertx.ext.auth.User} and clear the {@link Session} (if any) in the current context. Unlike
* {@link #logout()} no redirect will be performed.
*/
void clear();
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ public User user() {
return decoratedContext.user();
}

@Override
public UserContext userContext() {
return decoratedContext.userContext();
}

@Override
public Session session() {
return decoratedContext.session();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@
import io.vertx.core.http.impl.HttpUtils;
import io.vertx.core.impl.ContextInternal;
import io.vertx.ext.auth.User;
import io.vertx.ext.web.FileUpload;
import io.vertx.ext.web.RequestBody;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.Session;
import io.vertx.ext.web.*;
import io.vertx.ext.web.handler.HttpException;
import io.vertx.ext.web.handler.impl.UserHolder;

Expand Down Expand Up @@ -66,7 +63,7 @@ public class RoutingContextImpl extends RoutingContextImplBase {
private final AtomicBoolean cleanup = new AtomicBoolean(false);
private List<FileUpload> fileUploads;
private Session session;
private User user;
private UserContext userContext;

private volatile boolean isSessionAccessed = false;
private volatile boolean endHandlerCalled = false;
Expand Down Expand Up @@ -366,18 +363,11 @@ public boolean isSessionAccessed(){
}

@Override
public User user() {
return user;
}

@Override
public void setUser(User user) {
this.user = user;
}

@Override
public void clearUser() {
this.user = null;
public UserContext userContext() {
if (userContext == null) {
userContext = new UserContextImpl(this);
}
return userContext;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@ public void clearUser() {
inner.clearUser();
}

@Override
public UserContext userContext() {
return inner.userContext();
}

@Override
public User user() {
return inner.user();
Expand Down
Loading

0 comments on commit 542dcf4

Please sign in to comment.