From f696b9ab952a9658b1c40d375efb7e21fd35c281 Mon Sep 17 00:00:00 2001
From: Zoltan Maradics
Date: Thu, 18 Apr 2019 17:05:01 +0200
Subject: [PATCH 001/140] Add PKCE support
---
.../uaa/account/OpenIdConfiguration.java | 10 ++
.../uaa/account/OpenIdConfigurationTests.java | 1 +
.../uaa/oauth/UaaAuthorizationEndpoint.java | 40 +++++
.../oauth/pkce/PkceValidationException.java | 17 ++
.../uaa/oauth/pkce/PkceValidationService.java | 167 ++++++++++++++++++
.../identity/uaa/oauth/pkce/PkceVerifier.java | 32 ++++
.../pkce/verifiers/PlainPkceVerifier.java | 27 +++
.../pkce/verifiers/S256PkceVerifier.java | 49 +++++
...EnhancedAuthorizationCodeTokenGranter.java | 121 +++++++++++++
.../oauth/UaaAuthorizationEndpointTest.java | 43 +++++
.../oauth/pkce/PkceValidationServiceTest.java | 129 ++++++++++++++
.../pkce/verifiers/PlainPkceVerifierTest.java | 47 +++++
.../pkce/verifiers/S256PkceVerifierTest.java | 47 +++++
.../identity/uaa/test/UaaTestAccounts.java | 6 +
.../webapp/WEB-INF/spring/oauth-endpoints.xml | 32 +++-
...uthorizationCodeGrantIntegrationTests.java | 128 ++++++++++++++
.../util/IntegrationTestUtils.java | 128 ++++++++++++++
.../uaa/login/AuthorizeEndpointDocs.java | 11 ++
.../identity/uaa/login/TokenEndpointDocs.java | 8 +
.../endpoints/OpenIdConnectEndpointDocs.java | 1 +
20 files changed, 1043 insertions(+), 1 deletion(-)
create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationException.java
create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationService.java
create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceVerifier.java
create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/PlainPkceVerifier.java
create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/S256PkceVerifier.java
create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/PkceEnhancedAuthorizationCodeTokenGranter.java
create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationServiceTest.java
create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/PlainPkceVerifierTest.java
create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/S256PkceVerifierTest.java
diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.java b/model/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.java
index 84ec3ce9a01..8c760e07faf 100644
--- a/model/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.java
+++ b/model/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.java
@@ -53,6 +53,8 @@ public class OpenIdConfiguration {
private String serviceDocumentation = "http://docs.cloudfoundry.org/api/uaa/";
@JsonProperty("ui_locales_supported")
private String[] uiLocalesSupported = new String[]{"en-US"};
+ @JsonProperty("code_challenge_methods_supported")
+ private String[] codeChallengeMethodsSupported = new String[]{"S256", "plain"};
//For json serialization
public OpenIdConfiguration() {}
@@ -200,4 +202,12 @@ public String[] getUiLocalesSupported() {
public void setUiLocalesSupported(String[] uiLocalesSupported) {
this.uiLocalesSupported = uiLocalesSupported;
}
+
+ public String[] getCodeChallengeMethodsSupported() {
+ return codeChallengeMethodsSupported;
+ }
+
+ public void setCodeChallengeMethodsSupported(String[] codeChallengeMethodsSupported) {
+ this.codeChallengeMethodsSupported = codeChallengeMethodsSupported;
+ }
}
diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConfigurationTests.java b/model/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConfigurationTests.java
index 5eccec6b825..541fca4fd31 100644
--- a/model/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConfigurationTests.java
+++ b/model/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConfigurationTests.java
@@ -57,6 +57,7 @@ public void testDefaultClaims() {
assertFalse(defaultConfig.isClaimsParameterSupported());
assertEquals("http://docs.cloudfoundry.org/api/uaa/", defaultConfig.getServiceDocumentation());
assertArrayEquals(new String[]{"en-US"}, defaultConfig.getUiLocalesSupported());
+ assertArrayEquals(new String[]{"S256", "plain"}, defaultConfig.getCodeChallengeMethodsSupported());
}
}
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java
index d151c3819a4..9485f0827e1 100644
--- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java
@@ -17,6 +17,7 @@
import org.apache.http.client.utils.URIUtils;
import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal;
import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants;
+import org.cloudfoundry.identity.uaa.oauth.pkce.PkceValidationService;
import org.cloudfoundry.identity.uaa.oauth.token.CompositeToken;
import org.cloudfoundry.identity.uaa.util.UaaHttpRequestUtils;
import org.cloudfoundry.identity.uaa.zone.MultitenantClientServices;
@@ -122,6 +123,8 @@ public class UaaAuthorizationEndpoint extends AbstractEndpoint implements Authen
private OpenIdSessionStateCalculator openIdSessionStateCalculator;
private static final List supported_response_types = Arrays.asList("code", "token", "id_token");
+
+ private PkceValidationService pkceValidationService;
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map model,
@@ -159,6 +162,8 @@ public ModelAndView authorize(Map model,
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
+
+ validateAuthorizationRequestPkceParameters(authorizationRequest.getRequestParameters());
String resolvedRedirect = "";
try {
@@ -243,6 +248,32 @@ public ModelAndView authorize(Map model,
}
}
+
+ /**
+ * PKCE parameters check:
+ * code_challenge: (Optional) Must be provided for PKCE and must not be empty.
+ * code_challenge_method: (Optional) Default value is "plain". See .well-known
+ * endpoint for supported code challenge methods list.
+ * @param authorizationRequestParameters Authorization request parameters.
+ */
+ protected void validateAuthorizationRequestPkceParameters(Map authorizationRequestParameters) {
+ if (pkceValidationService != null) {
+ String codeChallenge = authorizationRequestParameters.get(PkceValidationService.CODE_CHALLENGE);
+ if (codeChallenge != null) {
+ if(!PkceValidationService.isCodeChallengeParameterValid(codeChallenge)) {
+ throw new InvalidRequestException("Code challenge length must between 43 and 128 and use only [A-Z],[a-z],[0-9],_,.,-,~ characters.");
+ }
+ String codeChallengeMethod = authorizationRequestParameters.get(PkceValidationService.CODE_CHALLENGE_METHOD);
+ if (codeChallengeMethod == null) {
+ codeChallengeMethod = "plain";
+ }
+ if (!pkceValidationService.isCodeChallengeMethodSupported(codeChallengeMethod)) {
+ throw new InvalidRequestException("Unsupported code challenge method. Supported: " +
+ pkceValidationService.getSupportedCodeChallengeMethods().toString());
+ }
+ }
+ }
+ }
// This method handles /oauth/authorize calls when user is not logged in and the prompt=none param is used
@Override
@@ -837,4 +868,13 @@ public OpenIdSessionStateCalculator getOpenIdSessionStateCalculator() {
public void setOpenIdSessionStateCalculator(OpenIdSessionStateCalculator openIdSessionStateCalculator) {
this.openIdSessionStateCalculator = openIdSessionStateCalculator;
}
+
+ public PkceValidationService getPkceValidationService() {
+ return pkceValidationService;
+ }
+
+ public void setPkceValidationService(PkceValidationService pkceValidationService) {
+ this.pkceValidationService = pkceValidationService;
+ }
+
}
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationException.java
new file mode 100644
index 00000000000..1064f638ff5
--- /dev/null
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationException.java
@@ -0,0 +1,17 @@
+package org.cloudfoundry.identity.uaa.oauth.pkce;
+
+/**
+ * Universal PKCE Validation Service exception
+ *
+ * @author Zoltan Maradics
+ *
+ */
+public class PkceValidationException extends Exception{
+
+ private static final long serialVersionUID = 7887667018613362856L;
+
+ public PkceValidationException(String msg) {
+ super(msg);
+ }
+
+}
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationService.java
new file mode 100644
index 00000000000..54d072f6e85
--- /dev/null
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationService.java
@@ -0,0 +1,167 @@
+package org.cloudfoundry.identity.uaa.oauth.pkce;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * PKCE Validation Service.
+ * - Validate Code Verifier parameter.
+ * - Validate Code Challenge parameter.
+ * - Validate Code Challenge Method parameter.
+ * - List supported code challenge methods.
+ * - Verify code verifier and code challenge based on code challenge method.
+ *
+ * @author Zoltan Maradics
+ */
+
+public class PkceValidationService {
+
+ /*
+ * Regular expression match with any string:
+ * - Length between 43 and 128
+ * - Contains only [A-Z],[a-z],[0-9],_,.,-,~ characters
+ * (Note: '_' is part of the 'w' in the pattern.)
+ */
+ private static final Pattern pattern = Pattern.compile("^[\\w\\.\\-\\~]{43,128}$");
+
+ public static final String CODE_CHALLENGE = "code_challenge";
+ public static final String CODE_CHALLENGE_METHOD = "code_challenge_method";
+ public static final String CODE_VERIFIER = "code_verifier";
+
+ private Map pkceVerifiers;
+
+ public PkceValidationService() {
+ this(Collections.emptyMap());
+ }
+
+ public PkceValidationService(Map pkceVerifiers) {
+ this.pkceVerifiers = pkceVerifiers;
+ }
+
+ /**
+ * Get all supported code challenge methods.
+ * @return Set of supported code challenge methods.
+ */
+ public Set getSupportedCodeChallengeMethods() {
+ Set supportedCodeChallengeMethods = this.pkceVerifiers.keySet();
+ return supportedCodeChallengeMethods;
+ }
+
+ /**
+ * Check code challenge method is supported or not.
+ * @param codeChallengeMethod
+ * Code challenge method parameter.
+ * @return true if the code challenge method is supported.
+ * false otherwise.
+ */
+ public boolean isCodeChallengeMethodSupported(String codeChallengeMethod) {
+ if (codeChallengeMethod == null) {
+ return false;
+ }
+ return this.pkceVerifiers.containsKey(codeChallengeMethod);
+ }
+
+ /**
+ * Check presence of PKCE parameters and validate.
+ * @param requestParameters
+ * Map of query parameters of Authorization request.
+ * @param codeVerifier
+ * Code verifier.
+ * @return true: (1) in case of Authorization Code Grant without PKCE.
+ * (2) in case of Authorization Code Grant with PKCE and code verifier
+ * matched with code challenge based on code challenge method.
+ * false: in case of Authorization Code Grant with PKCE and code verifier
+ * does not match with code challenge based on code challenge method.
+ * @throws PkceValidationException
+ * (1) Code verifier must be provided for this authorization code.
+ * (2) Code verifier not required for this authorization code.
+ */
+ public boolean checkAndValidate(Map requestParameters, String codeVerifier) throws PkceValidationException {
+ if (!hasPkceParameters(requestParameters, codeVerifier)) {
+ return true;
+ }
+ String codeChallengeMethod = extractCodeChallengeMethod(requestParameters);
+ return pkceVerifiers.get(codeChallengeMethod).verify(codeVerifier,
+ requestParameters.get(PkceValidationService.CODE_CHALLENGE));
+ }
+
+ /**
+ * Check if PKCE parameters are present.
+ * @param requestParameters
+ * Map of authorization request parameters.
+ * @param codeVerifier
+ * Code verifier.
+ * @return true: There are Code Challenge and Code Verifier parameters with not null value.
+ * false: There are no PKCE parameters.
+ * @throws PkceValidationException
+ * (1) Code verifier must be provided for this authorization code.
+ * (2) Code verifier not required for this authorization code.
+ */
+ protected boolean hasPkceParameters(Map requestParameters, String codeVerifier) throws PkceValidationException{
+ String codeChallenge = requestParameters.get(CODE_CHALLENGE);
+ if (codeChallenge != null) {
+ if (codeVerifier != null && !codeVerifier.isEmpty()) {
+ return true;
+ }else {
+ throw new PkceValidationException("Code verifier must be provided for this authorization code.");
+ }
+ }else if (codeVerifier != null && !codeVerifier.isEmpty()){
+ throw new PkceValidationException("Code verifier not required for this authorization code.");
+ }
+ return false;
+ }
+
+ /**
+ * Extract code challenge method from request.
+ * @param requestParameters: Authorization request parameters.
+ * @return
+ * If there is no code challenge method in authorization request then return: "plain"
+ * Otherwise return the value of code challenge method parameter.
+ */
+ protected String extractCodeChallengeMethod(Map requestParameters) {
+ String codeChallengeMethod = requestParameters.get(CODE_CHALLENGE_METHOD);
+ if (codeChallengeMethod == null) {
+ return "plain";
+ }else {
+ return codeChallengeMethod;
+ }
+ }
+
+ /**
+ * Validate the code verifier parameter based on RFC 7636 recommendations.
+ *
+ * @param codeVerifier: Code Verifier parameter from token request.
+ * @return true or false based on evaluation.
+ */
+ public static boolean isCodeVerifierParameterValid(String codeVerifier) {
+ return matchWithPattern(codeVerifier);
+ }
+
+ /**
+ * Validate the code challenge parameter based on RFC 7636 recommendations.
+ *
+ * @param codeChallenge: Code Challenge parameter from token request.
+ * @return true or false based on evaluation.
+ */
+ public static boolean isCodeChallengeParameterValid(String codeChallenge) {
+ return matchWithPattern(codeChallenge);
+ }
+
+ /**
+ * Validate parameter with predefined regular expression (length and used
+ * character set)
+ *
+ * @param parameter: Code Verifier or Code Challenge
+ * @return true or false based on parameter match with regular expression
+ */
+ protected static boolean matchWithPattern(String parameter) {
+ if (parameter == null) {
+ return false;
+ }
+ final Matcher matcher = pattern.matcher(parameter);
+ return matcher.matches();
+ }
+}
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceVerifier.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceVerifier.java
new file mode 100644
index 00000000000..9977d6db46a
--- /dev/null
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceVerifier.java
@@ -0,0 +1,32 @@
+package org.cloudfoundry.identity.uaa.oauth.pkce;
+
+/**
+ * Each PKCE verifier MUST implement this interface to be able to be used
+ * in PKCE validation service.
+ *
+ * @author Zoltan Maradics
+ *
+ */
+public interface PkceVerifier {
+
+ /**
+ * Verify that the code verifier matches the code challenge based on code challenge method.
+ * code_challenge = code_challenge_method(code_verifier)
+ *
+ * @param codeVerifier
+ * Code verifier parameter.
+ * @param codeChallenge
+ * Code challenge parameter.
+ * @return true: if code verifier transformed with code challenge method match
+ * with code challenge.
+ * false: otherwise.
+ */
+ public boolean verify(String codeVerifier, String codeChallenge);
+
+ /**
+ * Getter for Code Challenge Method name.
+ *
+ * @return
+ */
+ public String getCodeChallengeMethod();
+}
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/PlainPkceVerifier.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/PlainPkceVerifier.java
new file mode 100644
index 00000000000..d2e89da6f75
--- /dev/null
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/PlainPkceVerifier.java
@@ -0,0 +1,27 @@
+package org.cloudfoundry.identity.uaa.oauth.pkce.verifiers;
+
+import org.cloudfoundry.identity.uaa.oauth.pkce.PkceVerifier;
+
+/**
+ * Plain code challenge method implementation.
+ *
+ * @author Zoltan Maradics
+ *
+ */
+public class PlainPkceVerifier implements PkceVerifier{
+
+ private final String codeChallengeMethod = "plain";
+
+ @Override
+ public boolean verify(String codeVerifier, String codeChallenge) {
+ if (codeVerifier == null || codeChallenge == null) {
+ return false;
+ }
+ return codeChallenge.equals(codeVerifier);
+ }
+
+ @Override
+ public String getCodeChallengeMethod() {
+ return codeChallengeMethod;
+ }
+}
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/S256PkceVerifier.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/S256PkceVerifier.java
new file mode 100644
index 00000000000..6d3d998c595
--- /dev/null
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/S256PkceVerifier.java
@@ -0,0 +1,49 @@
+package org.cloudfoundry.identity.uaa.oauth.pkce.verifiers;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.apache.commons.codec.binary.Base64;
+import org.cloudfoundry.identity.uaa.oauth.pkce.PkceVerifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * SHA-256 code challenge method implementation.
+ *
+ * @author Zoltan Maradics
+ *
+ */
+public class S256PkceVerifier implements PkceVerifier {
+
+ private static Logger logger = LoggerFactory.getLogger(S256PkceVerifier.class);
+ private final String codeChallengeMethod = "S256";
+
+ public S256PkceVerifier() {
+ }
+
+ @Override
+ public boolean verify(String codeVerifier, String codeChallenge) {
+ if (codeVerifier == null || codeChallenge == null) {
+ return false;
+ }
+ try {
+ byte[] bytes = codeVerifier.getBytes("US-ASCII");
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ md.update(bytes, 0, bytes.length);
+ byte[] digest = md.digest();
+ return codeChallenge.contentEquals(Base64.encodeBase64URLSafeString(digest));
+ } catch (UnsupportedEncodingException e) {
+ logger.debug(e.getMessage(),e);
+ } catch (NoSuchAlgorithmException e) {
+ logger.debug(e.getMessage(),e);
+ }
+ return false;
+ }
+
+ @Override
+ public String getCodeChallengeMethod() {
+ return codeChallengeMethod;
+ }
+}
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/PkceEnhancedAuthorizationCodeTokenGranter.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/PkceEnhancedAuthorizationCodeTokenGranter.java
new file mode 100644
index 00000000000..a8bbd878ee7
--- /dev/null
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/PkceEnhancedAuthorizationCodeTokenGranter.java
@@ -0,0 +1,121 @@
+package org.cloudfoundry.identity.uaa.oauth.token;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.cloudfoundry.identity.uaa.oauth.pkce.PkceValidationException;
+import org.cloudfoundry.identity.uaa.oauth.pkce.PkceValidationService;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
+import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
+import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
+import org.springframework.security.oauth2.common.exceptions.RedirectMismatchException;
+import org.springframework.security.oauth2.common.util.OAuth2Utils;
+import org.springframework.security.oauth2.provider.ClientDetails;
+import org.springframework.security.oauth2.provider.ClientDetailsService;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.OAuth2Request;
+import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
+import org.springframework.security.oauth2.provider.TokenRequest;
+import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
+import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
+import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
+
+public class PkceEnhancedAuthorizationCodeTokenGranter extends AuthorizationCodeTokenGranter {
+
+ private final AuthorizationCodeServices authorizationCodeServices;
+
+ private PkceValidationService pkceValidationService;
+
+ public PkceEnhancedAuthorizationCodeTokenGranter(AuthorizationServerTokenServices tokenServices,
+ AuthorizationCodeServices authorizationCodeServices, ClientDetailsService clientDetailsService,
+ OAuth2RequestFactory requestFactory) {
+ super(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory);
+ this.authorizationCodeServices = authorizationCodeServices;
+ }
+
+ @Override
+ protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
+
+ Map parameters = tokenRequest.getRequestParameters();
+ String authorizationCode = parameters.get("code");
+ String redirectUri = parameters.get(OAuth2Utils.REDIRECT_URI);
+
+ if (authorizationCode == null) {
+ throw new InvalidRequestException("An authorization code must be supplied.");
+ }
+
+ /*
+ * PKCE code verifier parameter length and charset validation
+ */
+ String codeVerifier = parameters.get(PkceValidationService.CODE_VERIFIER);
+ if (codeVerifier != null && !PkceValidationService.isCodeVerifierParameterValid(codeVerifier)) {
+ throw new InvalidRequestException("Code verifier length must between 43 and 128 and use only [A-Z],[a-z],[0-9],_,.,-,~ characters.");
+ }
+
+ OAuth2Authentication storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode);
+ if (storedAuth == null) {
+ throw new InvalidGrantException("Invalid authorization code: " + authorizationCode);
+ }
+
+ /*
+ * PKCE code verifier parameter verification
+ */
+ try {
+ if (pkceValidationService != null && !pkceValidationService.checkAndValidate(storedAuth.getOAuth2Request().getRequestParameters(), codeVerifier)) {
+ // has PkceValidation service and validation failed
+ throw new InvalidGrantException("Invalid code verifier: " + codeVerifier);
+ }
+ } catch (PkceValidationException exception) {
+ // during the validation one of the PKCE parameters missing
+ throw new InvalidGrantException("PKCE error: "+ exception.getMessage());
+ }
+ // No pkceValidationService defined or Pkce validation successfully passed
+
+ OAuth2Request pendingOAuth2Request = storedAuth.getOAuth2Request();
+ // https://jira.springsource.org/browse/SECOAUTH-333
+ // This might be null, if the authorization was done without the redirect_uri
+ // parameter
+ String redirectUriApprovalParameter = pendingOAuth2Request.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
+
+ if ((redirectUri != null || redirectUriApprovalParameter != null)
+ && !pendingOAuth2Request.getRedirectUri().equals(redirectUri)) {
+ throw new RedirectMismatchException("Redirect URI mismatch.");
+ }
+
+ String pendingClientId = pendingOAuth2Request.getClientId();
+ String clientId = tokenRequest.getClientId();
+ if (clientId != null && !clientId.equals(pendingClientId)) {
+ // just a sanity check.
+ throw new InvalidClientException("Client ID mismatch");
+ }
+
+ // Secret is not required in the authorization request, so it won't be available
+ // in the pendingAuthorizationRequest. We do want to check that a secret is
+ // provided
+ // in the token request, but that happens elsewhere.
+
+ Map combinedParameters = new HashMap(
+ pendingOAuth2Request.getRequestParameters());
+ // Combine the parameters adding the new ones last so they override if there are
+ // any clashes
+ combinedParameters.putAll(parameters);
+
+ // Make a new stored request with the combined parameters
+ OAuth2Request finalStoredOAuth2Request = pendingOAuth2Request.createOAuth2Request(combinedParameters);
+
+ Authentication userAuth = storedAuth.getUserAuthentication();
+
+ return new OAuth2Authentication(finalStoredOAuth2Request, userAuth);
+
+ }
+
+ public PkceValidationService getPkceValidationService() {
+ return pkceValidationService;
+ }
+
+ public void setPkceValidationService(PkceValidationService pkceValidationService) {
+ this.pkceValidationService = pkceValidationService;
+ }
+
+}
diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java
index 454b54b24e4..6cf3d4c8657 100644
--- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java
+++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java
@@ -3,7 +3,12 @@
import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication;
import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails;
import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal;
+import org.cloudfoundry.identity.uaa.oauth.pkce.PkceValidationService;
+import org.cloudfoundry.identity.uaa.oauth.pkce.PkceVerifier;
+import org.cloudfoundry.identity.uaa.oauth.pkce.verifiers.PlainPkceVerifier;
+import org.cloudfoundry.identity.uaa.oauth.pkce.verifiers.S256PkceVerifier;
import org.cloudfoundry.identity.uaa.oauth.token.CompositeToken;
+import org.cloudfoundry.identity.uaa.test.UaaTestAccounts;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -42,6 +47,7 @@ public class UaaAuthorizationEndpointTest {
private AuthorizationCodeServices authorizationCodeServices;
private Set responseTypes;
private OpenIdSessionStateCalculator openIdSessionStateCalculator;
+ private PkceValidationService pkceValidationService;
private HashMap model = new HashMap<>();
private SimpleSessionStatus sessionStatus = new SimpleSessionStatus();
@@ -58,6 +64,14 @@ public void setup() {
uaaAuthorizationEndpoint.setOpenIdSessionStateCalculator(openIdSessionStateCalculator);
responseTypes = new HashSet<>();
+ PlainPkceVerifier plainPkceVerifier = new PlainPkceVerifier();
+ S256PkceVerifier s256PkceVerifier = new S256PkceVerifier();
+ Map pkceVerifiers = new HashMap();
+ pkceVerifiers.put(plainPkceVerifier.getCodeChallengeMethod(), plainPkceVerifier);
+ pkceVerifiers.put(s256PkceVerifier.getCodeChallengeMethod(), s256PkceVerifier);
+ pkceValidationService = new PkceValidationService(pkceVerifiers);
+ uaaAuthorizationEndpoint.setPkceValidationService(pkceValidationService);
+
when(openIdSessionStateCalculator.calculate("userid", null, "http://example.com")).thenReturn("opbshash");
when(authorizationCodeServices.createAuthorizationCode(any(OAuth2Authentication.class))).thenReturn("code");
}
@@ -305,6 +319,35 @@ public void testApproveWithModifiedApprovalParameters() {
assertThat(((RedirectView)view).getUrl(), containsString("error=invalid_scope"));
}
+ @Test(expected = InvalidRequestException.class)
+ public void testShortCodeChallengeParameter() throws Exception {
+ Map parameters = new HashMap();
+ parameters.put(PkceValidationService.CODE_CHALLENGE, "ShortCodeChallenge");
+ uaaAuthorizationEndpoint.validateAuthorizationRequestPkceParameters(parameters);
+ }
+
+ @Test(expected = InvalidRequestException.class)
+ public void testLongCodeChallengeParameter() throws Exception {
+ Map parameters = new HashMap();
+ parameters.put(PkceValidationService.CODE_CHALLENGE, "LongCodeChallenge12346574382823193700987654321326352173528351287635126532123452534234254323254325325325432342532532254325432532532");
+ uaaAuthorizationEndpoint.validateAuthorizationRequestPkceParameters(parameters);
+ }
+
+ @Test(expected = InvalidRequestException.class)
+ public void testForbiddenCodeChallengeParameter() throws Exception {
+ Map parameters = new HashMap();
+ parameters.put(PkceValidationService.CODE_CHALLENGE, "#ForbiddenCodeChallenge098765445647544743211234657438282319370#");
+ uaaAuthorizationEndpoint.validateAuthorizationRequestPkceParameters(parameters);
+ }
+
+ @Test(expected = InvalidRequestException.class)
+ public void testUnsupportedCodeChallengeMethodParameter() throws Exception {
+ Map parameters = new HashMap();
+ parameters.put(PkceValidationService.CODE_CHALLENGE, UaaTestAccounts.CODE_CHALLENGE);
+ parameters.put(PkceValidationService.CODE_CHALLENGE_METHOD, "unsupportedMethod");
+ uaaAuthorizationEndpoint.validateAuthorizationRequestPkceParameters(parameters);
+ }
+
private AuthorizationRequest getAuthorizationRequest(String clientId, String redirectUri, String state,
String scope, Set responseTypes) {
HashMap parameters = new HashMap<>();
diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationServiceTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationServiceTest.java
new file mode 100644
index 00000000000..bb0a11e0e49
--- /dev/null
+++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationServiceTest.java
@@ -0,0 +1,129 @@
+package org.cloudfoundry.identity.uaa.oauth.pkce;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.*;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.cloudfoundry.identity.uaa.oauth.pkce.verifiers.PlainPkceVerifier;
+import org.cloudfoundry.identity.uaa.oauth.pkce.verifiers.S256PkceVerifier;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ *
+ * @author Zoltan Maradics
+ *
+ */
+public class PkceValidationServiceTest {
+
+ private PkceValidationService pkceValidationService;
+ private Map authorizeRequestParameters;
+
+ private final String longCodeChallengeOrCodeVerifierParameter = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cME9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cME9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
+ private final String shortCodeChallengeOrCodeVerifierParameter = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-c";
+ private final String containsForbiddenCharactersCodeChallengeOrCodeVerifierParameter = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM%";
+ private final String validPlainCodeChallengeOrCodeVerifierParameter = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
+
+ private final String invalidCodeChallengeMethod = "InvalidMethod";
+
+ @Before
+ public void createPkceValidationService() throws Exception {
+ pkceValidationService = new PkceValidationService(createPkceVerifiers());
+ authorizeRequestParameters = new HashMap();
+ }
+
+ @Test
+ public void testLongCodeChallengeParameter() throws Exception {
+ assertFalse(PkceValidationService.matchWithPattern(longCodeChallengeOrCodeVerifierParameter));
+ }
+
+ @Test
+ public void testShortCodeChallengeParameter() throws Exception {
+ assertFalse(PkceValidationService.matchWithPattern(shortCodeChallengeOrCodeVerifierParameter));
+ }
+
+ @Test
+ public void testContainsForbiddenCharactersCodeChallengeParameter() throws Exception {
+ assertFalse(PkceValidationService
+ .matchWithPattern(containsForbiddenCharactersCodeChallengeOrCodeVerifierParameter));
+ }
+
+ @Test
+ public void testNullCodeChallengeOrCodeVerifierParameters() throws Exception {
+ assertFalse(PkceValidationService.matchWithPattern(null));
+ }
+
+ @Test
+ public void testValidCodeChallengeParameter() throws Exception {
+ assertTrue(PkceValidationService.matchWithPattern(validPlainCodeChallengeOrCodeVerifierParameter));
+ }
+
+ @Test
+ public void testInvalidCodeChallengeMethodParameter() throws Exception {
+ assertFalse(pkceValidationService.isCodeChallengeMethodSupported(invalidCodeChallengeMethod));
+ }
+
+ @Test
+ public void testNullCodeChallengeMethodParameter() throws Exception {
+ assertFalse(pkceValidationService.isCodeChallengeMethodSupported(null));
+ }
+
+ @Test
+ public void testS256CodeChallengeMethodParameter() throws Exception {
+ assertTrue(pkceValidationService.isCodeChallengeMethodSupported("S256"));
+ }
+
+ @Test
+ public void testPlainCodeChallengeMethodParameter() throws Exception {
+ assertTrue(pkceValidationService.isCodeChallengeMethodSupported("plain"));
+ }
+
+ @Test
+ public void testNoPkceParametersForEvaluation() throws Exception {
+ assertTrue(pkceValidationService.checkAndValidate(authorizeRequestParameters, null));
+ }
+
+ @Test(expected = PkceValidationException.class)
+ public void testCodeChallengeMissingForEvaluation() throws Exception {
+ pkceValidationService.checkAndValidate(authorizeRequestParameters,
+ validPlainCodeChallengeOrCodeVerifierParameter);
+ }
+
+ @Test(expected = PkceValidationException.class)
+ public void testCodeVerifierMissingForEvaluation() throws Exception {
+ authorizeRequestParameters.put(PkceValidationService.CODE_CHALLENGE,
+ validPlainCodeChallengeOrCodeVerifierParameter);
+ pkceValidationService.checkAndValidate(authorizeRequestParameters, "");
+ }
+
+ @Test
+ public void testNoCodeChallengeMethodForEvaluation() throws Exception {
+ authorizeRequestParameters.put(PkceValidationService.CODE_CHALLENGE,
+ validPlainCodeChallengeOrCodeVerifierParameter);
+ assertThat(pkceValidationService.checkAndValidate(authorizeRequestParameters,
+ validPlainCodeChallengeOrCodeVerifierParameter), is(true));
+ }
+
+ @Test
+ public void testPkceValidationServiceConstructorWithCodeChallengeMethodsMap() throws Exception {
+ Set testHashSet = new HashSet<>(Arrays.asList("S256", "plain"));
+ assertEquals(testHashSet, pkceValidationService.getSupportedCodeChallengeMethods());
+ }
+
+ private Map createPkceVerifiers() {
+ S256PkceVerifier s256PkceVerifier = new S256PkceVerifier();
+ PlainPkceVerifier plainPkceVerifier = new PlainPkceVerifier();
+ Map pkceVerifiers = new HashMap();
+ pkceVerifiers.put(plainPkceVerifier.getCodeChallengeMethod(), plainPkceVerifier);
+ pkceVerifiers.put(s256PkceVerifier.getCodeChallengeMethod(), s256PkceVerifier);
+ return pkceVerifiers;
+ }
+}
diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/PlainPkceVerifierTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/PlainPkceVerifierTest.java
new file mode 100644
index 00000000000..54881644af4
--- /dev/null
+++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/PlainPkceVerifierTest.java
@@ -0,0 +1,47 @@
+package org.cloudfoundry.identity.uaa.oauth.pkce.verifiers;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.cloudfoundry.identity.uaa.oauth.pkce.verifiers.PlainPkceVerifier;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ *
+ * @author Zoltan Maradics
+ *
+ */
+public class PlainPkceVerifierTest {
+
+ private PlainPkceVerifier plainPkceVerifier;
+
+ private final String matchParameter = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
+ private final String mismatchParameter = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
+
+ @Before
+ public void createPlainCodeChallengeMethod() throws Exception {
+ plainPkceVerifier = new PlainPkceVerifier();
+ }
+
+ @Test
+ public void testCodeVerifierMethodWithMatchParameters() throws Exception {
+ assertTrue(plainPkceVerifier.verify(matchParameter, matchParameter));
+ }
+
+ @Test
+ public void testCodeVerifierMethodWithMismatchParameters() throws Exception {
+ assertFalse(plainPkceVerifier.verify(matchParameter, mismatchParameter));
+ }
+
+ @Test
+ public void testCodeChallengeIsNull() throws Exception {
+ assertFalse(plainPkceVerifier.verify(matchParameter, null));
+ }
+
+ @Test
+ public void testCodeVerifierIsNull() throws Exception {
+ assertFalse(plainPkceVerifier.verify(null, matchParameter));
+ }
+
+}
diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/S256PkceVerifierTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/S256PkceVerifierTest.java
new file mode 100644
index 00000000000..aef11f7736f
--- /dev/null
+++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/S256PkceVerifierTest.java
@@ -0,0 +1,47 @@
+package org.cloudfoundry.identity.uaa.oauth.pkce.verifiers;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.cloudfoundry.identity.uaa.oauth.pkce.verifiers.S256PkceVerifier;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ *
+ * @author Zoltan Maradics
+ *
+ */
+public class S256PkceVerifierTest {
+
+ private S256PkceVerifier s256CodeChallengeMethod;
+
+ private final String validCodeChallenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
+ private final String validCodeVerifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
+
+ @Before
+ public void createS256CodeChallengeMethod() throws Exception {
+ s256CodeChallengeMethod = new S256PkceVerifier();
+ }
+
+ @Test
+ public void testCodeVerifierMethodWithMatchParameters() throws Exception {
+ assertTrue(s256CodeChallengeMethod.verify(validCodeVerifier, validCodeChallenge));
+ }
+
+ @Test
+ public void testCodeVerifierMethodWithMismatchParameters() throws Exception {
+ assertFalse(s256CodeChallengeMethod.verify(validCodeVerifier, validCodeVerifier));
+ }
+
+ @Test
+ public void testCodeChallengeIsNull() throws Exception {
+ assertFalse(s256CodeChallengeMethod.verify(validCodeVerifier, null));
+ }
+
+ @Test
+ public void testCodeVerifierIsNull() throws Exception {
+ assertFalse(s256CodeChallengeMethod.verify(null, validCodeChallenge));
+ }
+
+}
diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/test/UaaTestAccounts.java b/server/src/test/java/org/cloudfoundry/identity/uaa/test/UaaTestAccounts.java
index 0e8aecb00be..08755fbce4d 100644
--- a/server/src/test/java/org/cloudfoundry/identity/uaa/test/UaaTestAccounts.java
+++ b/server/src/test/java/org/cloudfoundry/identity/uaa/test/UaaTestAccounts.java
@@ -52,6 +52,12 @@ public class UaaTestAccounts implements TestAccounts {
public static final String DEFAULT_PASSWORD = "koala";
public static final String DEFAULT_USERNAME = "marissa";
+
+ public static final String CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
+
+ public static final String CODE_CHALLENGE_METHOD_S256 = "S256";
+
+ public static final String CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
private static final Logger logger = LoggerFactory.getLogger(UaaTestAccounts.class);
diff --git a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml
index eb5efb7f3a9..98839e9d598 100755
--- a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml
+++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml
@@ -34,13 +34,28 @@
client-details-service-ref="jdbcClientDetailsService"
token-services-ref="tokenServices" user-approval-handler-ref="userManagedApprovalHandler"
authorization-request-manager-ref="authorizationRequestManager" request-validator-ref="oauth2RequestValidator">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -158,6 +173,7 @@
+
@@ -454,6 +470,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/AuthorizationCodeGrantIntegrationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/AuthorizationCodeGrantIntegrationTests.java
index a87dd3946ec..ae9fad2ad74 100755
--- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/AuthorizationCodeGrantIntegrationTests.java
+++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/AuthorizationCodeGrantIntegrationTests.java
@@ -32,7 +32,9 @@
import java.util.Map;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
+import static org.hamcrest.CoreMatchers.containsString;
/**
* @author Dave Syer
@@ -55,6 +57,98 @@ public void testSuccessfulAuthorizationCodeFlow() throws Exception {
}
@Test
+ public void testSuccessfulAuthorizationCodeFlowWithPkceS256() throws Exception {
+ testAuthorizationCodeFlowWithPkce_Internal(UaaTestAccounts.CODE_CHALLENGE,
+ UaaTestAccounts.CODE_CHALLENGE_METHOD_S256, UaaTestAccounts.CODE_VERIFIER);
+ testAuthorizationCodeFlowWithPkce_Internal(UaaTestAccounts.CODE_CHALLENGE,
+ UaaTestAccounts.CODE_CHALLENGE_METHOD_S256, UaaTestAccounts.CODE_VERIFIER);
+ }
+
+ @Test
+ public void testSuccessfulAuthorizationCodeFlowWithPkcePlain() throws Exception {
+ testAuthorizationCodeFlowWithPkce_Internal(UaaTestAccounts.CODE_CHALLENGE, "plain", UaaTestAccounts.CODE_CHALLENGE);
+ testAuthorizationCodeFlowWithPkce_Internal(UaaTestAccounts.CODE_CHALLENGE, "plain", UaaTestAccounts.CODE_CHALLENGE);
+ }
+
+ @Test
+ public void testPkcePlainWithWrongCodeVerifier() throws Exception {
+ ResponseEntity
+ */
+ private boolean matches(Predicate matcher, String requestedRedirect) {
+ for (int i = 1; i <= 5; i++) {
if (!matcher.test(requestedRedirect)) {
return false;
}
- String decoded = urlDecodeAndNormalize(requestedRedirect); //todo: optimize by only normalizing at end?
- if (decoded.equals(requestedRedirect)) { //todo: move to for loop?
- return true;
+ String decoded = urlDecode(requestedRedirect);
+ if (decoded.equals(requestedRedirect)) {
+ return matcher.test(StringUtils.cleanPath(decoded));
}
requestedRedirect = decoded;
}
+ logger.debug("aborted url decoding loop to mitigate DOS attack that sends a repeatedly url-encoded path");
return false;
}
- private String urlDecodeAndNormalize(String url) {
- return StringUtils.cleanPath(urlDecode(url));
- }
-
@SneakyThrows
private String urlDecode(String url) {
return URLDecoder.decode(url, StandardCharsets.UTF_8.name());
From 33688a76204aaf63ff7986f9c887bba82070f240 Mon Sep 17 00:00:00 2001
From: Ariel Taitz
Date: Thu, 27 Jun 2019 17:10:26 +0300
Subject: [PATCH 007/140] meaningful name for number of url-decode attempts
---
.../identity/uaa/oauth/beans/LegacyRedirectResolver.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
index 8683528c866..d2736ffc432 100644
--- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
@@ -89,7 +89,8 @@ protected boolean redirectMatches(String requestedRedirect, String clientRedirec
*
*/
private boolean matches(Predicate matcher, String requestedRedirect) {
- for (int i = 1; i <= 5; i++) {
+ int maxDecodeAttempts = 5;
+ for (int i = 1; i <= maxDecodeAttempts; i++) {
if (!matcher.test(requestedRedirect)) {
return false;
}
From 3132bf184f0620aa0d8fa51faf48ffe0282dbd59 Mon Sep 17 00:00:00 2001
From: Ariel Taitz
Date: Thu, 27 Jun 2019 17:13:36 +0300
Subject: [PATCH 008/140] capitilize first letter of log sentence
---
.../identity/uaa/oauth/beans/LegacyRedirectResolver.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
index d2736ffc432..818784dcda9 100644
--- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
@@ -100,7 +100,7 @@ private boolean matches(Predicate matcher, String requestedRedirect) {
}
requestedRedirect = decoded;
}
- logger.debug("aborted url decoding loop to mitigate DOS attack that sends a repeatedly url-encoded path");
+ logger.debug("Aborted url decoding loop to mitigate DOS attack that sends a repeatedly url-encoded path");
return false;
}
From af733108498a964c21d289ccdf0360e95bc532c1 Mon Sep 17 00:00:00 2001
From: Ariel Taitz
Date: Thu, 27 Jun 2019 17:30:58 +0300
Subject: [PATCH 009/140] better name for matches function
---
.../identity/uaa/oauth/beans/LegacyRedirectResolver.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
index 818784dcda9..3d6e544bac6 100644
--- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
@@ -59,7 +59,7 @@ protected boolean redirectMatches(String requestedRedirect, String clientRedirec
} else {
matcher = req -> super.redirectMatches(req, normalizedClientRedirect);
}
- return matches(matcher, normalizedRequestedRedirect);
+ return matchesAfterNormalization(matcher, normalizedRequestedRedirect);
} catch (IllegalArgumentException e) {
logger.error(
String.format("Could not validate whether requestedRedirect (%s) matches clientRedirectUri (%s)",
@@ -88,7 +88,7 @@ protected boolean redirectMatches(String requestedRedirect, String clientRedirec
*
*
*/
- private boolean matches(Predicate matcher, String requestedRedirect) {
+ private boolean matchesAfterNormalization(Predicate matcher, String requestedRedirect) {
int maxDecodeAttempts = 5;
for (int i = 1; i <= maxDecodeAttempts; i++) {
if (!matcher.test(requestedRedirect)) {
From 0775178f42ed0e9653346e1fd67b698af8cd383c Mon Sep 17 00:00:00 2001
From: Ariel Taitz
Date: Thu, 11 Jul 2019 16:34:27 +0300
Subject: [PATCH 010/140] Added "final" modifier for method-local constant
---
.../identity/uaa/oauth/beans/LegacyRedirectResolver.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
index 3d6e544bac6..ebfcd6a845a 100644
--- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/LegacyRedirectResolver.java
@@ -89,7 +89,7 @@ protected boolean redirectMatches(String requestedRedirect, String clientRedirec
*
*/
private boolean matchesAfterNormalization(Predicate matcher, String requestedRedirect) {
- int maxDecodeAttempts = 5;
+ final int maxDecodeAttempts = 5;
for (int i = 1; i <= maxDecodeAttempts; i++) {
if (!matcher.test(requestedRedirect)) {
return false;
From e8a845883d1e3e311c377a6ea7861c3d8f8247c7 Mon Sep 17 00:00:00 2001
From: Cloud Foundry Identity Team
Date: Thu, 28 Jan 2021 01:44:05 +0000
Subject: [PATCH 011/140] Update UAA image reference in k8s deployment template
to
cloudfoundry/uaa@sha256:37fcf63a156b75174fbc7be0e3809d64cda6851de0bfe6fa7f12793d919de96a
---
k8s/templates/values/image.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/k8s/templates/values/image.yml b/k8s/templates/values/image.yml
index 756a85434ee..54e35b92c5b 100644
--- a/k8s/templates/values/image.yml
+++ b/k8s/templates/values/image.yml
@@ -1,3 +1,3 @@
#@data/values
---
-image: "cloudfoundry/uaa@sha256:7b76095c9848216b4871b7120fe5366ab560d20635027c17833267d5e425197f"
+image: "cloudfoundry/uaa@sha256:37fcf63a156b75174fbc7be0e3809d64cda6851de0bfe6fa7f12793d919de96a"
From 333954df0df9327cea2a75c906a63aa5fea3544e Mon Sep 17 00:00:00 2001
From: Florian Tack
Date: Tue, 10 Nov 2020 16:42:22 +0100
Subject: [PATCH 012/140] Restructure login method to not read all
IdentityProviders on login_hint
---
.../identity/uaa/login/LoginInfoEndpoint.java | 70 ++++++++++++-------
.../uaa/login/LoginInfoEndpointTests.java | 24 +++++--
2 files changed, 65 insertions(+), 29 deletions(-)
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java
index 4a8042215b0..aea0b5682f2 100755
--- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java
@@ -74,6 +74,7 @@
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
@@ -273,15 +274,31 @@ private String login(Model model, Principal principal, List excludedProm
clientName = (String) clientInfo.get(ClientConstants.CLIENT_NAME);
}
- Map samlIdentityProviders =
- getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys);
- Map oauthIdentityProviders =
- getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys);
- Map allIdentityProviders =
- new HashMap<>() {{
- putAll(samlIdentityProviders);
- putAll(oauthIdentityProviders);
- }};
+ Map samlIdentityProviders;
+ Map oauthIdentityProviders;
+ Map allIdentityProviders = Collections.emptyMap();
+ Map loginHintProviders = Collections.emptyMap();
+
+ String loginHintParam = extractLoginHintParam(session, request);
+ UaaLoginHint uaaLoginHint = UaaLoginHint.parseRequestParameter(loginHintParam);
+ if (uaaLoginHint != null && (allowedIdentityProviderKeys == null || allowedIdentityProviderKeys.contains(uaaLoginHint.getOrigin()))) {
+ if (!(OriginKeys.UAA.equals(uaaLoginHint.getOrigin()) || OriginKeys.LDAP.equals(uaaLoginHint.getOrigin()))) {
+ try {
+ IdentityProvider loginHintProvider = externalOAuthProviderConfigurator
+ .retrieveByOrigin(uaaLoginHint.getOrigin(), IdentityZoneHolder.get().getId());
+ loginHintProviders = Collections.singletonList(loginHintProvider).stream().collect(
+ new MapCollector(
+ IdentityProvider::getOriginKey, IdentityProvider::getConfig));
+ } catch (EmptyResultDataAccessException ignored) {
+ }
+ }
+ oauthIdentityProviders = Collections.emptyMap();
+ samlIdentityProviders = Collections.emptyMap();
+ } else {
+ samlIdentityProviders = getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys);
+ oauthIdentityProviders = getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys);
+ allIdentityProviders = new HashMap<>() {{putAll(samlIdentityProviders);putAll(oauthIdentityProviders);}};
+ }
boolean fieldUsernameShow = true;
boolean returnLoginPrompts = true;
@@ -311,8 +328,8 @@ private String login(Model model, Principal principal, List excludedProm
}
Map.Entry idpForRedirect;
- idpForRedirect = evaluateLoginHint(model, session, samlIdentityProviders,
- oauthIdentityProviders, allIdentityProviders, allowedIdentityProviderKeys, request);
+ idpForRedirect = evaluateLoginHint(model, samlIdentityProviders,
+ oauthIdentityProviders, allIdentityProviders, allowedIdentityProviderKeys, loginHintParam, uaaLoginHint, loginHintProviders);
boolean discoveryEnabled = IdentityZoneHolder.get().getConfig().isIdpDiscoveryEnabled();
boolean discoveryPerformed = Boolean.parseBoolean(request.getParameter("discoveryPerformed"));
@@ -480,27 +497,29 @@ private Map.Entry evaluateIdpDiscove
return idpForRedirect;
}
+ private String extractLoginHintParam(HttpSession session, HttpServletRequest request) {
+ String loginHintParam =
+ ofNullable(session)
+ .flatMap(s -> ofNullable(SessionUtils.getSavedRequestSession(s)))
+ .flatMap(sr -> ofNullable(sr.getParameterValues("login_hint")))
+ .flatMap(lhValues -> Arrays.stream(lhValues).findFirst())
+ .orElse(request.getParameter("login_hint"));
+ return loginHintParam;
+ }
+
private Map.Entry evaluateLoginHint(
Model model,
- HttpSession session,
Map samlIdentityProviders,
Map oauthIdentityProviders,
Map allIdentityProviders,
List allowedIdentityProviderKeys,
- HttpServletRequest request
+ String loginHintParam,
+ UaaLoginHint uaaLoginHint,
+ Map loginHintProviders
) {
-
Map.Entry idpForRedirect = null;
- String loginHintParam =
- ofNullable(session)
- .flatMap(s -> ofNullable(SessionUtils.getSavedRequestSession(s)))
- .flatMap(sr -> ofNullable(sr.getParameterValues("login_hint")))
- .flatMap(lhValues -> Arrays.stream(lhValues).findFirst())
- .orElse(request.getParameter("login_hint"));
-
if (loginHintParam != null) {
// parse login_hint in JSON format
- UaaLoginHint uaaLoginHint = UaaLoginHint.parseRequestParameter(loginHintParam);
if (uaaLoginHint != null) {
logger.debug("Received login hint: " + loginHintParam);
logger.debug("Received login hint with origin: " + uaaLoginHint.getOrigin());
@@ -519,12 +538,13 @@ private Map.Entry evaluateLoginHint(
allIdentityProviders.entrySet().stream().filter(
idp -> idp.getKey().equals(uaaLoginHint.getOrigin())
).collect(Collectors.toList());
- if (hintIdentityProviders.size() > 1) {
+ if (loginHintProviders.size() > 1) {
throw new IllegalStateException(
"There is a misconfiguration with the identity provider(s). Please contact your system administrator."
);
- } else if (hintIdentityProviders.size() == 1) {
- idpForRedirect = hintIdentityProviders.get(0);
+ }
+ if (loginHintProviders.size() == 1) {
+ idpForRedirect = new ArrayList<>(loginHintProviders.entrySet()).get(0);
logger.debug("Setting redirect from origin login_hint to: " + idpForRedirect);
} else {
logger.debug("Client does not allow provider for login_hint with origin key: "
diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java
index 9e1dbb0d570..9f6f0110237 100755
--- a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java
+++ b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java
@@ -36,6 +36,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.dao.DataAccessException;
+import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
@@ -1163,7 +1164,7 @@ void loginHintOriginOidc() throws Exception {
MultitenantClientServices clientDetailsService = mockClientService();
- mockOidcProvider(mockIdentityProviderProvisioning);
+ mockLoginHintProvider(configurator);
LoginInfoEndpoint endpoint = getEndpoint(IdentityZoneHolder.get(), clientDetailsService);
@@ -1184,7 +1185,7 @@ void loginHintOriginOidcForJson() throws Exception {
MultitenantClientServices clientDetailsService = mockClientService();
- mockOidcProvider(mockIdentityProviderProvisioning);
+ mockLoginHintProvider(configurator);
LoginInfoEndpoint endpoint = getEndpoint(IdentityZoneHolder.get(), clientDetailsService);
@@ -1197,7 +1198,7 @@ void loginHintOriginOidcForJson() throws Exception {
assertNotNull(extendedModelMap.get("prompts"));
assertTrue(extendedModelMap.get("prompts") instanceof Map);
Map returnedPrompts = (Map) extendedModelMap.get("prompts");
- assertEquals(3, returnedPrompts.size());
+ assertEquals(2, returnedPrompts.size());
}
@Test
@@ -1210,6 +1211,7 @@ void loginHintOriginInvalid() throws Exception {
SavedRequest savedRequest = SessionUtils.getSavedRequestSession(mockHttpServletRequest.getSession());
when(savedRequest.getParameterValues("login_hint")).thenReturn(new String[]{"{\"origin\":\"my-OIDC-idp1\"}"});
+ when(configurator.retrieveByOrigin(eq("my-OIDC-idp1"), anyString())).thenThrow(new EmptyResultDataAccessException(0));
endpoint.loginForHtml(extendedModelMap, null, mockHttpServletRequest, singletonList(MediaType.TEXT_HTML));
@@ -1405,7 +1407,7 @@ void loginHintOverridesDefaultProvider() throws Exception {
MultitenantClientServices clientDetailsService = mockClientService();
- mockOidcProvider(mockIdentityProviderProvisioning);
+ mockLoginHintProvider(configurator);
LoginInfoEndpoint endpoint = getEndpoint(IdentityZoneHolder.get(), clientDetailsService);
@@ -1656,4 +1658,18 @@ private static void mockOidcProvider(IdentityProviderProvisioning mockIdentityPr
when(mockOidcConfig.isShowLinkText()).thenReturn(true);
when(mockIdentityProviderProvisioning.retrieveAll(anyBoolean(), any())).thenReturn(singletonList(mockProvider));
}
+
+ private static void mockLoginHintProvider(ExternalOAuthProviderConfigurator mockIdentityProviderProvisioning)
+ throws MalformedURLException {
+ IdentityProvider mockProvider = mock(IdentityProvider.class);
+ when(mockProvider.getOriginKey()).thenReturn("my-OIDC-idp1");
+ when(mockProvider.getType()).thenReturn(OriginKeys.OIDC10);
+ AbstractExternalOAuthIdentityProviderDefinition mockOidcConfig = mock(OIDCIdentityProviderDefinition.class);
+ when(mockOidcConfig.getAuthUrl()).thenReturn(new URL("http://localhost:8080/uaa"));
+ when(mockOidcConfig.getRelyingPartyId()).thenReturn("client-id");
+ when(mockOidcConfig.getResponseType()).thenReturn("token");
+ when(mockProvider.getConfig()).thenReturn(mockOidcConfig);
+ when(mockOidcConfig.isShowLinkText()).thenReturn(true);
+ when(mockIdentityProviderProvisioning.retrieveByOrigin(eq("my-OIDC-idp1"), any())).thenReturn(mockProvider);
+ }
}
From 0e31bb4cce3399d242b477dd2a9d992e2bcfc407 Mon Sep 17 00:00:00 2001
From: Florian Tack
Date: Thu, 12 Nov 2020 11:29:25 +0100
Subject: [PATCH 013/140] Move read configurations and parameters to allow
earlier decisions
---
.../identity/uaa/login/LoginInfoEndpoint.java | 36 ++++++++++---------
.../uaa/login/LoginInfoEndpointTests.java | 7 ----
2 files changed, 20 insertions(+), 23 deletions(-)
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java
index aea0b5682f2..d9b3c8a276e 100755
--- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java
@@ -274,14 +274,25 @@ private String login(Model model, Principal principal, List excludedProm
clientName = (String) clientInfo.get(ClientConstants.CLIENT_NAME);
}
+ //Read all configuration and parameters at the beginning to allow earlier decisions
+ boolean discoveryEnabled = IdentityZoneHolder.get().getConfig().isIdpDiscoveryEnabled();
+ boolean discoveryPerformed = Boolean.parseBoolean(request.getParameter("discoveryPerformed"));
+ String defaultIdentityProviderName = IdentityZoneHolder.get().getConfig().getDefaultIdentityProvider();
+ boolean accountChooserEnabled = IdentityZoneHolder.get().getConfig().isAccountChooserEnabled();
+ boolean otherAccountSignIn = Boolean.parseBoolean(request.getParameter("otherAccountSignIn"));
+ boolean savedAccountsEmpty = getSavedAccounts(request.getCookies(), SavedAccountOption.class).isEmpty();
+ boolean accountChooserNeeded = accountChooserEnabled && !(otherAccountSignIn || savedAccountsEmpty) && discoveryEnabled &&!discoveryPerformed;
+
+ String loginHintParam = extractLoginHintParam(session, request);
+ UaaLoginHint uaaLoginHint = UaaLoginHint.parseRequestParameter(loginHintParam);
+
Map samlIdentityProviders;
Map oauthIdentityProviders;
Map allIdentityProviders = Collections.emptyMap();
Map loginHintProviders = Collections.emptyMap();
- String loginHintParam = extractLoginHintParam(session, request);
- UaaLoginHint uaaLoginHint = UaaLoginHint.parseRequestParameter(loginHintParam);
if (uaaLoginHint != null && (allowedIdentityProviderKeys == null || allowedIdentityProviderKeys.contains(uaaLoginHint.getOrigin()))) {
+ // Login hint: Only try to read the hinted IdP from database
if (!(OriginKeys.UAA.equals(uaaLoginHint.getOrigin()) || OriginKeys.LDAP.equals(uaaLoginHint.getOrigin()))) {
try {
IdentityProvider loginHintProvider = externalOAuthProviderConfigurator
@@ -294,6 +305,10 @@ private String login(Model model, Principal principal, List excludedProm
}
oauthIdentityProviders = Collections.emptyMap();
samlIdentityProviders = Collections.emptyMap();
+ } else if (accountChooserNeeded || (discoveryEnabled && !discoveryPerformed)) {
+ //Account Chooser and discovery do not need any IdP information
+ oauthIdentityProviders = Collections.emptyMap();
+ samlIdentityProviders = Collections.emptyMap();
} else {
samlIdentityProviders = getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys);
oauthIdentityProviders = getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys);
@@ -331,10 +346,6 @@ private String login(Model model, Principal principal, List excludedProm
idpForRedirect = evaluateLoginHint(model, samlIdentityProviders,
oauthIdentityProviders, allIdentityProviders, allowedIdentityProviderKeys, loginHintParam, uaaLoginHint, loginHintProviders);
- boolean discoveryEnabled = IdentityZoneHolder.get().getConfig().isIdpDiscoveryEnabled();
- boolean discoveryPerformed = Boolean.parseBoolean(request.getParameter("discoveryPerformed"));
- String defaultIdentityProviderName = IdentityZoneHolder.get().getConfig().getDefaultIdentityProvider();
-
idpForRedirect = evaluateIdpDiscovery(model, samlIdentityProviders, oauthIdentityProviders,
allIdentityProviders, allowedIdentityProviderKeys, idpForRedirect, discoveryEnabled, discoveryPerformed, defaultIdentityProviderName);
if (idpForRedirect == null && !jsonResponse && !fieldUsernameShow && allIdentityProviders.size() == 1) {
@@ -381,7 +392,7 @@ private String login(Model model, Principal principal, List excludedProm
excludedPrompts, returnLoginPrompts);
if (principal == null) {
- return getUnauthenticatedRedirect(model, request, discoveryEnabled, discoveryPerformed);
+ return getUnauthenticatedRedirect(model, request, discoveryEnabled, discoveryPerformed, accountChooserNeeded);
}
return "home";
}
@@ -390,25 +401,18 @@ private String getUnauthenticatedRedirect(
Model model,
HttpServletRequest request,
boolean discoveryEnabled,
- boolean discoveryPerformed
+ boolean discoveryPerformed,
+ boolean accountChooserNeeded
) {
String formRedirectUri = request.getParameter(UaaSavedRequestAwareAuthenticationSuccessHandler.FORM_REDIRECT_PARAMETER);
if (hasText(formRedirectUri)) {
model.addAttribute(UaaSavedRequestAwareAuthenticationSuccessHandler.FORM_REDIRECT_PARAMETER, formRedirectUri);
}
- boolean accountChooserEnabled = IdentityZoneHolder.get().getConfig().isAccountChooserEnabled();
- boolean otherAccountSignIn = Boolean.parseBoolean(request.getParameter("otherAccountSignIn"));
- boolean savedAccountsEmpty = getSavedAccounts(request.getCookies(), SavedAccountOption.class).isEmpty();
-
if (discoveryEnabled) {
if (model.containsAttribute("login_hint")) {
return goToPasswordPage(request.getParameter("email"), model);
}
- boolean accountChooserNeeded = accountChooserEnabled
- && !(otherAccountSignIn || savedAccountsEmpty)
- && !discoveryPerformed;
-
if (accountChooserNeeded) {
return "idp_discovery/account_chooser";
}
diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java
index 9f6f0110237..d498b30de69 100755
--- a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java
+++ b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java
@@ -818,13 +818,6 @@ void authcodeWithAllowedProviderStillUsesAccountChooser() throws Exception {
MultitenantClientServices clientDetailsService = mock(MultitenantClientServices.class);
when(clientDetailsService.loadClientByClientId("client-id", "other-zone")).thenReturn(clientDetails);
- // mock SamlIdentityProviderConfigurator
- List clientIDPs = new LinkedList<>();
- clientIDPs.add(createIdentityProviderDefinition("my-client-awesome-idp1", "other-zone"));
- clientIDPs.add(createIdentityProviderDefinition("uaa", "other-zone"));
- when(mockSamlIdentityProviderConfigurator.getIdentityProviderDefinitions(eq(allowedProviders), eq(zone))).thenReturn(clientIDPs);
-
-
LoginInfoEndpoint endpoint = getEndpoint(IdentityZoneHolder.get(), clientDetailsService);
endpoint.loginForHtml(extendedModelMap, null, request, singletonList(MediaType.TEXT_HTML));
From 7e7c376cd4bc5180eb78b43bcc33b21b069bb8aa Mon Sep 17 00:00:00 2001
From: Florian Tack
Date: Fri, 29 Jan 2021 13:18:49 +0100
Subject: [PATCH 014/140] Fix bug with invalid login_hint on login page
---
.../identity/uaa/login/LoginInfoEndpoint.java | 12 ++++++++--
.../uaa/login/LoginInfoEndpointTests.java | 24 +++++++++++++++++++
2 files changed, 34 insertions(+), 2 deletions(-)
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java
index d9b3c8a276e..9776b363adf 100755
--- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java
@@ -303,8 +303,16 @@ private String login(Model model, Principal principal, List excludedProm
} catch (EmptyResultDataAccessException ignored) {
}
}
- oauthIdentityProviders = Collections.emptyMap();
- samlIdentityProviders = Collections.emptyMap();
+ if (!loginHintProviders.isEmpty()) {
+ oauthIdentityProviders = Collections.emptyMap();
+ samlIdentityProviders = Collections.emptyMap();
+ } else {
+ samlIdentityProviders = getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys);
+ oauthIdentityProviders = getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys);
+ allIdentityProviders = new HashMap<>();
+ allIdentityProviders.putAll(samlIdentityProviders);
+ allIdentityProviders.putAll(oauthIdentityProviders);
+ }
} else if (accountChooserNeeded || (discoveryEnabled && !discoveryPerformed)) {
//Account Chooser and discovery do not need any IdP information
oauthIdentityProviders = Collections.emptyMap();
diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java
index d498b30de69..e410ed4aa9e 100755
--- a/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java
+++ b/server/src/test/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpointTests.java
@@ -1151,6 +1151,30 @@ void invalidLoginHintErrorOnDiscoveryPage() throws Exception {
assertEquals("idp_discovery/email", redirect);
}
+ @Test
+ public void testInvalidLoginHintLoginPageReturnsList() throws Exception {
+ MockHttpServletRequest mockHttpServletRequest = getMockHttpServletRequest();
+
+ BaseClientDetails clientDetails = new BaseClientDetails();
+ clientDetails.setClientId("client-id");
+ MultitenantClientServices clientDetailsService = mock(MultitenantClientServices.class);
+ when(clientDetailsService.loadClientByClientId("client-id", "uaa")).thenReturn(clientDetails);
+ LoginInfoEndpoint endpoint = getEndpoint(IdentityZoneHolder.get(), clientDetailsService);
+
+ List clientAllowedIdps = new LinkedList<>();
+ clientAllowedIdps.add(createOIDCIdentityProvider("my-OIDC-idp1"));
+ clientAllowedIdps.add(createOIDCIdentityProvider("my-OIDC-idp2"));
+ when(mockIdentityProviderProvisioning.retrieveAll(eq(true), anyString())).thenReturn(clientAllowedIdps);
+ when(mockIdentityProviderProvisioning.retrieveByOrigin(eq("invalidorigin"), anyString())).thenThrow(new EmptyResultDataAccessException(1));
+
+ SavedRequest savedRequest = SessionUtils.getSavedRequestSession(mockHttpServletRequest.getSession());
+ when(savedRequest.getParameterValues("login_hint")).thenReturn(new String[]{"{\"origin\":\"invalidorigin\"}"});
+
+ endpoint.loginForHtml(extendedModelMap, null, mockHttpServletRequest, Collections.singletonList(MediaType.TEXT_HTML));
+
+ assertFalse(((Map)extendedModelMap.get("oauthLinks")).isEmpty());
+ }
+
@Test
void loginHintOriginOidc() throws Exception {
MockHttpServletRequest mockHttpServletRequest = getMockHttpServletRequest();
From c32c34908d7e885202ef6ea86a143732c16cb121 Mon Sep 17 00:00:00 2001
From: Florian Tack
Date: Fri, 29 Jan 2021 13:19:10 +0100
Subject: [PATCH 015/140] Add MockMvc Test to show performance improvement
---
.../LoginPagePerformanceMockMvcTest.java | 166 ++++++++++++++++++
1 file changed, 166 insertions(+)
create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/performance/LoginPagePerformanceMockMvcTest.java
diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/performance/LoginPagePerformanceMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/performance/LoginPagePerformanceMockMvcTest.java
new file mode 100644
index 00000000000..3ad5ab09761
--- /dev/null
+++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/performance/LoginPagePerformanceMockMvcTest.java
@@ -0,0 +1,166 @@
+package org.cloudfoundry.identity.uaa.performance;
+
+import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf;
+import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.createOtherIdentityZoneAndReturnResult;
+import static org.junit.Assert.assertFalse;
+import static org.springframework.http.MediaType.TEXT_HTML;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.io.File;
+import java.net.URL;
+import java.util.Collections;
+
+import org.cloudfoundry.identity.uaa.DefaultTestContext;
+import org.cloudfoundry.identity.uaa.codestore.JdbcExpiringCodeStore;
+import org.cloudfoundry.identity.uaa.constants.OriginKeys;
+import org.cloudfoundry.identity.uaa.impl.config.IdentityZoneConfigurationBootstrap;
+import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils;
+import org.cloudfoundry.identity.uaa.provider.AbstractExternalOAuthIdentityProviderDefinition;
+import org.cloudfoundry.identity.uaa.provider.IdentityProvider;
+import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning;
+import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition;
+import org.cloudfoundry.identity.uaa.provider.oauth.OidcMetadataFetcher;
+import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor;
+import org.cloudfoundry.identity.uaa.web.LimitedModeUaaFilter;
+import org.cloudfoundry.identity.uaa.zone.IdentityZone;
+import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
+import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
+import org.springframework.security.oauth2.provider.client.BaseClientDetails;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.util.StopWatch;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.WebApplicationContext;
+
+@DefaultTestContext
+@DirtiesContext
+public class LoginPagePerformanceMockMvcTest {
+
+ private WebApplicationContext webApplicationContext;
+
+ private RandomValueStringGenerator generator;
+
+ private MockMvc mockMvc;
+
+
+
+ private File originalLimitedModeStatusFile;
+
+ @MockBean
+ OidcMetadataFetcher oidcMetadataFetcher;
+
+ @BeforeEach
+ void setUpContext(
+ @Autowired WebApplicationContext webApplicationContext,
+ @Autowired MockMvc mockMvc,
+ @Autowired LimitedModeUaaFilter limitedModeUaaFilter
+ ) {
+ generator = new RandomValueStringGenerator();
+ this.webApplicationContext = webApplicationContext;
+ this.mockMvc = mockMvc;
+ SecurityContextHolder.clearContext();
+
+ originalLimitedModeStatusFile = MockMvcUtils.getLimitedModeStatusFile(webApplicationContext);
+ MockMvcUtils.resetLimitedModeStatusFile(webApplicationContext, null);
+ assertFalse(limitedModeUaaFilter.isEnabled());
+ }
+
+ @AfterEach
+ void resetGenerator(
+ @Autowired JdbcExpiringCodeStore jdbcExpiringCodeStore
+ ) {
+ jdbcExpiringCodeStore.setGenerator(new RandomValueStringGenerator(24));
+ }
+
+ @AfterEach
+ void tearDown(@Autowired IdentityZoneConfigurationBootstrap identityZoneConfigurationBootstrap) throws Exception {
+ MockMvcUtils.setSelfServiceLinksEnabled(webApplicationContext, IdentityZone.getUaaZoneId(), true);
+ identityZoneConfigurationBootstrap.afterPropertiesSet();
+ SecurityContextHolder.clearContext();
+ IdentityZoneHolder.clear();
+ MockMvcUtils.resetLimitedModeStatusFile(webApplicationContext, originalLimitedModeStatusFile);
+ }
+
+ @Test
+ void idpDiscoveryRedirectsToOIDCProvider(
+ @Autowired JdbcIdentityProviderProvisioning jdbcIdentityProviderProvisioning
+ ) throws Exception {
+ String subdomain = "oidc-discovery-" + generator.generate().toLowerCase();
+ IdentityZone zone = MultitenancyFixture.identityZone(subdomain, subdomain);
+ zone.getConfig().setIdpDiscoveryEnabled(true);
+ BaseClientDetails client = new BaseClientDetails("admin", null, null, "client_credentials",
+ "clients.admin,scim.read,scim.write,idps.write,uaa.admin", "http://redirect.url");
+ client.setClientSecret("admin-secret");
+ createOtherIdentityZoneAndReturnResult(mockMvc, webApplicationContext, client, zone, false, IdentityZoneHolder.getCurrentZoneId());
+
+
+ createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null);
+ createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null);
+ createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null);
+ createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null);
+ createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null);
+ createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null);
+ createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null);
+ createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null);
+ createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null);
+ createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null);
+ createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", null);
+ String originKey = createOIDCProvider(jdbcIdentityProviderProvisioning, generator, zone, "id_token code", "test.org");
+
+ StopWatch stopWatch = new StopWatch();
+ stopWatch.start();
+ for (int i = 0; i <1000; i++) {
+ MvcResult mvcResult = mockMvc.perform(get("/login")
+ .with(cookieCsrf())
+ .header("Accept", TEXT_HTML)
+ .with(new SetServerNameRequestPostProcessor(zone.getSubdomain() + ".localhost")))
+ .andExpect(status().isOk())
+ .andReturn();
+ MockHttpServletResponse response = mvcResult.getResponse();
+ }
+
+ stopWatch.stop();
+ long totalTimeMillis = stopWatch.getTotalTimeMillis();
+
+ System.out.println(totalTimeMillis + "ms");
+ }
+
+
+ private static String createOIDCProvider(JdbcIdentityProviderProvisioning jdbcIdentityProviderProvisioning, RandomValueStringGenerator generator, IdentityZone zone, String responseType, String domain) throws Exception {
+ String originKey = generator.generate();
+ AbstractExternalOAuthIdentityProviderDefinition definition = new OIDCIdentityProviderDefinition();
+ definition.setAuthUrl(new URL("http://myauthurl.com"));
+ definition.setTokenKey("key");
+ definition.setTokenUrl(new URL("http://mytokenurl.com"));
+ definition.setRelyingPartyId("id");
+ definition.setRelyingPartySecret("secret");
+ definition.setLinkText("my oidc provider");
+ if (StringUtils.hasText(responseType)) {
+ definition.setResponseType(responseType);
+ }
+ if (StringUtils.hasText(domain)) {
+ definition.setEmailDomain(Collections.singletonList(domain));
+ }
+
+ IdentityProvider identityProvider = MultitenancyFixture.identityProvider(originKey, zone.getId());
+ identityProvider.setType(OriginKeys.OIDC10);
+ identityProvider.setConfig(definition);
+ createIdentityProvider(jdbcIdentityProviderProvisioning, zone, identityProvider);
+ return originKey;
+ }
+
+ private static IdentityProvider createIdentityProvider(JdbcIdentityProviderProvisioning jdbcIdentityProviderProvisioning, IdentityZone identityZone, IdentityProvider activeIdentityProvider) {
+ activeIdentityProvider.setIdentityZoneId(identityZone.getId());
+ return jdbcIdentityProviderProvisioning.create(activeIdentityProvider, identityZone.getId());
+ }
+}
From e21ece5dc99c129c7616ab0322c4c803c1b18764 Mon Sep 17 00:00:00 2001
From: Cloud Foundry Identity Team
Date: Wed, 3 Feb 2021 00:53:19 +0000
Subject: [PATCH 016/140] Update UAA image reference in k8s deployment template
to
cloudfoundry/uaa@sha256:c1d54700f1e6b8fabe49917a8e4ca59f381a11c69982439525f9af8a08f29e0c
---
k8s/templates/values/image.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/k8s/templates/values/image.yml b/k8s/templates/values/image.yml
index 54e35b92c5b..0ac73de2027 100644
--- a/k8s/templates/values/image.yml
+++ b/k8s/templates/values/image.yml
@@ -1,3 +1,3 @@
#@data/values
---
-image: "cloudfoundry/uaa@sha256:37fcf63a156b75174fbc7be0e3809d64cda6851de0bfe6fa7f12793d919de96a"
+image: "cloudfoundry/uaa@sha256:c1d54700f1e6b8fabe49917a8e4ca59f381a11c69982439525f9af8a08f29e0c"
From ecf3734d0b8533f1571220c450979f5595d92edf Mon Sep 17 00:00:00 2001
From: Bruce Ricard
Date: Thu, 4 Feb 2021 11:51:58 -0800
Subject: [PATCH 017/140] Bump dependency
* the previous version of mime has a vulnerability
https://nvd.nist.gov/vuln/detail/CVE-2017-16138
---
uaa/slate/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/uaa/slate/package.json b/uaa/slate/package.json
index db4427c6ca5..b11f2920864 100644
--- a/uaa/slate/package.json
+++ b/uaa/slate/package.json
@@ -116,7 +116,7 @@
"map-obj": "^1.0.1",
"media-typer": "^0.3.0",
"meow": "^3.7.0",
- "mime": "^1.3.4",
+ "mime": "^2.5.0",
"mime-db": "^1.22.0",
"mime-types": "^2.1.10",
"minimatch": "^3.0.0",
From 9834fc3405b31a0a887d22e48e4162d57364217c Mon Sep 17 00:00:00 2001
From: Cloud Foundry Identity Team
Date: Sun, 14 Feb 2021 06:41:15 +0000
Subject: [PATCH 018/140] Update UAA image reference in k8s deployment template
to
cloudfoundry/uaa@sha256:7fd48f08134e279a4fe2b8a200ecfdca8cda847175df38f1293e9818d7dd53cc
---
k8s/templates/values/image.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/k8s/templates/values/image.yml b/k8s/templates/values/image.yml
index 0ac73de2027..0a6418f398e 100644
--- a/k8s/templates/values/image.yml
+++ b/k8s/templates/values/image.yml
@@ -1,3 +1,3 @@
#@data/values
---
-image: "cloudfoundry/uaa@sha256:c1d54700f1e6b8fabe49917a8e4ca59f381a11c69982439525f9af8a08f29e0c"
+image: "cloudfoundry/uaa@sha256:7fd48f08134e279a4fe2b8a200ecfdca8cda847175df38f1293e9818d7dd53cc"
From fdd4e72ee103ba12f9baef19058eb0acc9edaa53 Mon Sep 17 00:00:00 2001
From: Peter Chen
Date: Thu, 25 Feb 2021 11:55:01 -0800
Subject: [PATCH 019/140] feat: for unit tests, add time out to waiting for DB
to start
- and print out the DB boot log if fail to start
[#177100664]
---
scripts/start_db_helper.sh | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/scripts/start_db_helper.sh b/scripts/start_db_helper.sh
index b7dcb37889a..036e9c9f3d8 100755
--- a/scripts/start_db_helper.sh
+++ b/scripts/start_db_helper.sh
@@ -16,7 +16,8 @@ function bootDB {
db=$1
if [[ "${db}" = "postgresql" ]]; then
- launchDB="(/docker-entrypoint.sh postgres -c 'max_connections=250' &> /var/log/postgres-boot.log) &"
+ bootLogLocation="/var/log/postgres-boot.log"
+ launchDB="(/docker-entrypoint.sh postgres -c 'max_connections=250' &> ${bootLogLocation}) &"
testConnection="(! ps aux | grep docker-entrypoint | grep -v 'grep') && psql -h localhost -U postgres -c '\conninfo' &>/dev/null"
initDB="psql -c 'drop database if exists uaa;' -U postgres; psql -c 'create database uaa;' -U postgres; psql -c 'drop user if exists root;' --dbname=uaa -U postgres; psql -c \"create user root with superuser password 'changeme';\" --dbname=uaa -U postgres; psql -c 'show max_connections;' --dbname=uaa -U postgres;"
@@ -27,7 +28,8 @@ function bootDB {
elif [[ "${db}" = "mysql" ]] || [[ "${db}" = "mysql-5.6" ]]; then
- launchDB="(MYSQL_DATABASE=uaa MYSQL_ROOT_HOST=127.0.0.1 MYSQL_ROOT_PASSWORD='changeme' bash /entrypoint.sh mysqld &> /var/log/mysql-boot.log) &"
+ bootLogLocation="/var/log/mysql-boot.log"
+ launchDB="(MYSQL_DATABASE=uaa MYSQL_ROOT_HOST=127.0.0.1 MYSQL_ROOT_PASSWORD='changeme' bash /entrypoint.sh mysqld &> ${bootLogLocation}) &"
testConnection="echo '\s;' | mysql -uroot -pchangeme &>/dev/null"
initDB="mysql -uroot -pchangeme -e 'SET GLOBAL max_connections = 250; ALTER DATABASE uaa DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;';"
@@ -37,7 +39,8 @@ function bootDB {
}
elif [[ "${db}" = "percona" ]]; then
- launchDB="bash /entrypoint.sh &> /var/log/mysql-boot.log"
+ bootLogLocation="/var/log/mysql-boot.log"
+ launchDB="bash /entrypoint.sh &> ${bootLogLocation}"
testConnection="echo '\s;' | mysql &>/dev/null"
initDB="mysql -e \"CREATE USER 'root'@'127.0.0.1' IDENTIFIED BY 'changeme' ;\";
mysql -e \"GRANT ALL ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION ;\";
@@ -60,7 +63,9 @@ function bootDB {
echo -n "Booting $db"
set -x
eval "$launchDB"
- while true; do
+
+ for i in {0..600} # wait at most 10 mins to the database to start
+ do
set +ex
eval "$testConnection"
exitcode=$?
@@ -80,4 +85,8 @@ function bootDB {
echo -n "."
sleep 1
done
+
+ echo "Printing database boot logs:"
+ cat "$bootLogLocation"
+ exit 1
}
From 766e981735b144aa33b674b7f3fab9d798a8f3d0 Mon Sep 17 00:00:00 2001
From: Olivier Lechevalier
Date: Mon, 26 Oct 2020 17:07:48 +0900
Subject: [PATCH 020/140] Fix typo
---
.../manager/DynamicZoneAwareAuthenticationManagerTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java
index fc0ffc45b32..1eb215e7691 100644
--- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java
+++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java
@@ -142,7 +142,7 @@ void testNonUAAZoneUaaActiveAccountLocked() {
}
@Test
- void testNonUAAZoneUaaActiveUaaAuthenticationSucccess() {
+ void testNonUAAZoneUaaActiveUaaAuthenticationSuccess() {
IdentityZoneHolder.set(ZONE);
when(providerProvisioning.retrieveByOrigin(OriginKeys.UAA, ZONE.getId())).thenReturn(uaaActive);
when(providerProvisioning.retrieveByOrigin(OriginKeys.LDAP, ZONE.getId())).thenReturn(ldapActive);
From c88dfff40118d395b71607b4d3a2e265168625e4 Mon Sep 17 00:00:00 2001
From: Markus Strehle
Date: Fri, 19 Mar 2021 09:28:58 +0100
Subject: [PATCH 021/140] refactor: implement recommendation from thymeleaf
(#1512)
by change I found warnings in log. in total 3 warnings found
[THYMELEAF][Test worker] Template Mode 'HTML5' is deprecated. Using Template Mode 'HTML' instead.
[THYMELEAF][Test worker] Template Mode 'HTML5' is deprecated. Using Template Mode 'HTML' instead.
Initializing Spring TestDispatcherServlet ''
Initializing Servlet ''
Completed initialization in 3 ms
The layout:decorator/data-layout-decorator processor has been deprecated and will be removed in the next major version of the layout dialect. Please use layout:decorate/data-layout-decorate instead to future-proof your code. See https://github.com/ultraq/thymeleaf-layout-dialect/issues/95 for more information.
Fragment expression "layouts/main" is being wrapped as a Thymeleaf 3 fragment expression (~{...}) for backwards compatibility purposes. This wrapping will be dropped in the next major version of the expression processor, so please rewrite as a Thymeleaf 3 fragment expression to future-proof your code. See https://github.com/thymeleaf/thymeleaf/issues/451 for more information.
## https://github.com/ultraq/thymeleaf-layout-dialect/issues/95 , changed layout:decorator to layout:decorate
## use template mode HTML instead of HTML5
## swtiched back to from th:with="isLdap which was removed with spring update
---
.../identity/uaa/login/ThymeleafConfig.java | 3 ++-
.../resources/templates/web/access_confirmation.html | 2 +-
.../templates/web/access_confirmation_error.html | 2 +-
.../resources/templates/web/accounts/email_sent.html | 2 +-
.../resources/templates/web/accounts/link_prompt.html | 2 +-
.../templates/web/accounts/new_activation_email.html | 2 +-
server/src/main/resources/templates/web/approvals.html | 2 +-
.../src/main/resources/templates/web/change_email.html | 2 +-
.../main/resources/templates/web/change_password.html | 2 +-
.../src/main/resources/templates/web/email_sent.html | 2 +-
server/src/main/resources/templates/web/error.html | 2 +-
.../resources/templates/web/external_auth_error.html | 2 +-
.../resources/templates/web/force_password_change.html | 2 +-
.../main/resources/templates/web/forgot_password.html | 2 +-
server/src/main/resources/templates/web/home.html | 2 +-
.../templates/web/idp_discovery/account_chooser.html | 2 +-
.../resources/templates/web/idp_discovery/email.html | 2 +-
.../templates/web/idp_discovery/password.html | 2 +-
.../main/resources/templates/web/invalid_request.html | 2 +-
.../templates/web/invitations/accept_invite.html | 10 +++++-----
server/src/main/resources/templates/web/login.html | 2 +-
.../main/resources/templates/web/login_implicit.html | 2 +-
.../main/resources/templates/web/mfa/enter_code.html | 2 +-
.../templates/web/mfa/manual_registration.html | 2 +-
.../src/main/resources/templates/web/mfa/qr_code.html | 2 +-
server/src/main/resources/templates/web/passcode.html | 2 +-
.../main/resources/templates/web/reset_password.html | 2 +-
.../src/main/resources/templates/web/switch_idp.html | 2 +-
28 files changed, 33 insertions(+), 32 deletions(-)
diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java
index 673230e6472..7efd81c0e43 100644
--- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java
+++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java
@@ -28,6 +28,7 @@
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
+import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;
import java.nio.charset.StandardCharsets;
@@ -104,7 +105,7 @@ public org.springframework.web.servlet.view.ContentNegotiatingViewResolver viewR
private SpringResourceTemplateResolver baseHtmlTemplateResolver(ApplicationContext context) {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setSuffix(".html");
- templateResolver.setTemplateMode("HTML5");
+ templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setApplicationContext(context);
return templateResolver;
}
diff --git a/server/src/main/resources/templates/web/access_confirmation.html b/server/src/main/resources/templates/web/access_confirmation.html
index 99e230ec0ba..a66b703a9e3 100644
--- a/server/src/main/resources/templates/web/access_confirmation.html
+++ b/server/src/main/resources/templates/web/access_confirmation.html
@@ -2,7 +2,7 @@
+ layout:decorate="~{layouts/main}">
diff --git a/server/src/main/resources/templates/web/access_confirmation_error.html b/server/src/main/resources/templates/web/access_confirmation_error.html
index 7d454fd650c..2fbe2f85337 100644
--- a/server/src/main/resources/templates/web/access_confirmation_error.html
+++ b/server/src/main/resources/templates/web/access_confirmation_error.html
@@ -1,5 +1,5 @@
-
+
diff --git a/server/src/main/resources/templates/web/accounts/email_sent.html b/server/src/main/resources/templates/web/accounts/email_sent.html
index 5b0d5084c10..389d37b61b0 100644
--- a/server/src/main/resources/templates/web/accounts/email_sent.html
+++ b/server/src/main/resources/templates/web/accounts/email_sent.html
@@ -1,7 +1,7 @@
+ layout:decorate="~{layouts/main}">
diff --git a/server/src/main/resources/templates/web/accounts/link_prompt.html b/server/src/main/resources/templates/web/accounts/link_prompt.html
index 145b18d69fa..27d34953a52 100644
--- a/server/src/main/resources/templates/web/accounts/link_prompt.html
+++ b/server/src/main/resources/templates/web/accounts/link_prompt.html
@@ -1,7 +1,7 @@
+ layout:decorate="~{layouts/main}">
Create your account
diff --git a/server/src/main/resources/templates/web/accounts/new_activation_email.html b/server/src/main/resources/templates/web/accounts/new_activation_email.html
index a77bc756f2b..7150431ff98 100644
--- a/server/src/main/resources/templates/web/accounts/new_activation_email.html
+++ b/server/src/main/resources/templates/web/accounts/new_activation_email.html
@@ -1,7 +1,7 @@
+ layout:decorate="~{layouts/main}">
Create your account
diff --git a/server/src/main/resources/templates/web/approvals.html b/server/src/main/resources/templates/web/approvals.html
index a72c4348b7d..6d392a12450 100644
--- a/server/src/main/resources/templates/web/approvals.html
+++ b/server/src/main/resources/templates/web/approvals.html
@@ -2,7 +2,7 @@
+ layout:decorate="~{layouts/main}">