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

Allow to customize OIDC JavaRequest checks #34812

Merged
merged 1 commit into from
Jul 18, 2023
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
29 changes: 27 additions & 2 deletions docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1069,11 +1069,36 @@

You can check if implementing single-page applications (SPAs) the way it is suggested in the xref:security-oidc-bearer-token-authentication.adoc#single-page-applications[Single-page applications] section of the "OpenID Connect (OIDC) Bearer token authentication" guide meets your requirements.

If you prefer to use SPAs and JavaScript APIs such as `Fetch` or `XMLHttpRequest`(XHR) with Quarkus web applications, be aware that OpenID Connect providers might not support cross-origin resource sharing (CORS) for authorization endpoints where the users are authenticated after a redirect from Quarkus.

Check warning on line 1072 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer. Raw Output: {"message": "[Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 1072, "column": 1}}}, "severity": "INFO"}
This will lead to authentication failures if the Quarkus application and the OpenID Connect provider are hosted on different HTTP domains, ports, or both.

In such cases, set the `quarkus.oidc.authentication.java-script-auto-redirect` property to `false`, which will instruct Quarkus to return a `499` status code and a `WWW-Authenticate` header with the `OIDC` value.
You must also update the browser script to set the `X-Requested-With` header with the `JavaScript` value and reload the last requested page in case of a `499` status code.
In such cases, set the `quarkus.oidc.authentication.java-script-auto-redirect` property to `false`, which will instruct Quarkus to return a `499` status code and a `WWW-Authenticate` header with the `OIDC` value.

The browser script must set a header to identify the current request as a JavaScript request for `499` status code to be returned when `quarkus.oidc.authentication.java-script-auto-redirect` property is set to `false`.

If the script engine sets an engine-specific request header itself, then you can register a custom `quarkus.oidc.JavaScriptRequestChecker` bean, which will inform Quarkus if the current request is a JavaScript request. For example, if the JavaScript engine sets a header such as `HX-Request: true` then you can have it checked like this:

[source,java]
----
import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.oidc.JavaScriptRequestChecker;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomJavaScriptRequestChecker implements JavaScriptRequestChecker {

@Override
public boolean isJavaScriptRequest(RoutingContext context) {
return "true".equals(context.request().getHeader("HX-Request"));
}
}
----

and reload the last requested page in case of a `499` status code.

Otherwise you must also update the browser script to set the `X-Requested-With` header with the `JavaScript` value and reload the last requested page in case of a `499` status code.

For example:

[source,javascript]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.quarkus.oidc;

import io.vertx.ext.web.RoutingContext;

/**
* JavaScriptRequestChecker can be used to check if the current request was made
* by JavaScript running inside Single-page application (SPA).
* <p/>
* Some OpenId Connect providers may not support CORS in their authorization endpoints.
* In such cases, SPA needs to avoid using JavaScript for running authorization code flow redirects
* and instead delegate it to the browser.
* <p/>
* If this checker confirms it is a JavaScript request and if authentication challenge redirects are also disabled with
* 'quarkus.oidc.authentication.java-script-auto-redirect=false' then an HTTP error status `499` will be reported allowing
* SPA to intercept this error and repeat the last request causing the challenge with the browser API.
*/
public interface JavaScriptRequestChecker {
/**
* Check if the current request was made by JavaScript
*
* @param context {@link RoutingContext}
* @return true if the current request was made by JavaScript
*/
boolean isJavaScriptRequest(RoutingContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -907,11 +907,16 @@ public enum ResponseMode {
/**
* If this property is set to 'true' then a normal 302 redirect response will be returned
* if the request was initiated via JavaScript API such as XMLHttpRequest or Fetch and the current user needs to be
* (re)authenticated which may not be desirable for Single Page Applications since
* (re)authenticated which may not be desirable for Single-page applications (SPA) since
* it automatically following the redirect may not work given that OIDC authorization endpoints typically do not support
* CORS.
* If this property is set to `false` then a status code of '499' will be returned to allow
* the client to handle the redirect manually
* <p/>
* If this property is set to 'false' then a status code of '499' will be returned to allow
* SPA to handle the redirect manually if a request header identifying current request as a JavaScript request is found.
* 'X-Requested-With' request header with its value set to either `JavaScript` or `XMLHttpRequest` is expected by
* default if
* this property is enabled. You can register a custom {@linkplain JavaScriptRequestChecker} to do a custom JavaScript
* request check instead.
*/
@ConfigItem(defaultValue = "true")
public boolean javaScriptAutoRedirect = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import io.quarkus.logging.Log;
import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.IdTokenCredential;
import io.quarkus.oidc.JavaScriptRequestChecker;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.Authentication;
import io.quarkus.oidc.OidcTenantConfig.Authentication.ResponseMode;
Expand Down Expand Up @@ -511,6 +512,10 @@ private boolean isIdTokenRequired(TenantConfigContext configContext) {
}

private boolean isJavaScript(RoutingContext context) {
JavaScriptRequestChecker checker = resolver.getJavaScriptRequestChecker();
if (checker != null) {
return checker.isJavaScriptRequest(context);
}
String value = context.request().getHeader("X-Requested-With");
return "JavaScript".equals(value) || "XMLHttpRequest".equals(value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;

import io.quarkus.oidc.JavaScriptRequestChecker;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.SecurityEvent;
Expand Down Expand Up @@ -41,6 +42,9 @@ public class DefaultTenantConfigResolver {
@Inject
Instance<TenantConfigResolver> tenantConfigResolver;

@Inject
Instance<JavaScriptRequestChecker> javaScriptRequestChecker;

@Inject
TenantConfigBean tenantConfigBean;

Expand Down Expand Up @@ -83,6 +87,9 @@ public void verifyResolvers() {
if (userInfoCache.isAmbiguous()) {
throw new IllegalStateException("Multiple " + UserInfo.class + " beans registered");
}
if (javaScriptRequestChecker.isAmbiguous()) {
throw new IllegalStateException("Multiple " + JavaScriptRequestChecker.class + " beans registered");
}
}

Uni<OidcTenantConfig> resolveConfig(RoutingContext context) {
Expand Down Expand Up @@ -240,6 +247,10 @@ public TenantConfigBean getTenantConfigBean() {
return tenantConfigBean;
}

public JavaScriptRequestChecker getJavaScriptRequestChecker() {
return javaScriptRequestChecker.isResolvable() ? javaScriptRequestChecker.get() : null;
}

private class DefaultStaticTenantResolver implements TenantResolver {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

public class TokenCustomizerFinder {

private TokenCustomizerFinder() {

}

public static TokenCustomizer find(OidcTenantConfig oidcConfig) {
if (oidcConfig == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.oidc.JavaScriptRequestChecker;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomJavaScriptRequestChecker implements JavaScriptRequestChecker {

@Override
public boolean isJavaScriptRequest(RoutingContext context) {
return "true".equals(context.request().getHeader("HX-Request"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ quarkus.oidc.tenant-web-app.credentials.secret=secret
quarkus.oidc.tenant-web-app.application-type=web-app
quarkus.oidc.tenant-web-app.roles.source=userinfo
quarkus.oidc.tenant-web-app.allow-user-info-cache=false
# Adding this property should not affect the flow if no expected request header
# "HX-Request" identifiying it as a JavaScript request is found
quarkus.oidc.tenant-web-app.authentication.java-script-auto-redirect=false

# Tenant Web App Java Script
quarkus.oidc.tenant-web-app-javascript.auth-server-url=${keycloak.url}/realms/quarkus-webapp
quarkus.oidc.tenant-web-app-javascript.client-id=quarkus-app-webapp
quarkus.oidc.tenant-web-app-javascript.credentials.secret=secret
quarkus.oidc.tenant-web-app-javascript.authentication.java-script-auto-redirect=false
quarkus.oidc.tenant-web-app-javascript.application-type=web-app

# Tenant Web App No Discovery (Introspection + User Info)
quarkus.oidc.tenant-web-app-no-discovery.auth-server-url=${keycloak.url}/realms/quarkus-webapp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.net.URI;
Expand All @@ -16,6 +17,7 @@

import org.junit.jupiter.api.Test;

import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebRequest;
Expand Down Expand Up @@ -86,6 +88,22 @@ public void testResolveTenantIdentifierWebApp() throws IOException {
}
}

@Test
public void testJavaScriptRequest() throws IOException, InterruptedException {
try (final WebClient webClient = createWebClient()) {
try {
webClient.addRequestHeader("HX-Request", "true");
webClient.getPage("http://localhost:8081/tenant/tenant-web-app-javascript/api/user/webapp");
fail("499 status error is expected");
} catch (FailingHttpStatusCodeException ex) {
assertEquals(499, ex.getStatusCode());
assertEquals("OIDC", ex.getResponse().getResponseHeaderValue("WWW-Authenticate"));
}

webClient.getCookieManager().clearCookies();
}
}

@Test
public void testResolveTenantIdentifierWebApp2() throws IOException {
try (final WebClient webClient = createWebClient()) {
Expand Down
Loading