1818import org .slf4j .LoggerFactory ;
1919import org .springframework .beans .factory .InitializingBean ;
2020import org .springframework .dao .EmptyResultDataAccessException ;
21+ import org .springframework .util .StringUtils ;
2122import org .springframework .web .filter .OncePerRequestFilter ;
2223
2324import jakarta .servlet .FilterChain ;
3132
3233/**
3334 * This filter ensures that all requests are targeting a specific identity zone
34- * by hostname. If the hostname doesn't match an identity zone, a 404 error is
35- * sent.
36- *
35+ * by hostname or by path prefix /z/{subdomain}/ . If the hostname doesn't match
36+ * an identity zone, a 404 error is sent. Using both a subdomain (host) and a /z/
37+ * path is not allowed and returns 400.
3738 */
3839public class IdentityZoneResolvingFilter extends OncePerRequestFilter implements InitializingBean {
3940
41+ private static final String ZONE_PATH_PREFIX = "/z/" ;
42+
4043 private final IdentityZoneProvisioning dao ;
4144 private final Set <String > staticResources = Set .of ("/resources/" , "/vendor/font-awesome/" );
4245 private final Set <String > defaultZoneHostnames = new HashSet <>();
@@ -49,9 +52,21 @@ public IdentityZoneResolvingFilter(final IdentityZoneProvisioning dao) {
4952 @ Override
5053 protected void doFilterInternal (HttpServletRequest request , HttpServletResponse response , FilterChain filterChain )
5154 throws ServletException , IOException {
55+ String requestPath = UaaUrlUtils .getRequestPath (request );
56+ String subdomainFromHost = getSubdomainFromHost (request .getServerName ());
57+ String subdomainFromPath = getSubdomainFromPath (requestPath );
58+
59+ // 400: path starts with /z/ and host has a zone subdomain
60+ if (requestPath .startsWith (ZONE_PATH_PREFIX ) && StringUtils .hasText (subdomainFromHost )) {
61+ response .sendError (HttpServletResponse .SC_BAD_REQUEST , "Cannot use both subdomain and zone path" );
62+ return ;
63+ }
64+
65+ // Host always overrides path domain - path domain only works if there is no Host subdomain
66+ String subdomain = StringUtils .hasText (subdomainFromPath ) && "" .equals (subdomainFromHost ) ?
67+ subdomainFromPath : subdomainFromHost ;
68+
5269 IdentityZone identityZone = null ;
53- String hostname = request .getServerName ();
54- String subdomain = getSubdomain (hostname );
5570 if (subdomain != null ) {
5671 try {
5772 identityZone = dao .retrieveBySubdomain (subdomain );
@@ -66,7 +81,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
6681 }
6782 if (identityZone == null ) {
6883 // skip filter to static resources in order to serve images and css in case of invalid zones
69- boolean isStaticResource = staticResources .stream ().anyMatch (UaaUrlUtils . getRequestPath ( request ) ::startsWith );
84+ boolean isStaticResource = staticResources .stream ().anyMatch (requestPath ::startsWith );
7085 if (isStaticResource ) {
7186 filterChain .doFilter (request , response );
7287 return ;
@@ -84,7 +99,23 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
8499 }
85100 }
86101
87- private String getSubdomain (String hostname ) {
102+ /**
103+ * Returns the subdomain if path starts with /z/{subdomain}/, otherwise null.
104+ */
105+ private String getSubdomainFromPath (String path ) {
106+ if (path == null || !path .startsWith (ZONE_PATH_PREFIX )) {
107+ return null ;
108+ }
109+ String afterPrefix = path .substring (ZONE_PATH_PREFIX .length ());
110+ int slash = afterPrefix .indexOf ('/' );
111+ if (slash < 0 ) {
112+ return null ;
113+ }
114+ String subdomain = afterPrefix .substring (0 , slash );
115+ return StringUtils .hasText (subdomain ) ? subdomain : null ;
116+ }
117+
118+ private String getSubdomainFromHost (String hostname ) {
88119 String lowerHostName = hostname .toLowerCase ();
89120 if (defaultZoneHostnames .contains (lowerHostName )) {
90121 return "" ;
0 commit comments