ADR-0005: Support Forward Slashes in Entity IDs via URL Encoding
Status
Proposed
Context
Camunda's Identity REST API uses entity IDs (group, role, tenant, user, mapping rule) as path
segments in URLs — for example, PUT /v2/roles/{roleId}/groups/{groupId}. When Camunda is
configured with an OIDC provider such as Keycloak, entity IDs are sourced directly from the
identity provider. Keycloak group IDs commonly contain forward slashes (e.g., /myGroup,
/org/team/engineering).
When the frontend constructs a URL with a literal / in an entity ID, the resulting path (e.g.,
PUT /v2/roles/admin/groups//myGroup) contains extra path segments. Spring MVC finds no matching
route and returns 400. This effectively prevents any OIDC-sourced entity with / in its ID from
being managed through the REST API or the Identity UI.
Two compounding issues cause this:
- Validation allows
/:SecurityConfiguration.DEFAULT_EXTERNAL_ID_REGEXis.*when the OIDCgroupsClaimis configured, permitting any character in group IDs — including/. - Frontend does not encode: URL paths are constructed via template literals without
encodeURIComponent, so special characters are inserted verbatim.
See: https://github.com/camunda/camunda/issues/45215
Decision
We will use standard URL encoding (%2F) on the client side and configure the embedded Tomcat
server to pass encoded slashes through to Spring MVC. Entity IDs will not be normalized or
transformed — they must match exactly what the identity provider returns.
The solution has four layers:
1. Frontend: Encode IDs with encodeURIComponent
Every entity ID interpolated into a URL path is wrapped with encodeURIComponent(). This is
standard HTTP behavior per RFC 3986 and is a no-op for alphanumeric strings, making it safe for
all existing IDs.
// Before
apiPut(`${ROLES_ENDPOINT}/${roleId}/groups/${groupId}`);
// After
apiPut(`${ROLES_ENDPOINT}/${encodeURIComponent(roleId)}/groups/${encodeURIComponent(groupId)}`);
2. Tomcat: DECODE mode for encoded slashes
Tomcat 11 rejects %2F in URL paths by default (EncodedSolidusHandling.REJECT). A
TomcatConnectorCustomizer bean sets the handling mode to DECODE, which converts %2F to a
literal / at the connector level before Tomcat's path normalization runs.
connector.setEncodedSolidusHandling(EncodedSolidusHandling.DECODE.getValue());
PASS_THROUGH was tried first but does not work on Tomcat 10.1+: the path is still rejected during
path normalization even though the connector itself permits %2F. DECODE is the only mode that
survives the connector. Route matching continues to work because Spring's PathPatternParser
matches the decoded URI without collapsing the resulting // boundary, and @PathVariable binding
receives the leading-slash value as expected. This is covered by EncodedSlashIntegrationIT.
3. Spring MVC: PathPatternParser (no changes needed)
After the Tomcat connector decodes %2F, Spring sees a path with literal / characters (e.g.
/v2/roles/admin/groups//myGroup). The default PathPatternParser matches the variable segments
greedily and does not collapse the // boundary, so /v2/roles/{roleId}/groups/{groupId} still
binds correctly with groupId = "/myGroup". No configuration changes are required.
4. Spring Security: relax StrictHttpFirewall
Spring Security's default StrictHttpFirewall rejects any request whose URI contains %2F (or its
decoded variants in some forms) with a 400 before the request reaches the controller. A
WebSecurityCustomizer bean (WebSecurityConfig.encodedSlashFirewallCustomizer) installs a
StrictHttpFirewall with setAllowUrlEncodedSlash(true).
@Bean
public WebSecurityCustomizer encodedSlashFirewallCustomizer() {
final var firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
return web -> web.httpFirewall(firewall);
}
Spring Security 6.5 removed the autowired WebSecurityConfigurerAdapter#setHttpFirewall() setter,
so a plain @Bean HttpFirewall is no longer wired automatically — WebSecurityCustomizer is the
supported replacement. A single customizer covers all SecurityFilterChain beans because the
firewall runs in front of FilterChainProxy, ahead of any per-chain matcher.
Alternatives Considered
Normalize IDs (strip or replace slashes)
Rejected. Entity IDs must match the identity provider exactly. Normalizing would create a mismatch between Camunda's stored ID and the ID in OIDC tokens, breaking authorization checks.
Use request body instead of path variables
Rejected. This would require changing the REST API contract, breaking backward compatibility for all API consumers. URL encoding is the standard HTTP mechanism for this problem.
Custom path matching or wildcard patterns
Rejected. Using ** patterns or custom PathMatcher configuration would be fragile, make routing
ambiguous, and require changes across all controllers. Standard URL encoding solves the problem
without any routing changes.
Consequences
Positive
- Entity IDs from OIDC providers are supported as-is, regardless of special characters.
- The solution follows HTTP standards (RFC 3986) — no custom encoding schemes.
- Fully backward compatible:
encodeURIComponentis a no-op on alphanumeric strings, and theDECODEmode only affects requests that contain%2F. - No changes to controllers, service layer, or data model.
Negative
- Reverse proxies must forward raw URIs. Proxies like nginx may reject or decode
%2Fby default. Operators using reverse proxies must configure them to preserve encoded characters (e.g., nginx:proxy_passwith$request_uri, ormerge_slashes off). - External API clients must encode IDs. Any client calling the REST API directly must URL-encode entity IDs containing special characters. This is standard HTTP behavior but must be documented in the API reference.
DECODEapplies to all connectors. The Tomcat configuration affects all incoming requests, not just identity endpoints. Likewise, the relaxed Spring Security firewall is global. This is safe because%2Fin a path segment is only meaningful if the application interprets it — existing endpoints with alphanumeric IDs are unaffected.