Skip to content

Commit f87ad4c

Browse files
authored
Merge pull request #45691 from michalvavrik/feature/auth-failed-ex-with-attrs
Include missing Authentication failure messages in HTTP response in devmode
2 parents 665c842 + c9266ce commit f87ad4c

File tree

14 files changed

+351
-28
lines changed

14 files changed

+351
-28
lines changed

core/runtime/src/main/java/io/quarkus/runtime/LaunchMode.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public boolean isDevOrTest() {
2020
return this != NORMAL;
2121
}
2222

23+
public static boolean isDev() {
24+
return current() == DEVELOPMENT;
25+
}
26+
2327
/**
2428
* Returns true if the current launch is the server side of remote dev.
2529
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package io.quarkus.resteasy.test.security;
2+
3+
import java.util.Collections;
4+
import java.util.Optional;
5+
import java.util.Set;
6+
import java.util.function.Supplier;
7+
8+
import jakarta.enterprise.context.ApplicationScoped;
9+
import jakarta.ws.rs.GET;
10+
import jakarta.ws.rs.Path;
11+
12+
import org.hamcrest.Matchers;
13+
import org.jboss.shrinkwrap.api.asset.StringAsset;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.extension.RegisterExtension;
16+
17+
import io.quarkus.security.Authenticated;
18+
import io.quarkus.security.AuthenticationCompletionException;
19+
import io.quarkus.security.AuthenticationFailedException;
20+
import io.quarkus.security.identity.IdentityProviderManager;
21+
import io.quarkus.security.identity.SecurityIdentity;
22+
import io.quarkus.security.identity.request.AuthenticationRequest;
23+
import io.quarkus.security.identity.request.CertificateAuthenticationRequest;
24+
import io.quarkus.test.QuarkusDevModeTest;
25+
import io.quarkus.vertx.http.runtime.security.ChallengeData;
26+
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
27+
import io.restassured.RestAssured;
28+
import io.smallrye.mutiny.Uni;
29+
import io.vertx.ext.web.RoutingContext;
30+
31+
public class AuthenticationFailureResponseBodyDevModeTest {
32+
33+
private static final String RESPONSE_BODY = "failure";
34+
35+
public enum AuthFailure {
36+
AUTH_FAILED_WITH_BODY(() -> new AuthenticationFailedException(RESPONSE_BODY), true),
37+
AUTH_COMPLETION_WITH_BODY(() -> new AuthenticationCompletionException(RESPONSE_BODY), true),
38+
AUTH_FAILED_WITHOUT_BODY(AuthenticationFailedException::new, false),
39+
AUTH_COMPLETION_WITHOUT_BODY(AuthenticationCompletionException::new, false);
40+
41+
public final Supplier<Throwable> failureSupplier;
42+
private final boolean expectBody;
43+
44+
AuthFailure(Supplier<Throwable> failureSupplier, boolean expectBody) {
45+
this.failureSupplier = failureSupplier;
46+
this.expectBody = expectBody;
47+
}
48+
}
49+
50+
@RegisterExtension
51+
static QuarkusDevModeTest runner = new QuarkusDevModeTest()
52+
.withApplicationRoot((jar) -> jar
53+
.addClasses(SecuredResource.class, FailingAuthenticator.class, AuthFailure.class)
54+
.addAsResource(new StringAsset("""
55+
quarkus.http.auth.proactive=false
56+
"""), "application.properties"));
57+
58+
@Test
59+
public void testAuthenticationFailedExceptionBody() {
60+
assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, false);
61+
assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, true);
62+
assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, false);
63+
assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, true);
64+
}
65+
66+
@Test
67+
public void testAuthenticationCompletionExceptionBody() {
68+
assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITHOUT_BODY, false);
69+
assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITH_BODY, false);
70+
}
71+
72+
private static void assertExceptionBody(AuthFailure failure, boolean challenge) {
73+
int statusCode = challenge ? 302 : 401;
74+
boolean expectBody = failure.expectBody && statusCode == 401;
75+
RestAssured
76+
.given()
77+
.redirects().follow(false)
78+
.header("auth-failure", failure.toString())
79+
.header("challenge-data", challenge)
80+
.get("/secured")
81+
.then()
82+
.statusCode(statusCode)
83+
.body(expectBody ? Matchers.equalTo(RESPONSE_BODY) : Matchers.not(Matchers.containsString(RESPONSE_BODY)));
84+
}
85+
86+
@Authenticated
87+
@Path("secured")
88+
public static class SecuredResource {
89+
90+
@GET
91+
public String ignored() {
92+
return "ignored";
93+
}
94+
95+
}
96+
97+
@ApplicationScoped
98+
public static class FailingAuthenticator implements HttpAuthenticationMechanism {
99+
100+
@Override
101+
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
102+
return Uni.createFrom().failure(getFailureProducer(context));
103+
}
104+
105+
private static Supplier<Throwable> getFailureProducer(RoutingContext context) {
106+
return getAuthFailure(context).failureSupplier;
107+
}
108+
109+
private static AuthFailure getAuthFailure(RoutingContext context) {
110+
return AuthFailure.valueOf(context.request().getHeader("auth-failure"));
111+
}
112+
113+
@Override
114+
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
115+
// so that we don't need to implement an identity provider
116+
return Collections.singleton(CertificateAuthenticationRequest.class);
117+
}
118+
119+
@Override
120+
public Uni<ChallengeData> getChallenge(RoutingContext context) {
121+
if (Boolean.parseBoolean(context.request().getHeader("challenge-data"))) {
122+
return Uni.createFrom().item(new ChallengeData(302, null, null));
123+
} else {
124+
return Uni.createFrom().optional(Optional.empty());
125+
}
126+
}
127+
128+
}
129+
}

extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/AuthenticationCompletionExceptionMapper.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import org.jboss.logging.Logger;
88

9+
import io.quarkus.runtime.LaunchMode;
910
import io.quarkus.security.AuthenticationCompletionException;
1011

1112
@Provider
@@ -16,6 +17,9 @@ public class AuthenticationCompletionExceptionMapper implements ExceptionMapper<
1617
@Override
1718
public Response toResponse(AuthenticationCompletionException ex) {
1819
log.debug("Authentication has failed, returning HTTP status 401");
20+
if (LaunchMode.isDev() && ex.getMessage() != null) {
21+
return Response.status(401).entity(ex.getMessage()).build();
22+
}
1923
return Response.status(401).build();
2024
}
2125

extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/AuthenticationFailedExceptionMapper.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import org.jboss.logging.Logger;
1111

12+
import io.quarkus.runtime.LaunchMode;
1213
import io.quarkus.security.AuthenticationFailedException;
1314
import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
1415
import io.quarkus.vertx.http.runtime.security.ChallengeData;
@@ -19,7 +20,6 @@
1920
@Priority(Priorities.USER + 1)
2021
public class AuthenticationFailedExceptionMapper implements ExceptionMapper<AuthenticationFailedException> {
2122
private static final Logger log = Logger.getLogger(AuthenticationFailedExceptionMapper.class.getName());
22-
2323
private volatile CurrentVertxRequest currentVertxRequest;
2424

2525
CurrentVertxRequest currentVertxRequest() {
@@ -37,18 +37,25 @@ public Response toResponse(AuthenticationFailedException exception) {
3737
if (authenticator != null) {
3838
ChallengeData challengeData = authenticator.getChallenge(context)
3939
.await().indefinitely();
40-
Response.ResponseBuilder status = Response.status(challengeData.status);
41-
if (challengeData.headerName != null) {
42-
status.header(challengeData.headerName.toString(), challengeData.headerContent);
40+
int statusCode = challengeData == null ? 401 : challengeData.status;
41+
Response.ResponseBuilder responseBuilder = Response.status(statusCode);
42+
if (challengeData != null && challengeData.headerName != null) {
43+
responseBuilder.header(challengeData.headerName.toString(), challengeData.headerContent);
44+
}
45+
if (LaunchMode.isDev() && exception.getMessage() != null && statusCode == 401) {
46+
responseBuilder.entity(exception.getMessage());
4347
}
44-
log.debugf("Returning an authentication challenge, status code: %d", challengeData.status);
45-
return status.build();
48+
log.debugf("Returning an authentication challenge, status code: %d", statusCode);
49+
return responseBuilder.build();
4650
} else {
4751
log.error("HttpAuthenticator is not found, returning HTTP status 401");
4852
}
4953
} else {
5054
log.error("RoutingContext is not found, returning HTTP status 401");
5155
}
56+
if (LaunchMode.isDev() && exception.getMessage() != null) {
57+
return Response.status(401).entity(exception.getMessage()).build();
58+
}
5259
return Response.status(401).entity("Not Authenticated").build();
5360
}
5461
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package io.quarkus.resteasy.reactive.server.test.security;
2+
3+
import java.util.Collections;
4+
import java.util.Optional;
5+
import java.util.Set;
6+
import java.util.function.Supplier;
7+
8+
import jakarta.enterprise.context.ApplicationScoped;
9+
import jakarta.ws.rs.GET;
10+
import jakarta.ws.rs.Path;
11+
12+
import org.hamcrest.Matchers;
13+
import org.junit.jupiter.api.Test;
14+
15+
import io.quarkus.security.Authenticated;
16+
import io.quarkus.security.AuthenticationCompletionException;
17+
import io.quarkus.security.AuthenticationFailedException;
18+
import io.quarkus.security.identity.IdentityProviderManager;
19+
import io.quarkus.security.identity.SecurityIdentity;
20+
import io.quarkus.security.identity.request.AuthenticationRequest;
21+
import io.quarkus.security.identity.request.CertificateAuthenticationRequest;
22+
import io.quarkus.vertx.http.runtime.security.ChallengeData;
23+
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
24+
import io.restassured.RestAssured;
25+
import io.smallrye.mutiny.Uni;
26+
import io.vertx.ext.web.RoutingContext;
27+
28+
public abstract class AbstractAuthFailureResponseBodyDevModeTest {
29+
30+
private static final String RESPONSE_BODY = "failure";
31+
32+
public enum AuthFailure {
33+
AUTH_FAILED_WITH_BODY(() -> new AuthenticationFailedException(RESPONSE_BODY), true),
34+
AUTH_COMPLETION_WITH_BODY(() -> new AuthenticationCompletionException(RESPONSE_BODY), true),
35+
AUTH_FAILED_WITHOUT_BODY(AuthenticationFailedException::new, false),
36+
AUTH_COMPLETION_WITHOUT_BODY(AuthenticationCompletionException::new, false);
37+
38+
public final Supplier<Throwable> failureSupplier;
39+
private final boolean expectBody;
40+
41+
AuthFailure(Supplier<Throwable> failureSupplier, boolean expectBody) {
42+
this.failureSupplier = failureSupplier;
43+
this.expectBody = expectBody;
44+
}
45+
}
46+
47+
@Test
48+
public void testAuthenticationFailedExceptionBody() {
49+
assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, false);
50+
assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, true);
51+
assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, false);
52+
assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, true);
53+
}
54+
55+
@Test
56+
public void testAuthenticationCompletionExceptionBody() {
57+
assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITHOUT_BODY, false);
58+
assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITH_BODY, false);
59+
}
60+
61+
private static void assertExceptionBody(AuthFailure failure, boolean challenge) {
62+
int statusCode = challenge ? 302 : 401;
63+
boolean expectBody = failure.expectBody && statusCode == 401;
64+
RestAssured
65+
.given()
66+
.redirects().follow(false)
67+
.header("auth-failure", failure.toString())
68+
.header("challenge-data", challenge)
69+
.get("/secured")
70+
.then()
71+
.statusCode(statusCode)
72+
.body(expectBody ? Matchers.equalTo(RESPONSE_BODY)
73+
: Matchers.not(Matchers.containsString(RESPONSE_BODY)));
74+
}
75+
76+
@Authenticated
77+
@Path("secured")
78+
public static class SecuredResource {
79+
80+
@GET
81+
public String ignored() {
82+
return "ignored";
83+
}
84+
85+
}
86+
87+
@ApplicationScoped
88+
public static class FailingAuthenticator implements HttpAuthenticationMechanism {
89+
90+
@Override
91+
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
92+
return Uni.createFrom().failure(getFailureProducer(context));
93+
}
94+
95+
private static Supplier<Throwable> getFailureProducer(RoutingContext context) {
96+
return getAuthFailure(context).failureSupplier;
97+
}
98+
99+
private static AuthFailure getAuthFailure(RoutingContext context) {
100+
return AuthFailure.valueOf(context.request().getHeader("auth-failure"));
101+
}
102+
103+
@Override
104+
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
105+
// so that we don't need to implement an identity provider
106+
return Collections.singleton(CertificateAuthenticationRequest.class);
107+
}
108+
109+
@Override
110+
public Uni<ChallengeData> getChallenge(RoutingContext context) {
111+
if (Boolean.parseBoolean(context.request().getHeader("challenge-data"))) {
112+
return Uni.createFrom().item(new ChallengeData(302, null, null));
113+
} else {
114+
return Uni.createFrom().optional(Optional.empty());
115+
}
116+
}
117+
118+
}
119+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.quarkus.resteasy.reactive.server.test.security;
2+
3+
import org.jboss.shrinkwrap.api.asset.StringAsset;
4+
import org.junit.jupiter.api.extension.RegisterExtension;
5+
6+
import io.quarkus.test.QuarkusDevModeTest;
7+
8+
public class LazyAuthFailureResponseBodyDevModeTest extends AbstractAuthFailureResponseBodyDevModeTest {
9+
10+
@RegisterExtension
11+
static QuarkusDevModeTest runner = new QuarkusDevModeTest()
12+
.withApplicationRoot((jar) -> jar
13+
.addClasses(SecuredResource.class, FailingAuthenticator.class, AuthFailure.class)
14+
.addAsResource(new StringAsset("""
15+
quarkus.http.auth.proactive=false
16+
"""), "application.properties"));
17+
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.quarkus.resteasy.reactive.server.test.security;
2+
3+
import org.junit.jupiter.api.extension.RegisterExtension;
4+
5+
import io.quarkus.test.QuarkusDevModeTest;
6+
7+
public class ProactiveAuthFailureResponseBodyDevModeTest extends AbstractAuthFailureResponseBodyDevModeTest {
8+
9+
@RegisterExtension
10+
static QuarkusDevModeTest runner = new QuarkusDevModeTest()
11+
.withApplicationRoot((jar) -> jar
12+
.addClasses(SecuredResource.class, FailingAuthenticator.class, AuthFailure.class));
13+
14+
}

extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/AuthenticationCompletionExceptionMapper.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
import jakarta.ws.rs.core.Response;
44
import jakarta.ws.rs.ext.ExceptionMapper;
55

6+
import io.quarkus.runtime.LaunchMode;
67
import io.quarkus.security.AuthenticationCompletionException;
78

89
public class AuthenticationCompletionExceptionMapper implements ExceptionMapper<AuthenticationCompletionException> {
910

1011
@Override
1112
public Response toResponse(AuthenticationCompletionException ex) {
13+
if (LaunchMode.isDev() && ex.getMessage() != null) {
14+
return Response.status(Response.Status.UNAUTHORIZED).entity(ex.getMessage()).build();
15+
}
1216
return Response.status(Response.Status.UNAUTHORIZED).build();
1317
}
1418

extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/AuthenticationFailedExceptionMapper.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55

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

8+
import io.quarkus.runtime.LaunchMode;
89
import io.quarkus.security.AuthenticationFailedException;
910
import io.smallrye.mutiny.Uni;
1011
import io.vertx.ext.web.RoutingContext;
1112

1213
public class AuthenticationFailedExceptionMapper {
1314

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

0 commit comments

Comments
 (0)