Skip to content

Commit

Permalink
feat(security): include missing auth failure msg body in dev mode
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Jan 18, 2025
1 parent aec3f34 commit cb7ea96
Show file tree
Hide file tree
Showing 14 changed files with 348 additions and 28 deletions.
4 changes: 4 additions & 0 deletions core/runtime/src/main/java/io/quarkus/runtime/LaunchMode.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public boolean isDevOrTest() {
return this != NORMAL;
}

public static boolean isDev() {
return current() == DEVELOPMENT;
}

/**
* Returns true if the current launch is the server side of remote dev.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package io.quarkus.resteasy.test.security;

import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.Authenticated;
import io.quarkus.security.AuthenticationCompletionException;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AuthenticationRequest;
import io.quarkus.security.identity.request.CertificateAuthenticationRequest;
import io.quarkus.test.QuarkusDevModeTest;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.restassured.RestAssured;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

public class AuthenticationFailureResponseBodyDevModeTest {

private static final String RESPONSE_BODY = "failure";

public enum AuthFailure {
AUTH_FAILED_WITH_BODY(() -> new AuthenticationFailedException(RESPONSE_BODY), true),
AUTH_COMPLETION_WITH_BODY(() -> new AuthenticationCompletionException(RESPONSE_BODY), true),
AUTH_FAILED_WITHOUT_BODY(AuthenticationFailedException::new, false),
AUTH_COMPLETION_WITHOUT_BODY(AuthenticationCompletionException::new, false);

public final Supplier<Throwable> failureSupplier;
private final boolean expectBody;

AuthFailure(Supplier<Throwable> failureSupplier, boolean expectBody) {
this.failureSupplier = failureSupplier;
this.expectBody = expectBody;
}
}

@RegisterExtension
static QuarkusDevModeTest runner = new QuarkusDevModeTest()
.withApplicationRoot((jar) -> jar
.addClasses(SecuredResource.class, FailingAuthenticator.class, AuthFailure.class)
.addAsResource(new StringAsset("""
quarkus.http.auth.proactive=false
"""), "application.properties"));

@Test
public void testAuthenticationFailedExceptionBody() {
assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, false);
assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, true);
assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, false);
assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, true);
}

@Test
public void testAuthenticationCompletionExceptionBody() {
assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITHOUT_BODY, false);
assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITH_BODY, false);
}

private static void assertExceptionBody(AuthFailure failure, boolean challenge) {
RestAssured
.given()
.redirects().follow(false)
.header("auth-failure", failure.toString())
.header("challenge-data", challenge)
.get("/secured")
.then()
.statusCode(challenge ? 302 : 401)
.body(failure.expectBody ? Matchers.equalTo(RESPONSE_BODY)
: Matchers.not(Matchers.containsString(RESPONSE_BODY)));
}

@Authenticated
@Path("secured")
public static class SecuredResource {

@GET
public String ignored() {
return "ignored";
}

}

@ApplicationScoped
public static class FailingAuthenticator implements HttpAuthenticationMechanism {

@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
return Uni.createFrom().failure(getFailureProducer(context));
}

private static Supplier<Throwable> getFailureProducer(RoutingContext context) {
return getAuthFailure(context).failureSupplier;
}

private static AuthFailure getAuthFailure(RoutingContext context) {
return AuthFailure.valueOf(context.request().getHeader("auth-failure"));
}

@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
// so that we don't need to implement an identity provider
return Collections.singleton(CertificateAuthenticationRequest.class);
}

@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
if (Boolean.parseBoolean(context.request().getHeader("challenge-data"))) {
return Uni.createFrom().item(new ChallengeData(302, null, null));
} else {
return Uni.createFrom().optional(Optional.empty());
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import org.jboss.logging.Logger;

import io.quarkus.runtime.LaunchMode;
import io.quarkus.security.AuthenticationCompletionException;

@Provider
Expand All @@ -16,6 +17,9 @@ public class AuthenticationCompletionExceptionMapper implements ExceptionMapper<
@Override
public Response toResponse(AuthenticationCompletionException ex) {
log.debug("Authentication has failed, returning HTTP status 401");
if (LaunchMode.isDev() && ex.getMessage() != null) {
return Response.status(401).entity(ex.getMessage()).build();
}
return Response.status(401).build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import org.jboss.logging.Logger;

import io.quarkus.runtime.LaunchMode;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
Expand All @@ -19,7 +20,6 @@
@Priority(Priorities.USER + 1)
public class AuthenticationFailedExceptionMapper implements ExceptionMapper<AuthenticationFailedException> {
private static final Logger log = Logger.getLogger(AuthenticationFailedExceptionMapper.class.getName());

private volatile CurrentVertxRequest currentVertxRequest;

CurrentVertxRequest currentVertxRequest() {
Expand All @@ -37,18 +37,25 @@ public Response toResponse(AuthenticationFailedException exception) {
if (authenticator != null) {
ChallengeData challengeData = authenticator.getChallenge(context)
.await().indefinitely();
Response.ResponseBuilder status = Response.status(challengeData.status);
if (challengeData.headerName != null) {
status.header(challengeData.headerName.toString(), challengeData.headerContent);
int statusCode = challengeData == null ? 401 : challengeData.status;
Response.ResponseBuilder responseBuilder = Response.status(statusCode);
if (challengeData != null && challengeData.headerName != null) {
responseBuilder.header(challengeData.headerName.toString(), challengeData.headerContent);
}
if (LaunchMode.isDev() && exception.getMessage() != null) {
responseBuilder.entity(exception.getMessage());
}
log.debugf("Returning an authentication challenge, status code: %d", challengeData.status);
return status.build();
log.debugf("Returning an authentication challenge, status code: %d", statusCode);
return responseBuilder.build();
} else {
log.error("HttpAuthenticator is not found, returning HTTP status 401");
}
} else {
log.error("RoutingContext is not found, returning HTTP status 401");
}
if (LaunchMode.isDev() && exception.getMessage() != null) {
return Response.status(401).entity(exception.getMessage()).build();
}
return Response.status(401).entity("Not Authenticated").build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package io.quarkus.resteasy.reactive.server.test.security;

import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

import io.quarkus.security.Authenticated;
import io.quarkus.security.AuthenticationCompletionException;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AuthenticationRequest;
import io.quarkus.security.identity.request.CertificateAuthenticationRequest;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.restassured.RestAssured;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

public abstract class AbstractAuthFailureResponseBodyDevModeTest {

private static final String RESPONSE_BODY = "failure";

public enum AuthFailure {
AUTH_FAILED_WITH_BODY(() -> new AuthenticationFailedException(RESPONSE_BODY), true),
AUTH_COMPLETION_WITH_BODY(() -> new AuthenticationCompletionException(RESPONSE_BODY), true),
AUTH_FAILED_WITHOUT_BODY(AuthenticationFailedException::new, false),
AUTH_COMPLETION_WITHOUT_BODY(AuthenticationCompletionException::new, false);

public final Supplier<Throwable> failureSupplier;
private final boolean expectBody;

AuthFailure(Supplier<Throwable> failureSupplier, boolean expectBody) {
this.failureSupplier = failureSupplier;
this.expectBody = expectBody;
}
}

@Test
public void testAuthenticationFailedExceptionBody() {
assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, false);
assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, true);
assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, false);
assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, true);
}

@Test
public void testAuthenticationCompletionExceptionBody() {
assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITHOUT_BODY, false);
assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITH_BODY, false);
}

private static void assertExceptionBody(AuthFailure failure, boolean challenge) {
RestAssured
.given()
.redirects().follow(false)
.header("auth-failure", failure.toString())
.header("challenge-data", challenge)
.get("/secured")
.then()
.statusCode(challenge ? 302 : 401)
.body(failure.expectBody ? Matchers.equalTo(RESPONSE_BODY)
: Matchers.not(Matchers.containsString(RESPONSE_BODY)));
}

@Authenticated
@Path("secured")
public static class SecuredResource {

@GET
public String ignored() {
return "ignored";
}

}

@ApplicationScoped
public static class FailingAuthenticator implements HttpAuthenticationMechanism {

@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
return Uni.createFrom().failure(getFailureProducer(context));
}

private static Supplier<Throwable> getFailureProducer(RoutingContext context) {
return getAuthFailure(context).failureSupplier;
}

private static AuthFailure getAuthFailure(RoutingContext context) {
return AuthFailure.valueOf(context.request().getHeader("auth-failure"));
}

@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
// so that we don't need to implement an identity provider
return Collections.singleton(CertificateAuthenticationRequest.class);
}

@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
if (Boolean.parseBoolean(context.request().getHeader("challenge-data"))) {
return Uni.createFrom().item(new ChallengeData(302, null, null));
} else {
return Uni.createFrom().optional(Optional.empty());
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.resteasy.reactive.server.test.security;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusDevModeTest;

public class LazyAuthFailureResponseBodyDevModeTest extends AbstractAuthFailureResponseBodyDevModeTest {

@RegisterExtension
static QuarkusDevModeTest runner = new QuarkusDevModeTest()
.withApplicationRoot((jar) -> jar
.addClasses(SecuredResource.class, FailingAuthenticator.class, AuthFailure.class)
.addAsResource(new StringAsset("""
quarkus.http.auth.proactive=false
"""), "application.properties"));

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.quarkus.resteasy.reactive.server.test.security;

import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusDevModeTest;

public class ProactiveAuthFailureResponseBodyDevModeTest extends AbstractAuthFailureResponseBodyDevModeTest {

@RegisterExtension
static QuarkusDevModeTest runner = new QuarkusDevModeTest()
.withApplicationRoot((jar) -> jar
.addClasses(SecuredResource.class, FailingAuthenticator.class, AuthFailure.class));

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;

import io.quarkus.runtime.LaunchMode;
import io.quarkus.security.AuthenticationCompletionException;

public class AuthenticationCompletionExceptionMapper implements ExceptionMapper<AuthenticationCompletionException> {

@Override
public Response toResponse(AuthenticationCompletionException ex) {
if (LaunchMode.isDev() && ex.getMessage() != null) {
return Response.status(Response.Status.UNAUTHORIZED).entity(ex.getMessage()).build();
}
return Response.status(Response.Status.UNAUTHORIZED).build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

import org.jboss.resteasy.reactive.server.ServerExceptionMapper;

import io.quarkus.runtime.LaunchMode;
import io.quarkus.security.AuthenticationFailedException;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

public class AuthenticationFailedExceptionMapper {

@ServerExceptionMapper(value = AuthenticationFailedException.class, priority = Priorities.USER + 1)
public Uni<Response> handle(RoutingContext routingContext) {
return SecurityExceptionMapperUtil.handleWithAuthenticator(routingContext);
public Uni<Response> handle(RoutingContext routingContext, AuthenticationFailedException exception) {
return SecurityExceptionMapperUtil.handleWithAuthenticator(routingContext,
LaunchMode.isDev() ? exception.getMessage() : null);
}
}
Loading

0 comments on commit cb7ea96

Please sign in to comment.