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

Include missing Authentication failure messages in HTTP response in devmode #45691

Merged
Merged
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
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,129 @@
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) {
int statusCode = challenge ? 302 : 401;
boolean expectBody = failure.expectBody && statusCode == 401;
RestAssured
.given()
.redirects().follow(false)
.header("auth-failure", failure.toString())
.header("challenge-data", challenge)
.get("/secured")
.then()
.statusCode(statusCode)
.body(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 && statusCode == 401) {
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,119 @@
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) {
int statusCode = challenge ? 302 : 401;
boolean expectBody = failure.expectBody && statusCode == 401;
RestAssured
.given()
.redirects().follow(false)
.header("auth-failure", failure.toString())
.header("challenge-data", challenge)
.get("/secured")
.then()
.statusCode(statusCode)
.body(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
Loading