Skip to content
Open
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
1 change: 1 addition & 0 deletions scripts/cargo/uaa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jwt:
- excluded-claim1
- excluded-claim2
login:
zidHeaderEnabled: true
Copy link
Member

Choose a reason for hiding this comment

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

I checked docs and we have X-Identity-Zone-Id and X-Identity-Zone-Subdomain already , so what is X-zid in comparison ?

The header allows new possiblities, correct ? then we should have thing to tell admin, e.g.
allowZoneSwitchByHeader or allowZoneSwitchByZidHeader

saml:
activeKeyId: key1
keys:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.cloudfoundry.identity.uaa.util.UaaUrlUtils;
import org.cloudfoundry.identity.uaa.web.HeaderFilter;
import org.cloudfoundry.identity.uaa.web.LimitedModeUaaFilter;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneMismatchCheckFilter;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneResolvingFilter;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter;
Expand Down Expand Up @@ -169,8 +170,11 @@ FilterRegistrationBean<DisableInternalUserManagementFilter> userManagementFilter
}

@Bean
FilterRegistrationBean<IdentityZoneResolvingFilter> identityZoneResolvingFilter(IdentityZoneProvisioning provisioning) {
IdentityZoneResolvingFilter filter = new IdentityZoneResolvingFilter(provisioning);
FilterRegistrationBean<IdentityZoneResolvingFilter> identityZoneResolvingFilter(
final IdentityZoneProvisioning provisioning,
@Qualifier("zidHeaderEnabled") final boolean zidHeaderEnabled
) {
IdentityZoneResolvingFilter filter = new IdentityZoneResolvingFilter(provisioning, zidHeaderEnabled);
filter.setDefaultInternalHostnames(new HashSet<>(Arrays.asList(
UaaUrlUtils.getHostForURI(uaaProps.url()),
UaaUrlUtils.getHostForURI(loginProps.url()),
Expand All @@ -182,13 +186,26 @@ FilterRegistrationBean<IdentityZoneResolvingFilter> identityZoneResolvingFilter(
return bean;
}

@Bean
FilterRegistrationBean<IdentityZoneMismatchCheckFilter> identityZoneMismatchCheckFilter(
final IdentityZoneManager identityZoneManager
) {
final IdentityZoneMismatchCheckFilter filter = new IdentityZoneMismatchCheckFilter(
identityZoneManager,
new DefaultRedirectStrategy(),
"/login"
);
final FilterRegistrationBean<IdentityZoneMismatchCheckFilter> bean = new FilterRegistrationBean<>(filter);
bean.setEnabled(false);
return bean;
}

@Bean
FilterRegistrationBean<SessionResetFilter> sessionResetFilter(
@Qualifier("userDatabase") JdbcUaaUserDatabase userDatabase
) {
SessionResetFilter filter = new SessionResetFilter(
new DefaultRedirectStrategy(),
identityZoneManager,
"/login",
userDatabase
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.cloudfoundry.identity.uaa.web.HeaderFilter;
import org.cloudfoundry.identity.uaa.web.LimitedModeUaaFilter;
import org.cloudfoundry.identity.uaa.web.UaaFilterChain;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneMismatchCheckFilter;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneResolvingFilter;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter;
import org.springframework.beans.factory.annotation.Qualifier;
Expand Down Expand Up @@ -126,6 +127,7 @@ SecurityFilterChainPostProcessor securityFilterChainPostProcessor(
@Qualifier("disableIdTokenResponseFilter") FilterRegistrationBean<DisableIdTokenResponseTypeFilter> disableIdTokenResponseFilter,
@Qualifier("saml2WebSsoAuthenticationRequestFilter") FilterRegistrationBean<Filter> saml2WebSsoAuthenticationRequestFilter,
@Qualifier("saml2WebSsoAuthenticationFilter") FilterRegistrationBean<Filter> saml2WebSsoAuthenticationFilter,
@Qualifier("identityZoneMismatchCheckFilter") FilterRegistrationBean<IdentityZoneMismatchCheckFilter> identityZoneMismatchCheckFilter,
@Qualifier("identityZoneSwitchingFilter") FilterRegistrationBean<IdentityZoneSwitchingFilter> identityZoneSwitchingFilter,
@Qualifier("saml2LogoutRequestFilter") FilterRegistrationBean<Saml2LogoutRequestFilter> saml2LogoutRequestFilter,
@Qualifier("saml2LogoutResponseFilter") FilterRegistrationBean<Saml2LogoutResponseFilter> saml2LogoutResponseFilter,
Expand Down Expand Up @@ -167,7 +169,8 @@ SecurityFilterChainPostProcessor securityFilterChainPostProcessor(
additionalFilters.put(SecurityFilterChainPostProcessor.FilterPosition.position(filterPos++), disableIdTokenResponseFilter.getFilter());
additionalFilters.put(SecurityFilterChainPostProcessor.FilterPosition.position(filterPos++), saml2WebSsoAuthenticationRequestFilter.getFilter());
additionalFilters.put(SecurityFilterChainPostProcessor.FilterPosition.position(filterPos++), saml2WebSsoAuthenticationFilter.getFilter());
additionalFilters.put(SecurityFilterChainPostProcessor.FilterPosition.after(OAuth2AuthenticationProcessingFilter.class), identityZoneSwitchingFilter.getFilter());
additionalFilters.put(SecurityFilterChainPostProcessor.FilterPosition.after(OAuth2AuthenticationProcessingFilter.class), identityZoneMismatchCheckFilter.getFilter());
additionalFilters.put(SecurityFilterChainPostProcessor.FilterPosition.after(IdentityZoneMismatchCheckFilter.class), identityZoneSwitchingFilter.getFilter());
additionalFilters.put(SecurityFilterChainPostProcessor.FilterPosition.after(IdentityZoneSwitchingFilter.class), saml2LogoutRequestFilter.getFilter());
additionalFilters.put(SecurityFilterChainPostProcessor.FilterPosition.after(Saml2LogoutRequestFilter.class), saml2LogoutResponseFilter.getFilter());
additionalFilters.put(SecurityFilterChainPostProcessor.FilterPosition.after(Saml2LogoutResponseFilter.class), userManagementSecurityFilter.getFilter());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import org.cloudfoundry.identity.uaa.constants.OriginKeys;
import org.cloudfoundry.identity.uaa.user.UaaUserDatabase;
import org.cloudfoundry.identity.uaa.user.UaaUserPrototype;
import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.SecurityContext;
Expand All @@ -35,21 +34,18 @@
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Date;
import java.util.Objects;

public class SessionResetFilter extends OncePerRequestFilter {

private static final Logger logger = LoggerFactory.getLogger(SessionResetFilter.class);

private final RedirectStrategy strategy;
private final IdentityZoneManager identityZoneManager;
@Getter
private final String redirectUrl;
private final UaaUserDatabase userDatabase;

public SessionResetFilter(RedirectStrategy strategy, IdentityZoneManager identityZoneManager, String redirectUrl, UaaUserDatabase userDatabase) {
public SessionResetFilter(RedirectStrategy strategy, String redirectUrl, UaaUserDatabase userDatabase) {
this.strategy = strategy;
this.identityZoneManager = identityZoneManager;
this.redirectUrl = redirectUrl;
this.userDatabase = userDatabase;
}
Expand All @@ -58,12 +54,6 @@ public SessionResetFilter(RedirectStrategy strategy, IdentityZoneManager identit
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
SecurityContext context = SecurityContextHolder.getContext();
if (context != null && context.getAuthentication() != null && context.getAuthentication() instanceof UaaAuthentication authentication) {
// zone check
if (!Objects.equals(identityZoneManager.getCurrentIdentityZoneId(), authentication.getPrincipal().getZoneId())) {
handleRedirect(request, response);
return;
}

// is authenticated UAA user
if (authentication.isAuthenticated() &&
OriginKeys.UAA.equals(authentication.getPrincipal().getOrigin()) &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package org.cloudfoundry.identity.uaa.zone;

import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication;
import org.cloudfoundry.identity.uaa.oauth.UaaOauth2Authentication;
import org.cloudfoundry.identity.uaa.oauth.provider.authentication.OAuth2AuthenticationProcessingFilter;
import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;

/**
* Checks whether there is a mismatch between ...
* <ul>
Copy link
Member

Choose a reason for hiding this comment

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

What is the difference to X-Identity-Zone-Id, X-Identity-Zone-Subdomain ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the subdomain or the X-Zid header, one selects the identity zone to log in to. With the other two headers, IdZ switching can be performed (only possible if logged in to "uaa" zone).

Example for IdZ switching:

  • user has a group zones.custom.scim.read in the "uaa" zone
  • log in to "uaa" zone (here, the X-Zid can be used as an override), receive token
  • use the token for performing actions according to the scim.read scope in the zone with the ID "custom"
    • In the SCIM requests, he would then need to pass the X-Identity-Zone-Id header with the value "custom"

* <li>the identity zone in the {@link IdentityZoneHolder} (specified by the subdomain or "X-Zid" header) and</li>
* <li>the identity zone in the {@link SecurityContext} (the one set in the session or the token).</li>
* </ul>
* These two pieces of information being necessary also implies the position of the filter in the chain:
* <ul>
* <li>after {@link IdentityZoneResolvingFilter}, which sets the identity zone in the IdentityZoneHolder and</li>
* <li>after {@link SecurityContextPersistenceFilter}, which sets the SecurityContext from the session</li>
* <li>after {@link OAuth2AuthenticationProcessingFilter}, which sets the SecurityContext from the token passed in the request</li>
* </ul>
* Additionally, the filter must be placed before the {@link IdentityZoneSwitchingFilter}.
*/
public class IdentityZoneMismatchCheckFilter extends OncePerRequestFilter {

private final IdentityZoneManager identityZoneManager;
private final RedirectStrategy redirectStrategy;
private final String redirectUrl;

public IdentityZoneMismatchCheckFilter(
final IdentityZoneManager identityZoneManager,
final RedirectStrategy redirectStrategy,
final String redirectUrl
) {
this.identityZoneManager = identityZoneManager;
this.redirectStrategy = redirectStrategy;
this.redirectUrl = redirectUrl;
}

@Override
protected void doFilterInternal(
final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain
) throws ServletException, IOException {
final Optional<Authentication> authenticationOpt = Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication);

if (authenticationOpt.isEmpty()) {
// not yet authenticated -> continue
filterChain.doFilter(request, response);
return;
}

final Authentication authentication = authenticationOpt.get();

final String zoneIdFromSessionOrToken;
if (authentication instanceof UaaAuthentication uaaAuthentication) {
// authenticated via session
zoneIdFromSessionOrToken = uaaAuthentication.getPrincipal().getZoneId();
} else if (authentication instanceof UaaOauth2Authentication uaaOauth2Authentication) {
/* authenticated via OAuth2 token
* IMPORTANT: already addressed by the issuer check in OAuth2AuthenticationProcessingFilter
* -> requires zone-specific subdomain to be set in the 'iss' claim of the token */
zoneIdFromSessionOrToken = uaaOauth2Authentication.getZoneId();
} else {
// no zone information in authentication
filterChain.doFilter(request, response);
return;
}

// redirect to login page if the zones do not match
if (!Objects.equals(zoneIdFromSessionOrToken, identityZoneManager.getCurrentIdentityZoneId())) {
handleRedirect(request, response);
return;
}

filterChain.doFilter(request, response);
}

protected void handleRedirect(
final HttpServletRequest request,
final HttpServletResponse response
) throws IOException {
// if a session was present, invalidate it
final HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}

redirectStrategy.sendRedirect(request, response, redirectUrl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,45 +37,67 @@
*/
public class IdentityZoneResolvingFilter extends OncePerRequestFilter implements InitializingBean {

/**
* Header for specifying the identity zone in which the request should be performed. If both a subdomain and the
* header are defined, the header takes precedence.
*/
private static final String X_ZID_HEADER = "X-zid";

private final boolean zidHeaderEnabled;

private final IdentityZoneProvisioning dao;
private final Set<String> staticResources = Set.of("/resources/", "/vendor/font-awesome/");
private final Set<String> defaultZoneHostnames = new HashSet<>();
private final Logger logger = LoggerFactory.getLogger(getClass());

public IdentityZoneResolvingFilter(final IdentityZoneProvisioning dao) {
public IdentityZoneResolvingFilter(final IdentityZoneProvisioning dao, final boolean zidHeaderEnabled) {
this.dao = dao;
this.zidHeaderEnabled = zidHeaderEnabled;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
protected void doFilterInternal(
final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain
) throws ServletException, IOException {
final String zidFromHeader = request.getHeader(X_ZID_HEADER);
final String hostname = request.getServerName();
final String subdomain = getSubdomain(hostname);

IdentityZone identityZone = null;
String hostname = request.getServerName();
String subdomain = getSubdomain(hostname);
if (subdomain != null) {
try {
String zoneResolvingDescription = null; // for logging and error messages
try {
if (zidHeaderEnabled && zidFromHeader != null) {
zoneResolvingDescription = "zid '%s'".formatted(zidFromHeader);
identityZone = dao.retrieve(zidFromHeader);
} else {
zoneResolvingDescription = "subdomain '%s'".formatted(subdomain);
identityZone = dao.retrieveBySubdomain(subdomain);
} catch (EmptyResultDataAccessException ex) {
logger.debug("Cannot find identity zone for subdomain {}", subdomain);
} catch (Exception ex) {
String message = "Internal server error while fetching identity zone for subdomain" + subdomain;
logger.warn(message, ex);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message);
return;
}
} catch (final EmptyResultDataAccessException | ZoneDoesNotExistsException ex) {
logger.debug("Cannot find identity zone for {}", zoneResolvingDescription);
} catch (final Exception ex) {
final String message = "Internal server error while fetching identity zone for %s"
.formatted(zoneResolvingDescription);
logger.warn(message, ex);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message);
return;
}

if (identityZone == null) {
// skip filter to static resources in order to serve images and css in case of invalid zones
boolean isStaticResource = staticResources.stream().anyMatch(UaaUrlUtils.getRequestPath(request)::startsWith);
final boolean isStaticResource = staticResources.stream().anyMatch(UaaUrlUtils.getRequestPath(request)::startsWith);
if (isStaticResource) {
filterChain.doFilter(request, response);
return;
}

request.setAttribute("error_message_code", "zone.not.found");
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Cannot find identity zone for subdomain " + subdomain);
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Cannot find identity zone for %s".formatted(zoneResolvingDescription));
return;
}

try {
IdentityZoneHolder.set(identityZone);
filterChain.doFilter(request, response);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.cloudfoundry.identity.uaa.zone.beans;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class IdentityZoneResolvingConfig {

@Bean
@Qualifier("zidHeaderEnabled")
public boolean zidHeaderEnabled(@Value("${login.zidHeaderEnabled:false}") final boolean enabled) {
return enabled;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import org.cloudfoundry.identity.uaa.user.UaaUserDatabase;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManagerImpl;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -79,7 +78,7 @@ void setUpFilter() {
response = mock(HttpServletResponse.class);
session = mock(HttpSession.class);
when(request.getSession(anyBoolean())).thenReturn(session);
filter = new SessionResetFilter(new DefaultRedirectStrategy(), new IdentityZoneManagerImpl(),"/login", userDatabase);
filter = new SessionResetFilter(new DefaultRedirectStrategy(), "/login", userDatabase);
}

private void addUsersToInMemoryDb() {
Expand Down
Loading
Loading