Skip to content
Draft
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
70 changes: 70 additions & 0 deletions tck/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,76 @@ This sub-repo contains working applications that demonstrate and test various as
* Test URL: http://localhost:8080/app-securitycontext-auth/servlet?name=rezax (fails authentication via exception)
* Test URL: http://localhost:8080/app-securitycontext-auth/servlet?name=rezax (fails authentication via status return code)

## Running tests in parallel (`mvn -T<N>`)

The default `mvn verify` already uses the GlassFish pool (provisioned by
`glassfish-pool-maven-plugin`, started/cloned per slot, leased by each test
JVM). Adding `-T<N>` runs reactor modules in parallel and is a large
wall-clock win, 10x faster on average.

The pool itself is parallel-safe (`PoolBootstrap.up` is JVM-wide synchronized
+ idempotent, slot leasing uses `FileChannel.tryLock`), but test modules
have to follow a few rules to be `-T`-safe. Existing modules already comply;
when adding a new one, check the points below.

### 1. No host-port collisions across modules

Modules that start an embedded server bound to `localhost:<port>` (UnboundID
LDAP, Tomcat for the Mitre OP, …) must each pick a distinct port. Under `-T`
two modules on the same port fight: only one binds, the other silently
fails, and tests get cryptic 500s or HTTP timeouts.

Conventions in use:

- LDAP modules: 33389 (`app-ldap`), 33390 (`app-ldap2`), 33391 (`app-ldap3`),
12389-12413 for `app-ldap-*`. Pick the next free integer when adding one.
- Tomcat (Mitre OP) modules: 8443 + 8005 (`app-openid2`), 8444 + 8006
(`app-openid3`). Pick another (8445/8007, …) for any new openid-with-Mitre
module, and keep `server.xml` + the `ProtectedServlet` `providerURI`
annotation + the antrun `<replace token="http://localhost:8080" value="…">`
in sync.

### 2. No assumption that GF runs on a known port

Pool slots get ports from `adminBase + (slot-1) * portStride` (default
14848 + N*100), and a test JVM may lease any slot. Do NOT hardcode a slot's
HTTP/HTTPS port in app code. Use `@ArquillianResource URL base` for the
deployed-app URL; for outbound URLs that have to be configured at deployment
time (e.g. Soteria's `OpenIdAuthenticationMechanismDefinition.providerURI`),
use an EL expression backed by a `@RequestScoped`/`@Dependent` CDI bean that
reads `request.getServerName()/getServerPort()` at request time —
`app-openid`'s `OpenIdConfig.getProviderURI()` is the reference.

### 3. Pre-register every slot when an external service validates redirect URIs

When a third-party server (e.g. Mitre OP) validates redirect URIs against a
fixed allowlist, register one entry per *possible* slot. The openid-client
deployment may end up on slot 1, 2, … N, and Mitre rejects any redirect URI
not pre-registered. `app-openid2`/`app-openid3`'s antrun loops slot
1..`${session.request.degreeOfConcurrency}` into `clients.sql` using the
pool's `adminBase` + `portStride` — that property is Maven's `-TN` value
(defaults to 1) and is also the upper bound on how far the pool can grow,
since each Maven thread leases at most one slot at a time.

### 4. Wipe Tomcat `work/` before startup

If a module starts its own Tomcat in pre-integration-test, add
`<delete dir="${tomcat.dir}/work" quiet="true"/>` to the antrun *before*
`startup.sh`. Tomcat's `StandardManager` persists HTTP sessions to
`work/Catalina/localhost/<webapp>/SESSIONS.ser` on shutdown and rehydrates
them at startup; without the wipe, a re-run without `mvn clean` resurrects
the previous run's sessions and can skip flows the test depends on (e.g.
the OpenID consent page).

### 5. Don't race on shared paths in a `<plugins>` execution

Anything inheritable that writes to `${maven.multiModuleProjectDirectory}/…`
runs once per module under `-T` and races. The parent's source-staging step
uses a `mkdir`-based lock + marker file inside an `antrun` so first-acquirer
does the work and others fast-exit; copy that pattern for any new shared
preparation. Plain `maven-dependency-plugin:unpack` into a shared directory
is NOT thread-safe for the first-extraction window even with markers.

## Running the TCK in Docker

(needs updating to recent versions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import jakarta.ejb.Startup;

/**
* Starts up the embedded Unboundid LDAP server on port 33389 and loads a test directory
* Starts up the embedded Unboundid LDAP server on port 33390 and loads a test directory
* into it containing the same caller- and roles names as the Database and Embedded idenity
* stores are using.
*
Expand All @@ -45,7 +45,7 @@ public void init() {
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=net");
config.setListenerConfigs(
new InMemoryListenerConfig("myListener", null, 33389, null, null, null));
new InMemoryListenerConfig("myListener", null, 33390, null, null, null));

directoryServer = new InMemoryDirectoryServer(config);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
* this caller is in any of the roles {foo, bar, kaz}
*/
@LdapIdentityStoreDefinition(
url = "ldap://localhost:33389/",
url = "ldap://localhost:33390/",
bindDn = "uid=ldap,ou=apps,dc=jsr375,dc=net",
bindDnPassword = "changeOnInstall",
callerSearchBase = "dc=jsr375,dc=net",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import jakarta.ejb.Startup;

/**
* Starts up the embedded Unboundid LDAP server on port 33389 and loads a test directory
* Starts up the embedded Unboundid LDAP server on port 33391 and loads a test directory
* into it containing the same caller- and roles names as the Database and Embedded idenity
* stores are using.
*
Expand All @@ -45,7 +45,7 @@ public void init() {
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=net");
config.setListenerConfigs(
new InMemoryListenerConfig("myListener", null, 33389, null, null, null));
new InMemoryListenerConfig("myListener", null, 33391, null, null, null));

directoryServer = new InMemoryDirectoryServer(config);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
* this caller is in any of the roles {foo, bar, kaz}
*/
@LdapIdentityStoreDefinition(
url = "ldap://localhost:33389/",
url = "ldap://localhost:33391/",
bindDn = "uid=ldap,ou=apps,dc=jsr375,dc=net",
bindDnPassword = "changeOnInstall",
callerSearchBase = "dc=jsr375,dc=net",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@

import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.servlet.http.HttpServletRequest;

@Named
@Dependent
Expand All @@ -35,6 +37,9 @@ public class OpenIdConfig {

private Properties config;

@Inject
private HttpServletRequest request;

@PostConstruct
public void init() {
config = new Properties();
Expand Down Expand Up @@ -71,4 +76,14 @@ public String getClientSecret() {

return OidcProvider.CLIENT_SECRET_VALUE;
}

/**
* Provider URI computed from the live request's host:port so the test runs
* against whatever HTTP listener the GlassFish slot has bound (the dist's
* default 8080, the pool's 14849, etc.) without recompiling.
*/
public String getProviderURI() {
return "http://" + request.getServerName() + ":" + request.getServerPort()
+ "/openid-server/webresources/oidc-provider-demo";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
*/
@WebServlet("/Secured")
@OpenIdAuthenticationMechanismDefinition(
providerURI = "http://localhost:8080/openid-server/webresources/oidc-provider-demo",
providerURI = "${openIdConfig.providerURI}",
clientId = CLIENT_ID_VALUE,
clientSecret = CLIENT_SECRET_VALUE,
redirectURI = "${baseURL}/Callback")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
*/
@WebServlet("/Secured")
@OpenIdAuthenticationMechanismDefinition(
providerURI = "http://localhost:8080/openid-server/webresources/oidc-provider-demo",
providerURI = "${openIdConfig.providerURI}",
clientId = "${openIdConfig.clientId}",
clientSecret = "${openIdConfig.clientSecret}",
redirectURI = "${openIdConfig.redirectURI}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.UriInfo;

/**
* @author Gaurav Gupta
Expand All @@ -97,6 +99,18 @@ public class OidcProvider {

private static final String HTTPS_HOST = "https://localhost:";

/**
* Hardcoded base URL inside the static openid-configuration.json template
* and the JWT issuer claim. Rewritten at request time to {@link #issuer()}
* so the metadata and tokens match whatever HTTP listener GlassFish is
* actually bound to (the dist's default 8080, the pool's 14849, etc.).
*/
private static final String TEMPLATE_BASE_URL =
"http://localhost:8080/openid-server/webresources/oidc-provider-demo";

@Context
private UriInfo uriInfo;

private static String nonce;

boolean rolesInUserInfoEndpoint;
Expand Down Expand Up @@ -133,21 +147,20 @@ public Response getConfiguration() {
}
} catch (IOException ex) {}

if (oidcProviderHttpsPort != null && !oidcProviderHttpsPort.isEmpty()) {
String httpsHostAndPort = HTTPS_HOST + oidcProviderHttpsPort;
result = useHttpsHostAndPort(result, "http://localhost:8080/openid-server/webresources/oidc-provider-demo/auth", httpsHostAndPort);
result = useHttpsHostAndPort(result, "http://localhost:8080/openid-server/webresources/oidc-provider-demo/token", httpsHostAndPort);
result = useHttpsHostAndPort(result, "http://localhost:8080/openid-server/webresources/oidc-provider-demo/userinfo", httpsHostAndPort);
result = useHttpsHostAndPort(result, "http://localhost:8080/openid-server/webresources/oidc-provider-demo/revoke", httpsHostAndPort);
result = useHttpsHostAndPort(result, "http://localhost:8080/openid-server/webresources/oidc-provider-demo/certs", httpsHostAndPort);
}
// Rewrite every TEMPLATE_BASE_URL/<path> in the metadata to either the live
// HTTPS host:port (when configured) or the live HTTP base. Done in one pass
// so the issuer URL also tracks the live request.
String liveBase = (oidcProviderHttpsPort != null && !oidcProviderHttpsPort.isEmpty())
? HTTPS_HOST + oidcProviderHttpsPort + "/openid-server/webresources/oidc-provider-demo"
: issuer();
result = result.replace(TEMPLATE_BASE_URL, liveBase);

return Response.ok(result).header("Access-Control-Allow-Origin", "*").build();
}

private String useHttpsHostAndPort(String result, String endpoint, String httpsHostAndPort) {
String path = endpoint.substring(21);
return result.replace(endpoint, httpsHostAndPort + path);
/** Live request's base URL for this resource — used as both metadata issuer and JWT iss claim. */
private String issuer() {
return uriInfo.getBaseUriBuilder().path("oidc-provider-demo").build().toString();
}

@GET
Expand Down Expand Up @@ -209,7 +222,7 @@ public Response tokenEndpoint(
.build();

JWTClaimsSet.Builder jwtClaimsBuilder = new JWTClaimsSet.Builder()
.issuer("http://localhost:8080/openid-server/webresources/oidc-provider-demo")
.issuer(issuer())
.subject(getSubject())
.audience(List.of(CLIENT_ID_VALUE))
.expirationTime(new Date(now.getTime() + 1000 * 60 * 10))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import ee.jakarta.tck.security.test.client.CallbackServlet;
import ee.jakarta.tck.security.test.client.UnsecuredServlet;
import ee.jakarta.tck.security.test.client.UserNameServlet;
import ee.jakarta.tck.security.test.client.defaulttests.OpenIdConfig;
import ee.jakarta.tck.security.test.server.ApplicationConfig;
import ee.jakarta.tck.security.test.server.OidcProvider;

Expand Down Expand Up @@ -81,6 +82,9 @@ public static WebArchive createClientDeployment(Class<?>... additionalClasses) {
.addClass(CallbackServlet.class)
.addClass(UnsecuredServlet.class)
.addClass(UserNameServlet.class)
// OpenIdConfig is the @Named CDI bean now backing every Secured*
// servlet's providerURI EL expression — needed by all client deployments.
.addClass(OpenIdConfig.class)
.addClasses(additionalClasses)
.addAsWebInfResource("beans.xml");

Expand Down
59 changes: 20 additions & 39 deletions tck/app-openid2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,30 @@
<replace token="http://localhost:8080" value="https://localhost:8443" dir="${tomcat.dir}/webapps/openid-connect-server-webapp/WEB-INF" summary="yes">
<include name="server-config.xml" />
</replace>
<replace token="http://localhost/" value="http://localhost:8080/openid-client/Callback" dir="${tomcat.dir}/webapps/openid-connect-server-webapp/WEB-INF/classes/db/hsql" summary="yes">
<include name="clients.sql" />
</replace>
<!--
Register the openid-client Callback URL for every slot the
pool may grow to (= ${maven.degreeOfConcurrency}, ie -TN).
Mitre rejects redirect_uris not pre-registered, and the
openid-client may land on any leased slot. Slot N's HTTP
port = adminBase + (N-1)*portStride + 1.
-->
<exec executable="bash" failonerror="true">
<arg value="-c"/>
<arg value="set -e; F='${tomcat.dir}/webapps/openid-connect-server-webapp/WEB-INF/classes/db/hsql/clients.sql'; awk -v base=${gf.pool.adminBase} -v stride=${gf.pool.portStride} -v slots=${maven.degreeOfConcurrency} -v marker=&quot;'http://localhost/'&quot; 'index($0, marker) { for (i=1; i&lt;=slots; i++) { port = base + (i-1)*stride + 1; printf &quot;\t(%cclient%c, %chttp://localhost:%d/openid-client/Callback%c),\n&quot;, 39, 39, 39, port, 39 } next } { print }' &quot;$F&quot; &gt; &quot;${F}.new&quot; &amp;&amp; mv &quot;${F}.new&quot; &quot;$F&quot;"/>
</exec>

<copy file="server.xml" todir="${tomcat.dir}/conf" verbose="true" overwrite="true" force="true"/>
<copy file="localhost-rsa.jks" todir="${tomcat.dir}/conf" verbose="true"/>

<chmod dir="${tomcat.dir}/bin" perm="ugo+rx" includes="*" />

<!--
Drop Tomcat's persisted SESSIONS.ser so a re-run without
`mvn clean` doesn't resurrect Mitre's "client already
authorized" session and skip the consent page.
-->
<delete dir="${tomcat.dir}/work" quiet="true"/>

<exec executable="${tomcat.dir}/bin/startup.sh" dir="${tomcat.dir}" >
<env key="CATALINA_PID" value="${tomcat.pidfile}" />
</exec>
Expand Down Expand Up @@ -172,42 +187,8 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>keytool-maven-plugin</artifactId>
<version>2.0.2</version>
<executions>
<execution>
<id>import-tomcat-cert</id>
<phase>pre-integration-test</phase>
<goals>
<goal>importCertificate</goal>
</goals>
<configuration>
<file>${project.basedir}/tomcat.cert</file>
<alias>tomcat</alias>
<keystore>${trustStore.path}</keystore>
<storepass>${trustStore.password}</storepass>
<noprompt>true</noprompt>
<trustcacerts>true</trustcacerts>
<verbose>true</verbose>
</configuration>
</execution>
<execution>
<id>delete-tomcat-cert</id>
<phase>post-integration-test</phase>
<goals>
<goal>deleteAlias</goal>
</goals>
<configuration>
<alias>tomcat</alias>
<keystore>${trustStore.path}</keystore>
<storepass>${trustStore.password}</storepass>
<verbose>true</verbose>
</configuration>
</execution>
</executions>
</plugin>

<!-- NOTE: Tomcat cert import lives in the parent pom (must run before pool:up clones the source GF). -->
</plugins>

<pluginManagement>
Expand Down
Binary file modified tck/app-openid3/localhost-rsa.jks
Binary file not shown.
Loading