quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.74k stars 2.67k forks source link

Custom `SecurityContext` provokes `StackOverflowError` #44078

Open kronst opened 3 hours ago

kronst commented 3 hours ago

Describe the bug

In my application I have a custom ContainerRequestFilter implementation which replaces the SecurityContext in the ContainerRequestContext. At the same time there is a io.quarkus.resteasy.runtime.SecurityContextFilter in quarkus-resteasy which replaces the actual SecurityIdentity with an implementation that uses modified security context (added in this commit).

Since my implementation of SecurityContext which is almost the same as QuarkusResteasySecurityContext invokes methods of SecurityIdentity this creates an infinity recursion and provokes StackOverflowError: SecurityContext calls SecurityIdentity > SecurityIdentity calls SecurityContext.

Is this a potential bug or is there any workaround how can I solve this issue? All examples of my implementations of filter and context provided. Also example project with a test that will failed due to StackOverflowError will be attached.

Expected behavior

No response

Actual behavior

No response

How to Reproduce?

Custom filter example:

@Provider
@PreMatching
public class CustomFilter implements ContainerRequestFilter {

    @Inject
    CurrentVertxRequest currentVertxRequest;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        SecurityContext securityContext = new CustomSecurityContext(
            requestContext,
            currentVertxRequest.getCurrent(),
            List.of("admin")
        );
        requestContext.setSecurityContext(securityContext);
    }
}

Custom security context example:

public class CustomSecurityContext implements SecurityContext {

    private final ContainerRequestContext requestContext;
    private final RoutingContext routingContext;
    private final List<String> roles;

    public CustomSecurityContext(ContainerRequestContext requestContext, RoutingContext routingContext, List<String> roles) {
        this.requestContext = requestContext;
        this.routingContext = routingContext;
        this.roles = roles;
    }

    @Override
    public Principal getUserPrincipal() {
        QuarkusHttpUser user = (QuarkusHttpUser) routingContext.user();
        return user != null && !user.getSecurityIdentity().isAnonymous() // StackOverflowError
            ? user.getSecurityIdentity().getPrincipal() // StackOverflowError
            : null;
    }

    @Override
    public boolean isUserInRole(String role) {
        SecurityIdentity user = CurrentIdentityAssociation.current();

        System.out.println(user.isAnonymous()); // StackOverflowError

        if (role.equals("**")) {
            return !user.isAnonymous(); // StackOverflowError
        } else {
            return roles.contains(role);
        }
    }

    @Override
    public boolean isSecure() {
        return true;
    }

    @Override
    public String getAuthenticationScheme() {
        String authorizationValue = requestContext.getHeaders().getFirst("Authorization");
        if (authorizationValue == null) {
            return null;
        } else {
            return authorizationValue.split(" ")[0].trim();
        }
    }
}

Resteasy SecurityContextFilter that replaces the SecurityIdentity:

@PreMatching
@Priority(Priorities.USER + 1)
@Provider
public class SecurityContextFilter implements ContainerRequestFilter {

    @Inject
    SecurityIdentity old;

    @Inject
    CurrentIdentityAssociation currentIdentityAssociation;

    @Inject
    RoutingContext routingContext;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        SecurityContext modified = requestContext.getSecurityContext();
        if (modified instanceof ServletSecurityContext || modified instanceof QuarkusResteasySecurityContext) {
            //an original security context, it has not been modified
            return;
        }
        Set<Credential> oldCredentials = old.getCredentials();
        Map<String, Object> oldAttributes = old.getAttributes();
        SecurityIdentity newIdentity = new SecurityIdentity() {
            @Override
            public Principal getPrincipal() {
                return modified.getUserPrincipal();
            }

            @Override
            public boolean isAnonymous() {
                return modified.getUserPrincipal() == null;
            }

            @Override
            public Set<String> getRoles() {
                throw new UnsupportedOperationException(
                        "retrieving all roles not supported when JAX-RS security context has been replaced");
            }

            @Override
            public boolean hasRole(String role) {
                return modified.isUserInRole(role);
            }

            @Override
            public <T extends Credential> T getCredential(Class<T> credentialType) {
                for (Credential cred : getCredentials()) {
                    if (credentialType.isAssignableFrom(cred.getClass())) {
                        return (T) cred;
                    }
                }
                return null;
            }

            @Override
            public Set<Credential> getCredentials() {
                return oldCredentials;
            }

            @Override
            public <T> T getAttribute(String name) {
                return (T) oldAttributes.get(name);
            }

            @Override
            public Map<String, Object> getAttributes() {
                return oldAttributes;
            }

            @Override
            public Uni<Boolean> checkPermission(Permission permission) {
                return Uni.createFrom().nullItem();
            }
        };
        routingContext.setUser(new QuarkusHttpUser(newIdentity));
        currentIdentityAssociation.setIdentity(newIdentity);
    }
}

Minimal example project: quarkus-custom-security-context.zip

Output of uname -a or ver

Linux laptop 6.8.0-47-generic #47~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Wed Oct 2 16:16:55 UTC 2 x86_64 x86_64 x86_64 GNU/Linux

Output of java -version

openjdk version "17.0.9" 2023-10-17 LTS

Quarkus version or git rev

3.14.3

Build tool (ie. output of mvnw --version or gradlew --version)

Gradle 8.11-rc-1

Additional information

No response

quarkus-bot[bot] commented 3 hours ago

/cc @sberyozkin (security)