All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.vertx.up.secure.handler.AuthPhylum Maven / Gradle / Ivy

There is a newer version: 0.9.0
Show newest version
package io.vertx.up.secure.handler;

import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.http.*;
import io.vertx.ext.auth.AuthProvider;
import io.vertx.ext.auth.User;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.Session;
import io.vertx.ext.web.handler.AuthHandler;
import io.vertx.up.eon.Strings;
import io.vertx.up.exception.WebException;
import io.vertx.up.exception.web._400BadRequestException;
import io.vertx.up.exception.web._401UnauthorizedException;
import io.vertx.up.exception.web._403ForbiddenException;
import io.vertx.up.exception.web._500InternalServerException;
import io.vertx.up.log.Annal;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

@SuppressWarnings("all")
public abstract class AuthPhylum implements AuthHandler {

    static final String AUTH_PROVIDER_CONTEXT_KEY = "io.vertx.ext.web.handler.AuthHandler.provider";
    protected final String realm;
    protected final Set authorities = new HashSet<>();
    final WebException FORBIDDEN = new _403ForbiddenException(this.getClass());
    final WebException UNAUTHORIZED = new _401UnauthorizedException(this.getClass());
    final WebException BAD_REQUEST = new _400BadRequestException(this.getClass());
    // Provider binding
    protected transient AuthProvider authProvider;

    public AuthPhylum(final AuthProvider authProvider) {
        this(authProvider, "");
    }

    public AuthPhylum(final AuthProvider authProvider, final String realm) {
        this.authProvider = authProvider;
        this.realm = realm;
    }

    @Override
    public AuthHandler addAuthority(final String authority) {
        this.authorities.add(authority);
        return this;
    }

    @Override
    public AuthHandler addAuthorities(final Set authorities) {
        this.authorities.addAll(authorities);
        return this;
    }

    protected String authenticateHeader(final RoutingContext context) {
        return null;
    }

    @Override
    public void authorize(final User user, final Handler> handler) {
        final int requiredcount = this.authorities.size();
        if (requiredcount > 0) {
            if (user == null) {
                handler.handle(Future.failedFuture(this.FORBIDDEN));
                return;
            }

            final AtomicInteger count = new AtomicInteger();
            final AtomicBoolean sentFailure = new AtomicBoolean();

            final Handler> authHandler = res -> {
                if (res.succeeded()) {
                    if (res.result()) {
                        if (count.incrementAndGet() == requiredcount) {
                            // Has all required authorities
                            handler.handle(Future.succeededFuture());
                        }
                    } else {
                        if (sentFailure.compareAndSet(false, true)) {
                            handler.handle(Future.failedFuture(this.FORBIDDEN));
                        }
                    }
                } else {
                    handler.handle(Future.failedFuture(res.cause()));
                }
            };
            for (final String authority : this.authorities) {
                if (!sentFailure.get()) {
                    user.isAuthorized(authority, authHandler);
                }
            }
        } else {
            // No auth required
            handler.handle(Future.succeededFuture());
        }
    }

    @Override
    public void handle(final RoutingContext ctx) {
        /*
         * Because AuthPhylum component is Handler of vert.x, here it the first step
         * Call handlePreflight to verify http specification
         * Please refer: https://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0
         * Cross domain
         */
        if (this.handlePreflight(ctx)) {
            return;
        }

        final User user = ctx.user();
        if (user != null) {
            // proceed to AuthZ
            this.authorizeUser(ctx, user);
            return;
        }
        // parse the request in order to extract the credentials object
        this.parseCredentials(ctx, res -> {
            if (res.failed()) {
                this.processException(ctx, res.cause());
                return;
            }
            // check if the user has been set
            final User updatedUser = ctx.user();

            if (updatedUser != null) {
                final Session session = ctx.session();
                if (session != null) {
                    // the user has upgraded from unauthenticated to authenticated
                    // session should be upgraded as recommended by owasp
                    session.regenerateId();
                }
                // proceed to AuthZ
                this.authorizeUser(ctx, updatedUser);
                return;
            }
            // proceed to authN
            this.getAuthProvider(ctx).authenticate(AuthReady.prepare(res.result(), ctx), authN -> {
                if (authN.succeeded()) {
                    final User authenticated = authN.result();
                    ctx.setUser(authenticated);
                    final Session session = ctx.session();
                    if (session != null) {
                        // the user has upgraded from unauthenticated to authenticated
                        // session should be upgraded as recommended by owasp
                        session.regenerateId();
                        // Store user's princple into session
                    }
                    // proceed to AuthZ
                    this.authorizeUser(ctx, authenticated);
                } else {
                    // The first time to get
                    final String header = this.authenticateHeader(ctx);
                    if (header != null) {
                        ctx.response().putHeader("WWW-Authenticate", header);
                    }
                    ctx.fail(null == authN.cause() ? this.UNAUTHORIZED : authN.cause());
                }
            });
        });
    }

    private void processException(final RoutingContext ctx, final Throwable exception) {
        if (null != exception) {
            if (exception instanceof WebException) {
                final WebException error = (WebException) exception;
                final HttpStatusCode statusCode = error.getStatus();
                final String payload = error.getMessage();

                final HttpServerResponse response = ctx.response();

                switch (statusCode) {
                    case MOVE_TEMPORARILY: {
                        response.putHeader(HttpHeaders.LOCATION, payload)
                                .setStatusCode(statusCode.code())
                                .end("Redirecting to " + payload + ".");
                    }
                    return;
                    case UNAUTHORIZED: {
                        final String header = this.authenticateHeader(ctx);
                        if (null != header) {
                            response.putHeader("WWW-Authenticate", header);
                        }
                        ctx.fail(this.UNAUTHORIZED);
                    }
                    return;
                    default:
                        ctx.fail(error);
                        return;
                }
            }
        }
        // Fallback 500
        ctx.fail(new _500InternalServerException(this.getClass(),
                null == exception ? null : exception.getMessage()));
    }

    private void authorizeUser(final RoutingContext ctx, final User user) {
        this.authorize(user, authZ -> {
            if (authZ.failed()) {
                this.processException(ctx, authZ.cause());
                return;
            }
            // success, allowed to continue
            ctx.next();
        });
    }

    private boolean handlePreflight(final RoutingContext 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
        if (request.method() == HttpMethod.OPTIONS) {
            // check if there is a access control request header
            final String accessControlRequestHeader = ctx.request().getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
            if (accessControlRequestHeader != null) {
                // lookup for the Authorization header
                for (final String ctrlReq : accessControlRequestHeader.split(Strings.COMMA)) {
                    if (ctrlReq.equalsIgnoreCase(HttpHeaders.AUTHORIZATION.toString())) {
                        // this request has auth in access control, so we can allow preflighs without authentication
                        ctx.next();
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private AuthProvider getAuthProvider(final RoutingContext ctx) {
        try {
            final AuthProvider provider = ctx.get(AUTH_PROVIDER_CONTEXT_KEY);
            if (provider != null) {
                // we're overruling the configured one for this request
                this.authProvider = provider;
                return provider;
            }
        } catch (final RuntimeException e) {
            // bad type, ignore and return default
            e.printStackTrace();
        }
        return this.authProvider;
    }

    protected Annal getLogger() {
        return Annal.get(this.getClass());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy