Quarkus - OIDC Proxy

Introduction

OIDC Proxy extends Quarkus OIDC extension and adds OIDC authorization code flow support for Quarkus OIDC service applications by proxying OIDC authorization code flow requests and delegating them to the real OIDC provider which is configured for the current Quarkus OIDC service application.

It allows an integration of Quarkus OIDC service applications with the external Single-page applications (SPA) or Quarkus OIDC web-app applications which authenticate users with the OIDC authorization code without exposing internal OIDC configuration details. OIDC Proxy can also help when external SPAs can not implement an authorization code flow with some OIDC and OAuth2 providers but which are supported at the Quarkus OIDC level.

Installation

Add this Maven dependency:

<dependency>
    <groupId>io.quarkiverse.oidc-proxy</groupId>
    <artifactId>quarkus-oidc-proxy</artifactId>
</dependency>

Getting Started

Adding the OIDC proxy dependency enables the SPA to use the OIDC proxy endpoints to authenticate users with an authorization code flow and send the acquired access tokens to the Quarkus OIDC service application.

For example, if the endpoint is listening at http://localhost:8080, then, after adding this dependency, the OIDC authorization endpoint is available at http://localhost:8080/q/oidc/authorize , the OIDC token endpoint is available at http://localhost:8080/q/oidc/token, the OIDC JWKS endpoint is available at http://localhost:8080/q/oidc/jwks and finally, the OIDC well known configuration endpoint which supports the discovery is available at http://localhost:8080/q/oidc/.well-known/openid-configuration.

For example, the following configuration is enabling an OIDC proxy over a Quarkus OIDC Auth0 service application, without having to configure your Auth0 application to allow redirects to the SPA page:

quarkus.oidc.auth-server-url=https://${auth0-dev}.us.auth0.com (1)
quarkus.oidc.client-id=${auth0-client-id}
quarkus.oidc.credentials.secret=${auth0-client-secret}

quarkus.oidc.authentication.redirect-path=/q/oidc/callback (2)
quarkus.oidc-proxy.external-redirect-uri=${external-spa-redirect-url}
1 OIDC service application which can only accept and verify the bearer access tokens.
2 Let OIDC proxy accept callbacks at the /q/oidc/callback path and redirect to the actual SPA redirect path. The Auth0 application will only need to allow a redirect to http://localhost:8080/q/oidc/callback.

OIDC proxy root and individual endpoint paths can be customized. You can customize the /q/oidc OIDC proxy root path with a quarkus.oidc-proxy.root-path property. Each individual endpoint can also be customized.

For example, if you set quarkus.oidc-proxy.root-path to openid-connect and quarkus.oidc-proxy.authorization-path to /authorization, then you will get the OIDC authorization endpoint available at http://localhost:8080/openid-connect/authorization, etc.

OIDC proxy endpoints are public because they have to delegate to the real OIDC provider.

If you use a wildcard authentication or role-based access control HTTP policy, make sure to permit access to the OIDC proxy endpoints, for example:

quarkus.http.auth.permission.service.paths=/* (1)
quarkus.http.auth.permission.service.policy=authenticated

quarkus.http.auth.permission.oidcproxy.paths=/q/oidc/* (2)
quarkus.http.auth.permission.oidcproxy.policy=permit
1 Requests to all service endpoints and resources must be authenticated
2 Permit access to the OIDC proxy endpoints only

Testing

One relatively simple way to test OIDC Proxy is to use HtmlUnit to test a Quarkus OIDC web-app endpoint which is configured to use OIDC provider whose endpoints point to the OIDC Proxy. OIDC Proxy will delegate to Keycloak to manage an authorization code flow for this Quarkus OIDC web-app endpoint. Keycloak is launched by DevServices for Keycloak which support both dev and integration test modes.

The Quarkus OIDC web-app endpoint will use an access token provided by the OIDC Proxy to call the OIDC service endpoint.

Here is how you can create such a test.

Start by adding the following dependencies to your test project:

<dependency>
    <groupId>net.sourceforge.htmlunit</groupId>
    <artifactId>htmlunit</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
</dependency>

Write this test code:

package io.quarkus.oidc.proxy;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
import com.gargoylesoftware.htmlunit.TextPage;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;

import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class OidcProxyTestCase {

    @Test
    public void testOidcProxy() throws Exception {

        try (final WebClient webClient = createWebClient()) {
            HtmlPage page = webClient.getPage("http://localhost:8081/web-app");

            assertEquals("Sign in to quarkus", page.getTitleText());

            HtmlForm loginForm = page.getForms().get(0);

            loginForm.getInputByName("username").setValueAttribute("alice");
            loginForm.getInputByName("password").setValueAttribute("alice");

            TextPage textPage = loginForm.getInputByName("login").click();

            assertEquals("web-app: ID alice, service: Bearer alice", textPage.getContent());

            webClient.getCookieManager().clearCookies();
        }

    }

    private WebClient createWebClient() {
        WebClient webClient = new WebClient();
        webClient.setCssErrorHandler(new SilentCssErrorHandler());
        return webClient;
    }

}

Add an OIDC web-app endpoint which will require an authorization code flow to support HtmlUnit calls and propagate the access token to the service endpoint:

package io.quarkus.oidc.proxy;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;

@Path("/web-app")
@Authenticated
public class OidcWebAppResource {

    @Inject
    @IdToken
    JsonWebToken idToken;

    @Inject
    @RestClient
    ServiceApiClient serviceApiClient;

    @GET
    @Produces("text/plain")
    public Uni<String> getName() {
        return serviceApiClient.getName().onItem()
                .transform(c -> ("web-app: " + idToken.getClaim("typ") + " " + idToken.getName() + ", service: " + c));
    }
}

Add ServiceApiClient which OidcWebAppResource will call:

package io.quarkus.oidc.proxy;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.core.MediaType;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import io.quarkus.oidc.token.propagation.AccessToken;
import io.smallrye.mutiny.Uni;

@RegisterRestClient(configKey = "service-api-client")
@AccessToken
public interface ServiceApiClient {

    @GET
    @Consumes(MediaType.TEXT_PLAIN)
    Uni<String> getName();
}

An OIDC service endpoint may look like this:

package io.quarkus.oidc.proxy;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.security.Authenticated;

@Path("/service")
@Authenticated
public class OidcServiceResource {

    @Inject
    JsonWebToken accessToken;

    @GET
    @Produces("text/plain")
    public String getName() {
        return accessToken.getClaim("typ") + " " + accessToken.getName();
    }
}

Finally, configure application.properties, note that DevServices for Keycloak will configure the OIDC service endpoint by setting its quarkus.oidc.auth-server-url, quarkus.oidc.client-id and quarkus.oidc.credentials.secret properties.

# Default OIDC tenant which supports the OIDC `service` endpoint is setup by DevServices for Keycloak

# The OIDC `web-app` tenant supports the OIDC `web-app` endpoint
quarkus.oidc.web-app.auth-server-url=http://localhost:8081/q/oidc (1)
quarkus.oidc.web-app.client-id=${quarkus.oidc.client-id}
quarkus.oidc.web-app.credentials.secret=secret
quarkus.oidc.web-app.application-type=web-app
quarkus.oidc.web-app.authentication.cookie-path=/web-app (2)
quarkus.rest-client.service-api-client.url=http://localhost:8081/service
1 OIDC Proxy sets all the required routes using the /q/oidc root path, as explained in the Getting Started section. All the individial endpoint addresses are auto-discovered but you can also configure them individually.
2 OIDC session cookie path is limited to the OIDC web-app endpoint path only to avoid it being recognized during the OIDC web-app endpoint propagating the access tokens to the service endpoint. It is only done to support this test setup but is not necessary if the service endpoint is not collocated with the web-app endpoint.

Next, once the integration test passes with these application.properties, you can add more tests verifying other OIDC Proxy properties.

Extension Configuration Reference

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

Type

Default

If the OIDC Proxy extension is enabled.

Environment variable: QUARKUS_OIDC_PROXY_ENABLED

boolean

true

OIDC service tenant identifier which can be set to select an OIDC tenant configuration. The default OIDC tenant configuration is used when this property is not set.

Environment variable: QUARKUS_OIDC_PROXY_TENANT_ID

string

OIDC proxy root path.

Environment variable: QUARKUS_OIDC_PROXY_ROOT_PATH

string

/q/oidc

OIDC proxy authorization endpoint path relative to the root-path().

Environment variable: QUARKUS_OIDC_PROXY_AUTHORIZATION_PATH

string

/authorize

OIDC proxy token endpoint path relative to the root-path()

Environment variable: QUARKUS_OIDC_PROXY_TOKEN_PATH

string

/token

OIDC proxy JSON Web Key Set endpoint path relative to the root-path()

Environment variable: QUARKUS_OIDC_PROXY_JWKS_PATH

string

/jwks

OIDC proxy UserInfo endpoint path relative to the root-path(). This path will not be supported if allow-id-token() is set to false.

Environment variable: QUARKUS_OIDC_PROXY_USER_INFO_PATH

string

/userinfo

Allow to return an ID token from the authorization code grant response.

Environment variable: QUARKUS_OIDC_PROXY_ALLOW_ID_TOKEN

boolean

true

Allow to return a refresh token from the authorization code grant response.

Environment variable: QUARKUS_OIDC_PROXY_ALLOW_REFRESH_TOKEN

boolean

true

Absolute external redirect URI.

If 'quarkus.oidc.authentication.redirect-path' is configured then configuring this property is required. In this case, the proxy will request a redirect to 'quarkus.oidc.authentication.redirect-path' and will redirect further to the external redirect URI.

Environment variable: QUARKUS_OIDC_PROXY_EXTERNAL_REDIRECT_URI

string

Client id that the external client must use. If this property is not set then the external client must provide a client_id which matches quarkus.oidc.client-id.

Environment variable: QUARKUS_OIDC_PROXY_EXTERNAL_CLIENT_ID

string

Client secret that the external client must use. If this property is not set then the external client must provide a client secret which matches the configured OIDC service client secret. External clients do not have to provide the client secret if it is not configured with either this property or the OIDC tenant configuration, in order to support public clients.

Environment variable: QUARKUS_OIDC_PROXY_EXTERNAL_CLIENT_SECRET

string