Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Refactor authentication code #2487

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
<module>vertx-web-validation</module>
<module>vertx-web-openapi-router</module>
<module>vertx-web-proxy</module>
<module>vertx-web-auth-common</module>
<module>vertx-web-auth-jwt</module>
<module>vertx-web-auth-oauth2</module>
</modules>

<profiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions vertx-web-auth-common/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>vertx-web-parent</artifactId>
<groupId>io.vertx</groupId>
<version>5.0.0-SNAPSHOT</version>
</parent>

<artifactId>vertx-web-auth-common</artifactId>

<properties>
<doc.skip>true</doc.skip>
</properties>

<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-auth-common</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> logout() {
return logout("/");
}

@Override
public Future<Void> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* The context allows access to the HTTP request and response to verify provided authentication information
* <p>
* 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();

}
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* An auth handler allows your application to provide authentication support.
* <p>
* An Auth handler may require a {@link SessionHandler} to be on the routing chain before it.
*
* @author <a href="http://tfox.org">Tim Fox</a>
* @author <a href="mailto:[email protected]">Paulo Lopes</a>
*/
@VertxGen(concrete = false)
public interface AuthenticationHandler extends Handler<RoutingContext> {
public interface AuthenticationHandler<C extends AuthenticationContext> extends Handler<C> {

}
Loading