Skip to content

Commit

Permalink
[WIP] Proxy jwt bearer to support corporate trust
Browse files Browse the repository at this point in the history
  • Loading branch information
strehle committed Feb 27, 2025
1 parent aeb0d14 commit 8276e37
Show file tree
Hide file tree
Showing 11 changed files with 751 additions and 501 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class OIDCIdentityProviderDefinition extends AbstractExternalOAuthIdentit
private URL discoveryUrl;
private boolean passwordGrantEnabled;
private boolean setForwardHeader;
private Boolean tokenExchangeEnabled;
@JsonInclude(JsonInclude.Include.NON_NULL)
private List<Prompt> prompts;
@JsonInclude(JsonInclude.Include.NON_NULL)
Expand Down Expand Up @@ -89,6 +90,15 @@ public void setAdditionalAuthzParameters(final Map<String, String> additonalAuth
this.additionalAuthzParameters = new HashMap<>(additonalAuthzParameters != null ? additonalAuthzParameters : emptyMap());
}


public Boolean isTokenExchangeEnabled() {
return tokenExchangeEnabled;
}

public void setTokenExchangeEnabled(Boolean tokenExchangeEnabled) {
this.tokenExchangeEnabled = tokenExchangeEnabled;
}

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
Expand Down Expand Up @@ -120,6 +130,9 @@ public boolean equals(Object o) {
if (!Objects.equals(this.additionalAuthzParameters, that.additionalAuthzParameters)) {
return false;
}
if (!Objects.equals(this.tokenExchangeEnabled, that.tokenExchangeEnabled)) {
return false;
}
return Objects.equals(discoveryUrl, that.discoveryUrl);

}
Expand All @@ -132,6 +145,7 @@ public int hashCode() {
result = 31 * result + (setForwardHeader ? 1 : 0);
result = 31 * result + (jwtClientAuthentication != null ? jwtClientAuthentication.hashCode() : 0);
result = 31 * result + (additionalAuthzParameters != null ? additionalAuthzParameters.hashCode() : 0);
result = 31 * result + (tokenExchangeEnabled != null ? tokenExchangeEnabled.hashCode() : 0);
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

class OIDCIdentityProviderDefinitionTests {

private final String defaultJson = "{\"emailDomain\":null,\"additionalConfiguration\":null,\"providerDescription\":null,\"externalGroupsWhitelist\":[],\"attributeMappings\":{},\"addShadowUserOnLogin\":true,\"storeCustomAttributes\":false,\"authUrl\":null,\"tokenUrl\":null,\"tokenKeyUrl\":null,\"tokenKey\":null,\"linkText\":null,\"showLinkText\":true,\"skipSslValidation\":false,\"relyingPartyId\":null,\"relyingPartySecret\":null,\"scopes\":null,\"issuer\":null,\"responseType\":\"code\",\"userInfoUrl\":null,\"jwtClientAuthentication\":false,\"additionalAuthzParameters\":{\"token_format\":\"jwt\"}}";
private final String defaultJson = "{\"emailDomain\":null,\"additionalConfiguration\":null,\"providerDescription\":null,\"externalGroupsWhitelist\":[],\"attributeMappings\":{},\"addShadowUserOnLogin\":true,\"storeCustomAttributes\":false,\"authUrl\":null,\"tokenUrl\":null,\"tokenKeyUrl\":null,\"tokenKey\":null,\"linkText\":null,\"showLinkText\":true,\"skipSslValidation\":false,\"tokenExchangeEnabled\":true,\"relyingPartyId\":null,\"relyingPartySecret\":null,\"scopes\":null,\"issuer\":null,\"responseType\":\"code\",\"userInfoUrl\":null,\"jwtClientAuthentication\":false,\"additionalAuthzParameters\":{\"token_format\":\"jwt\"}}";
String url = "https://accounts.google.com/.well-known/openid-configuration";

@Test
Expand Down Expand Up @@ -68,6 +68,14 @@ void serialize_prompts() {
assertThat(def.getPrompts()).isEqualTo(prompts);
}

@Test
void equalsTests() throws CloneNotSupportedException {
OIDCIdentityProviderDefinition original = JsonUtils.readValue(defaultJson, OIDCIdentityProviderDefinition.class);
OIDCIdentityProviderDefinition compare = (OIDCIdentityProviderDefinition) original.clone();
compare.setTokenExchangeEnabled(false);
assertThat(original).isNotEqualTo(compare);
}

@Test
void serialize_jwtClientAuthentication() {
OIDCIdentityProviderDefinition def = JsonUtils.readValue(defaultJson, OIDCIdentityProviderDefinition.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import org.cloudfoundry.identity.uaa.oauth.provider.OAuth2RequestFactory;
import org.cloudfoundry.identity.uaa.oauth.provider.error.OAuth2AuthenticationEntryPoint;
import org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants;
import org.cloudfoundry.identity.uaa.provider.IdentityProvider;
import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition;
import org.cloudfoundry.identity.uaa.provider.oauth.ExternalOAuthAuthenticationManager;
import org.cloudfoundry.identity.uaa.provider.oauth.ExternalOAuthCodeToken;
import org.cloudfoundry.identity.uaa.provider.saml.Saml2BearerGrantAuthenticationConverter;
Expand Down Expand Up @@ -254,7 +256,17 @@ protected Authentication attemptTokenAuthentication(HttpServletRequest request,
log.debug(GRANT_TYPE_JWT_BEARER + " found. Attempting authentication with assertion");
String assertion = request.getParameter("assertion");
if (assertion != null && externalOAuthAuthenticationManager != null) {
log.debug("Attempting OIDC JWT authentication for token endpoint.");
IdentityProvider<OIDCIdentityProviderDefinition> oidcProxy = externalOAuthAuthenticationManager.getOidcProxyTokenExchange(request);
if (oidcProxy != null) {
log.debug("Forward OIDC JWT authentication to oidc proxy");
String idpAssertion = externalOAuthAuthenticationManager.oidcJwtBearerGrant(
(UaaAuthenticationDetails) authenticationDetailsSource.buildDetails(request),
oidcProxy, assertion);
assertion = idpAssertion != null ? idpAssertion : assertion;
} else {
log.debug("Attempting OIDC JWT authentication for token endpoint.");
}

ExternalOAuthCodeToken token = new ExternalOAuthCodeToken(null, null, null, assertion, null, null);
token.setRequestContextPath(getContextPath(request));
authResult = externalOAuthAuthenticationManager.authenticate(token);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
package org.cloudfoundry.identity.uaa.authentication.manager;

import org.apache.commons.lang3.ObjectUtils;
import org.cloudfoundry.identity.uaa.authentication.AbstractClientParametersAuthenticationFilter;
import org.cloudfoundry.identity.uaa.authentication.ProviderConfigurationException;
import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails;
import org.cloudfoundry.identity.uaa.authentication.UaaLoginHint;
import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationFailureEvent;
import org.cloudfoundry.identity.uaa.client.UaaClient;
import org.cloudfoundry.identity.uaa.constants.ClientAuthentication;
import org.cloudfoundry.identity.uaa.constants.OriginKeys;
import org.cloudfoundry.identity.uaa.impl.config.RestTemplateConfig;
import org.cloudfoundry.identity.uaa.login.Prompt;
import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants;
import org.cloudfoundry.identity.uaa.oauth.jwt.JwtClientAuthentication;
import org.cloudfoundry.identity.uaa.provider.IdentityProvider;
import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning;
import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition;
Expand All @@ -23,55 +15,38 @@
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;

import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_PASSWORD;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.util.StringUtils.hasText;

public class PasswordGrantAuthenticationManager implements AuthenticationManager, ApplicationEventPublisherAware {

private final DynamicZoneAwareAuthenticationManager zoneAwareAuthzAuthenticationManager;
private final IdentityProviderProvisioning identityProviderProvisioning;
private final RestTemplateConfig restTemplateConfig;
private final ExternalOAuthAuthenticationManager externalOAuthAuthenticationManager;
private ApplicationEventPublisher eventPublisher;

public PasswordGrantAuthenticationManager(DynamicZoneAwareAuthenticationManager zoneAwareAuthzAuthenticationManager, final @Qualifier("identityProviderProvisioning") IdentityProviderProvisioning identityProviderProvisioning, RestTemplateConfig restTemplateConfig, ExternalOAuthAuthenticationManager externalOAuthAuthenticationManager) {
public PasswordGrantAuthenticationManager(DynamicZoneAwareAuthenticationManager zoneAwareAuthzAuthenticationManager, final @Qualifier("identityProviderProvisioning") IdentityProviderProvisioning identityProviderProvisioning, ExternalOAuthAuthenticationManager externalOAuthAuthenticationManager) {
this.zoneAwareAuthzAuthenticationManager = zoneAwareAuthzAuthenticationManager;
this.identityProviderProvisioning = identityProviderProvisioning;
this.restTemplateConfig = restTemplateConfig;
this.externalOAuthAuthenticationManager = externalOAuthAuthenticationManager;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UaaLoginHint uaaLoginHint = zoneAwareAuthzAuthenticationManager.extractLoginHint(authentication);
List<String> allowedProviders = getAllowedProviders();
List<String> allowedProviders = externalOAuthAuthenticationManager.getAllowedProviders();
String defaultProvider = IdentityZoneHolder.get().getConfig().getDefaultIdentityProvider();
UaaLoginHint loginHintToUse;
IdentityProvider<?> identityProvider = retrievePasswordIdp(uaaLoginHint, defaultProvider, allowedProviders);
Expand Down Expand Up @@ -149,96 +124,21 @@ private UaaLoginHint getUaaLoginHintForChainedAuth(List<String> allowedProviders
}

Authentication oidcPasswordGrant(Authentication authentication, final IdentityProvider<OIDCIdentityProviderDefinition> identityProvider) {
final OIDCIdentityProviderDefinition config = identityProvider.getConfig();

//Token per RestCall
URL tokenUrl = config.getTokenUrl();
String clientId = config.getRelyingPartyId();
String clientSecret = config.getRelyingPartySecret();
if (clientId == null) {
throw new ProviderConfigurationException("External OpenID Connect provider configuration is missing relyingPartyId.");
}
if (clientSecret == null && config.getJwtClientAuthentication() == null && config.getAuthMethod() == null) {
throw new ProviderConfigurationException("External OpenID Connect provider configuration is missing relyingPartySecret, jwtClientAuthentication or authMethod.");
}
if (tokenUrl == null) {
externalOAuthAuthenticationManager.fetchMetadataAndUpdateDefinition(config);
tokenUrl = Optional.ofNullable(config.getTokenUrl()).orElseThrow(() -> new ProviderConfigurationException("External OpenID Connect metadata is missing after discovery update."));
UaaAuthenticationDetails uaaAuthenticationDetails = null;
if (authentication.getDetails() instanceof UaaAuthenticationDetails details) {
uaaAuthenticationDetails = details;
}
String calcAuthMethod = ClientAuthentication.getCalculatedMethod(config.getAuthMethod(), clientSecret != null, config.getJwtClientAuthentication() != null);
String userName = authentication.getPrincipal() instanceof String pStr ? pStr : null;
if (userName == null || authentication.getCredentials() == null || !(authentication.getCredentials() instanceof String)) {
throw new BadCredentialsException("Request is missing username or password.");
}
Supplier<String> passProvider = () -> (String) authentication.getCredentials();
RestTemplate rt;
if (config.isSkipSslValidation()) {
rt = restTemplateConfig.trustingRestTemplate();
} else {
rt = restTemplateConfig.nonTrustingRestTemplate();
}

HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();

if (ClientAuthentication.PRIVATE_KEY_JWT.equals(calcAuthMethod)) {
/* ensure that the dynamic lookup of the cert and/or key for private key JWT works for an alias IdP in a
* custom IdZ */
final boolean allowDynamicValueLookupInCustomZone = hasText(identityProvider.getAliasZid()) && hasText(identityProvider.getAliasId());
params = new JwtClientAuthentication(externalOAuthAuthenticationManager.getKeyInfoService())
.getClientAuthenticationParameters(params, config, allowDynamicValueLookupInCustomZone);
} else if (ClientAuthentication.secretNeeded(calcAuthMethod)) {
String auth = clientId + ":" + clientSecret;
headers.add("Authorization", "Basic " + Base64Utils.encodeToString(auth.getBytes()));
} else {
params.add(AbstractClientParametersAuthenticationFilter.CLIENT_ID, clientId);
}
if (config.isSetForwardHeader() && authentication.getDetails() != null && authentication.getDetails() instanceof UaaAuthenticationDetails details) {
if (details.getOrigin() != null) {
headers.add("X-Forwarded-For", details.getOrigin());
}
}
params.add("grant_type", GRANT_TYPE_PASSWORD);
params.add("response_type", "id_token");
params.add("username", userName);
params.add("password", passProvider.get());
if (ObjectUtils.isNotEmpty(config.getScopes())) {
params.add("scope", String.join(" ", config.getScopes()));
}

List<Prompt> prompts = config.getPrompts();
List<String> promptsToInclude = new ArrayList<>();
if (prompts != null) {
for (Prompt prompt : prompts) {
if ("username".equals(prompt.getName()) || "password".equals(prompt.getName()) || "passcode".equals(prompt.getName())) {
continue;
}
promptsToInclude.add(prompt.getName());
}
}
if (authentication.getDetails() instanceof UaaAuthenticationDetails details) {
for (String prompt : promptsToInclude) {
String[] values = details.getParameterMap().get(prompt);
if (values == null || values.length != 1 || !hasText(values[0])) {
continue; //No single value given, skip this parameter
}
params.add(prompt, values[0]);
}
}


HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
String idToken = null;
String idToken;
try {
ResponseEntity<Map<String, String>> tokenResponse = rt.exchange(tokenUrl.toString(), HttpMethod.POST, request, new ParameterizedTypeReference<>() {
});

if (tokenResponse.hasBody()) {
Map<String, String> body = tokenResponse.getBody();
idToken = body != null ? body.get("id_token") : null;
}
idToken = externalOAuthAuthenticationManager.oauthTokenRequest(uaaAuthenticationDetails, identityProvider, GRANT_TYPE_PASSWORD, params);
} catch (HttpClientErrorException e) {
publish(new IdentityProviderAuthenticationFailureEvent(authentication, userName, OriginKeys.OIDC10, IdentityZoneHolder.getCurrentZoneId()));
throw new BadCredentialsException(e.getResponseBodyAsString(), e);
Expand All @@ -263,19 +163,6 @@ private boolean providerSupportsPasswordGrant(IdentityProvider provider) {
return config.isPasswordGrantEnabled();
}


private List<String> getAllowedProviders() {
Authentication clientAuth = SecurityContextHolder.getContext().getAuthentication();
if (clientAuth == null) {
throw new BadCredentialsException("No client authentication found.");
}
List<String> allowedProviders = null;
if (clientAuth.getPrincipal() instanceof UaaClient uaaClient && uaaClient.getAdditionalInformation() != null) {
allowedProviders = (List<String>) uaaClient.getAdditionalInformation().get(ClientConstants.ALLOWED_PROVIDERS);
}
return allowedProviders;
}

@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.eventPublisher = applicationEventPublisher;
Expand Down
Loading

0 comments on commit 8276e37

Please sign in to comment.