Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
19 changes: 19 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ jobs:
npx playwright test --reporter=list
env:
OPENIDM_URL: http://localhost:8080
- name: Start OpenIDM with custom context path /myidm
if: runner.os == 'Linux'
run: |
openidm/shutdown.sh
timeout 1m bash -c 'while [ -f openidm/.openidm.pid ]; do sleep 2; done' || true
rm -rf openidm/logs/*
OPENIDM_OPTS="-Dlogback.configurationFile=conf/logging-config.groovy -Dopenidm.context.path=/myidm" openidm/startup.sh &
timeout 3m bash -c 'until grep -q "OpenIDM ready" openidm/logs/openidm0.log.0 ; do sleep 5; done' || cat openidm/logs/openidm0.log.0
grep -q "OpenIDM ready" openidm/logs/openidm0.log.0
! grep "ERROR" openidm/logs/openidm0.log.0
! grep "SEVERE" openidm/logs/openidm0.log.0
- name: UI Smoke Tests with /myidm context path (Playwright)
if: runner.os == 'Linux'
run: |
cd e2e
npx playwright test --reporter=list
env:
OPENIDM_URL: http://localhost:8080
OPENIDM_CONTEXT_PATH: /myidm
- name: Test on Windows
if: runner.os == 'Windows'
run: |
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ WORKDIR /opt

#COPY openidm-zip/target/openidm-*.zip ./

RUN apt-get update \
RUN echo 'Acquire::Retries "3";' > /etc/apt/apt.conf.d/80-retries \
&& apt-get update \
&& apt-get install -y --no-install-recommends curl unzip \
&& bash -c 'if [ ! -z "$VERSION" ] ; then rm -rf ./*.zip ; curl -L https://github.com/OpenIdentityPlatform/OpenIDM/releases/download/$VERSION/openidm-$VERSION.zip --output openidm-$VERSION.zip ; fi' \
&& unzip openidm-*.zip && rm -rf *.zip \
Expand Down
5 changes: 3 additions & 2 deletions e2e/ui-smoke-test.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { test, expect } from "@playwright/test";

const BASE_URL = process.env.OPENIDM_URL || "http://localhost:8080";
const CONTEXT_PATH = process.env.OPENIDM_CONTEXT_PATH || "/openidm";
const ADMIN_USER = process.env.OPENIDM_ADMIN_USER || "openidm-admin";
const ADMIN_PASS = process.env.OPENIDM_ADMIN_PASS || "openidm-admin";

Expand Down Expand Up @@ -119,7 +120,7 @@ test.describe("OpenIDM UI Smoke Tests", () => {
});

test("REST API ping is accessible", async ({ request }) => {
const response = await request.get(`${BASE_URL}/openidm/info/ping`, {
const response = await request.get(`${BASE_URL}${CONTEXT_PATH}/info/ping`, {
headers: {
"X-OpenIDM-Username": ADMIN_USER,
"X-OpenIDM-Password": ADMIN_PASS,
Expand All @@ -131,7 +132,7 @@ test.describe("OpenIDM UI Smoke Tests", () => {
});

test("REST API config endpoint is accessible", async ({ request }) => {
const response = await request.get(`${BASE_URL}/openidm/config/ui/configuration`, {
const response = await request.get(`${BASE_URL}${CONTEXT_PATH}/config/ui/configuration`, {
headers: {
"X-OpenIDM-Username": ADMIN_USER,
"X-OpenIDM-Password": ADMIN_PASS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2013-2016 ForgeRock AS.
* Portions Copyrighted 2024-2025 3A Systems LLC.
* Portions Copyrighted 2024-2026 3A Systems LLC.
*/
package org.forgerock.openidm.servlet.internal;

Expand Down Expand Up @@ -74,7 +74,7 @@
/**
* A component to create and register the "API" Servlet; that is, the CHF Servlet that
*
* 1) listens on /openidm,
* 1) listens on /openidm (or the path configured via openidm.context.path system property),
* 2) dispatches to the HttpApplication, that is composed of
* a) the auth filter
* b) the JSON resource HTTP Handler, that
Expand All @@ -93,7 +93,11 @@ public class ServletComponent implements EventHandler {

static final String PID = "org.forgerock.openidm.api-servlet";

private static final String SERVLET_ALIAS = "/openidm";
/** System property name for the configurable REST context path. */
static final String OPENIDM_CONTEXT_PATH_PROPERTY = ServerConstants.OPENIDM_CONTEXT_PATH_PROPERTY;

/** Default REST context path. */
static final String OPENIDM_CONTEXT_PATH_DEFAULT = ServerConstants.OPENIDM_CONTEXT_PATH_DEFAULT;

private static final String API_ID = "frapi:openidm";

Expand Down Expand Up @@ -155,9 +159,19 @@ protected synchronized void unbindRegistrator(ServletFilterRegistrator registrat

private HttpServlet servlet;

/**
* Returns the servlet alias (REST context path) from the system property
* {@code openidm.context.path}, defaulting to {@code /openidm}.
*/
static String getServletAlias() {
return ServerConstants.normalizeContextPath(
System.getProperty(OPENIDM_CONTEXT_PATH_PROPERTY, OPENIDM_CONTEXT_PATH_DEFAULT));
}

Comment on lines +162 to +170
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context-path normalization logic (leading / + trimming trailing /) is duplicated across multiple components in this PR (e.g., servlet component, servlet registrator, CLI default URL, UI index injection). To reduce drift and edge-case inconsistencies (like the trailing-slash UI issue), consider centralizing normalization in a single helper (e.g., a ServerConstants#getNormalizedContextPath() utility) and reusing it everywhere.

Suggested change
/**
* Returns the servlet alias (REST context path) from the system property
* {@code openidm.context.path}, defaulting to {@code /openidm}.
*/
static String getServletAlias() {
String path = System.getProperty(OPENIDM_CONTEXT_PATH_PROPERTY, OPENIDM_CONTEXT_PATH_DEFAULT);
if (!path.startsWith("/")) {
path = "/" + path;
}
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
return path;
}
private static String normalizeContextPath(String path) {
if (!path.startsWith("/")) {
path = "/" + path;
}
if (path.endsWith("/") && path.length() > 1) {
path = path.substring(0, path.length() - 1);
}
return path;
}
/**
* Returns the servlet alias (REST context path) from the system property
* {@code openidm.context.path}, defaulting to {@code /openidm}.
*/
static String getServletAlias() {
return normalizeContextPath(
System.getProperty(OPENIDM_CONTEXT_PATH_PROPERTY, OPENIDM_CONTEXT_PATH_DEFAULT));
}

Copilot uses AI. Check for mistakes.
@Activate
protected void activate(ComponentContext context) throws ServletException, NamespaceException {
logger.debug("Registering servlet at {}", SERVLET_ALIAS);
final String servletAlias = getServletAlias();
logger.debug("Registering servlet at {}", servletAlias);

final Handler handler = CrestHttp.newHttpHandler(
new CrestApplication() {
Expand Down Expand Up @@ -201,8 +215,8 @@ public void stop() {

@SuppressWarnings("rawtypes")
final Dictionary params = new Hashtable();
servletRegistration.registerServlet(SERVLET_ALIAS, servlet, params);
logger.info("Registered servlet at {}", SERVLET_ALIAS);
servletRegistration.registerServlet(servletAlias, servlet, params);
logger.info("Registered servlet at {}", servletAlias);
}

@Deactivate
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2025-2026 3A Systems LLC.
*/

package org.forgerock.openidm.servlet.internal;

import static org.assertj.core.api.Assertions.assertThat;

import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;

/**
* Unit tests for {@link ServletComponent} context path configuration.
*/
public class ServletComponentTest {

@AfterMethod
public void clearSystemProperty() {
System.clearProperty(ServletComponent.OPENIDM_CONTEXT_PATH_PROPERTY);
}

@Test
public void testDefaultServletAlias() {
// When no system property is set, should return the default /openidm
System.clearProperty(ServletComponent.OPENIDM_CONTEXT_PATH_PROPERTY);
assertThat(ServletComponent.getServletAlias()).isEqualTo("/openidm");
}

@Test
public void testCustomServletAlias() {
// When system property is set to /myidm, should return /myidm
System.setProperty(ServletComponent.OPENIDM_CONTEXT_PATH_PROPERTY, "/myidm");
assertThat(ServletComponent.getServletAlias()).isEqualTo("/myidm");
}

@Test
public void testServletAliasWithoutLeadingSlash() {
// Should add leading slash if missing
System.setProperty(ServletComponent.OPENIDM_CONTEXT_PATH_PROPERTY, "myidm");
assertThat(ServletComponent.getServletAlias()).isEqualTo("/myidm");
}

@Test
public void testServletAliasWithTrailingSlash() {
// Should remove trailing slash
System.setProperty(ServletComponent.OPENIDM_CONTEXT_PATH_PROPERTY, "/myidm/");
assertThat(ServletComponent.getServletAlias()).isEqualTo("/myidm");
}

@Test
public void testServletAliasConstants() {
assertThat(ServletComponent.OPENIDM_CONTEXT_PATH_PROPERTY).isEqualTo("openidm.context.path");
assertThat(ServletComponent.OPENIDM_CONTEXT_PATH_DEFAULT).isEqualTo("/openidm");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
information: "Portions copyright [year] [name of copyright owner]".

Copyright 2017 ForgeRock AS.
Portions Copyright 2024-2025 3A Systems LLC.
Portions Copyright 2024-2026 3A Systems LLC.
////

:figure-caption!:
Expand Down Expand Up @@ -82,6 +82,11 @@ Note that for LDAP resources, you should not map the LDAP `dn` to the OpenIDM `u
...
----

[NOTE]
====
The `/openidm` context path shown in all URI examples throughout this guide is the default value. You can change it by setting the `openidm.context.path` system property in `conf/system.properties` or as a JVM argument (for example, `-Dopenidm.context.path=/myidm`). For more information, see xref:chap-configuration.adoc#configuring-rest-context-path["Configuring the REST Context Path"] in the __Integrator's Guide__.
====


[#rest-object-identifier]
=== Object Identifiers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
information: "Portions copyright [year] [name of copyright owner]".

Copyright 2017 ForgeRock AS.
Portions Copyright 2024-2025 3A Systems LLC.
Portions Copyright 2024-2026 3A Systems LLC.
////

:figure-caption!:
Expand Down Expand Up @@ -152,6 +152,37 @@ felix.fileinstall.enableConfigSave=false
----


[#configuring-rest-context-path]
==== Configuring the REST Context Path

By default, the OpenIDM REST API is available under the `/openidm` context path (for example, `\https://localhost:8443/openidm/`). You can change this base path by setting the `openidm.context.path` system property.

To set a custom REST context path, edit the `conf/system.properties` file and uncomment or add the following line, replacing `/openidm` with your preferred path:

[source]
----
openidm.context.path=/openidm
----

Alternatively, you can pass the property as a JVM argument when starting OpenIDM:

[source, console]
----
$ OPENIDM_OPTS="-Dopenidm.context.path=/myidm" ./startup.sh
----

The path must begin with a `/` and must not end with `/`. If the value provided does not start with a `/`, one is added automatically.

After changing this property, the REST API will be accessible under the new path, for example `\https://localhost:8443/myidm/config`. When the Admin UI or Self-Service UI is served by OpenIDM, `ResourceServlet` injects the effective context path into `index.html` at runtime as `window.__openidm_context_path`. The UI reads that injected value for its REST calls, so in this deployment model the UIs automatically follow a custom `openidm.context.path` without requiring a reverse proxy or a rebuild.

If you host the UI separately instead of serving it through OpenIDM, this runtime injection does not occur. In that case, you must provide equivalent runtime configuration for the UI, or use another deployment mechanism such as a reverse proxy or a custom UI build that targets the desired context path.

[NOTE]
====
Changing the context path affects all REST API endpoints. If you expose the Admin UI or Self-Service UI under a custom path, ensure that any external integrations, load balancer rules, or documentation referring to the `/openidm` path are updated accordingly.
====


[#configuring-proxy]
==== Communicating Through a Proxy Server

Expand Down
7 changes: 6 additions & 1 deletion openidm-servlet-registrator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
~ your own identifying information:
~ "Portions Copyrighted [year] [name of copyright owner]"
~
~ Portions Copyrighted 2024-2025 3A Systems LLC.
~ Portions Copyrighted 2024-2026 3A Systems LLC.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
Expand All @@ -37,6 +37,11 @@
<description>This bundle is duplicates of OpenIDM HTTP context bundle</description>

<dependencies>
<dependency>
<groupId>org.openidentityplatform.openidm</groupId>
<artifactId>openidm-system</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openidentityplatform.openidm</groupId>
<artifactId>openidm-enhanced-config</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Portions Copyrighted 2024-2025 3A Systems LLC.
* Portions Copyrighted 2024-2026 3A Systems LLC.
*/

package org.forgerock.openidm.servletregistration.impl;
Expand Down Expand Up @@ -55,6 +55,7 @@
import org.forgerock.openidm.servletregistration.RegisteredFilter;
import org.forgerock.openidm.servletregistration.ServletRegistration;
import org.forgerock.openidm.servletregistration.ServletFilterRegistrator;
import org.forgerock.openidm.core.ServerConstants;
import org.forgerock.util.Function;
import org.ops4j.pax.web.service.WebContainer;
import org.osgi.framework.BundleContext;
Expand Down Expand Up @@ -90,7 +91,21 @@ public class ServletRegistrationSingleton implements ServletRegistration {

private static final String[] DEFAULT_SERVLET_NAME = new String[] { "OpenIDM REST" };

private static final String[] DEFAULT_SERVLET_URL_PATTERNS = new String[] { "/openidm/*", "/selfservice/*" };
/** System property name for the configurable REST context path. */
private static final String OPENIDM_CONTEXT_PATH_PROPERTY = ServerConstants.OPENIDM_CONTEXT_PATH_PROPERTY;

/** Default REST context path. */
private static final String OPENIDM_CONTEXT_PATH_DEFAULT = ServerConstants.OPENIDM_CONTEXT_PATH_DEFAULT;

/**
* Returns the default servlet URL patterns, using the configured context path
* from the {@code openidm.context.path} system property (default: {@code /openidm}).
*/
private static String[] getDefaultServletUrlPatterns() {
String contextPath = ServerConstants.normalizeContextPath(
System.getProperty(OPENIDM_CONTEXT_PATH_PROPERTY, OPENIDM_CONTEXT_PATH_DEFAULT));
return new String[] { contextPath + "/*", "/selfservice/*" };
}

// Context of this scr component
private BundleContext bundleContext;
Expand Down Expand Up @@ -212,7 +227,7 @@ public URL apply(JsonValue jsonValue) throws JsonValueException {

// URL patterns to apply the filter to, e.g. one could also add "/openidmui/*");
List<String> urlPatterns = config.get(SERVLET_FILTER_URL_PATTERNS)
.defaultTo(Arrays.asList(DEFAULT_SERVLET_URL_PATTERNS))
.defaultTo(Arrays.asList(getDefaultServletUrlPatterns()))
.asList(String.class);

// Filter init params, a string to string map
Expand Down
Loading