Skip to content

Commit d884913

Browse files
committed
Sync branch '7.0.x'
2 parents d29c761 + 0fa81bf commit d884913

35 files changed

Lines changed: 1089 additions & 430 deletions

File tree

oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,8 @@ private static void throwError(OAuth2Error error, String parameterName,
489489
registeredClient);
490490
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST)
491491
&& (parameterName.equals(OAuth2ParameterNames.CLIENT_ID)
492-
|| parameterName.equals(OAuth2ParameterNames.STATE))) {
492+
|| parameterName.equals(OAuth2ParameterNames.STATE)
493+
|| parameterName.equals(OAuth2ParameterNames.REQUEST_URI))) {
493494
redirectUri = null; // Prevent redirects
494495
}
495496

oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,10 @@ private static void throwError(OAuth2Error error, String parameterName,
293293
String redirectUri = StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())
294294
? authorizationCodeRequestAuthentication.getRedirectUri()
295295
: registeredClient.getRedirectUris().iterator().next();
296-
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST)
297-
&& parameterName.equals(OAuth2ParameterNames.REDIRECT_URI)) {
296+
if ((error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST)
297+
|| error.getErrorCode().equals(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT))
298+
&& (parameterName.equals(OAuth2ParameterNames.CLIENT_ID)
299+
|| parameterName.equals(OAuth2ParameterNames.REDIRECT_URI))) {
298300
redirectUri = null; // Prevent redirects
299301
}
300302

oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -123,46 +123,60 @@ else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue()))
123123
principal = ANONYMOUS_AUTHENTICATION;
124124
}
125125

126-
// redirect_uri (OPTIONAL)
127-
String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
128-
if (StringUtils.hasText(redirectUri) && parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {
129-
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
126+
String redirectUri = null;
127+
if (!StringUtils.hasText(requestUri)) {
128+
// redirect_uri (OPTIONAL)
129+
redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
130+
if (StringUtils.hasText(redirectUri) && parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {
131+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
132+
}
130133
}
131134

132-
// scope (OPTIONAL)
133135
Set<String> scopes = null;
134-
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
135-
if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
136-
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
137-
}
138-
if (StringUtils.hasText(scope)) {
139-
scopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
136+
if (!StringUtils.hasText(requestUri)) {
137+
// scope (OPTIONAL)
138+
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
139+
if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
140+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
141+
}
142+
if (StringUtils.hasText(scope)) {
143+
scopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
144+
}
140145
}
141146

142-
// state (RECOMMENDED)
143-
String state = parameters.getFirst(OAuth2ParameterNames.STATE);
144-
if (StringUtils.hasText(state) && parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
145-
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
147+
String state = null;
148+
if (!StringUtils.hasText(requestUri)) {
149+
// state (RECOMMENDED)
150+
state = parameters.getFirst(OAuth2ParameterNames.STATE);
151+
if (StringUtils.hasText(state) && parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
152+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
153+
}
146154
}
147155

148-
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
149-
String codeChallenge = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE);
150-
if (StringUtils.hasText(codeChallenge) && parameters.get(PkceParameterNames.CODE_CHALLENGE).size() != 1) {
151-
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI);
156+
if (!StringUtils.hasText(requestUri)) {
157+
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
158+
String codeChallenge = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE);
159+
if (StringUtils.hasText(codeChallenge) && parameters.get(PkceParameterNames.CODE_CHALLENGE).size() != 1) {
160+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI);
161+
}
152162
}
153163

154-
// code_challenge_method (OPTIONAL for public clients) - RFC 7636 (PKCE)
155-
String codeChallengeMethod = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD);
156-
if (StringUtils.hasText(codeChallengeMethod)
157-
&& parameters.get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1) {
158-
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI);
164+
if (!StringUtils.hasText(requestUri)) {
165+
// code_challenge_method (OPTIONAL for public clients) - RFC 7636 (PKCE)
166+
String codeChallengeMethod = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD);
167+
if (StringUtils.hasText(codeChallengeMethod)
168+
&& parameters.get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1) {
169+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI);
170+
}
159171
}
160172

161-
// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
162-
if (!CollectionUtils.isEmpty(scopes) && scopes.contains(OidcScopes.OPENID)) {
163-
String prompt = parameters.getFirst("prompt");
164-
if (StringUtils.hasText(prompt) && parameters.get("prompt").size() != 1) {
165-
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "prompt");
173+
if (!StringUtils.hasText(requestUri)) {
174+
// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
175+
if (!CollectionUtils.isEmpty(scopes) && scopes.contains(OidcScopes.OPENID)) {
176+
String prompt = parameters.getFirst("prompt");
177+
if (StringUtils.hasText(prompt) && parameters.get("prompt").size() != 1) {
178+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "prompt");
179+
}
166180
}
167181
}
168182

oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ public void authenticateWhenClientNotAuthorizedToRequestCodeThenThrowOAuth2Autho
311311
assertThatExceptionOfType(OAuth2AuthorizationCodeRequestAuthenticationException.class)
312312
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
313313
.satisfies((ex) -> assertAuthenticationException(ex, OAuth2ErrorCodes.UNAUTHORIZED_CLIENT,
314-
OAuth2ParameterNames.CLIENT_ID, authentication.getRedirectUri()));
314+
OAuth2ParameterNames.CLIENT_ID, null));
315315
}
316316

317317
@Test

oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationProviderTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public void authenticateWhenClientNotAuthorizedToRequestCodeThenThrowOAuth2Autho
128128
assertThatExceptionOfType(OAuth2AuthorizationCodeRequestAuthenticationException.class)
129129
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
130130
.satisfies((ex) -> assertAuthenticationException(ex, OAuth2ErrorCodes.UNAUTHORIZED_CLIENT,
131-
OAuth2ParameterNames.CLIENT_ID, authentication.getRedirectUri()));
131+
OAuth2ParameterNames.CLIENT_ID, null));
132132
}
133133

134134
@Test

saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ tasks.register("opensaml5Test", Test) {
137137
classpath = sourceSets.opensaml5Test.output + sourceSets.opensaml5Test.runtimeClasspath
138138
}
139139

140+
tasks.register("bouncyCastleTest", Test) {
141+
useJUnitPlatform()
142+
testClassesDirs = sourceSets.test.output.classesDirs
143+
classpath = sourceSets.test.runtimeClasspath
144+
include "**/JdbcAssertingPartyMetadataRepositoryBouncyCastleTests.class"
145+
}
146+
140147
tasks.named("test") {
141-
dependsOn opensaml5Test
148+
exclude "**/JdbcAssertingPartyMetadataRepositoryBouncyCastleTests.class"
149+
dependsOn opensaml5Test, bouncyCastleTest
142150
}

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/internal/Saml2Utils.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.ByteArrayOutputStream;
2020
import java.io.IOException;
21+
import java.io.OutputStream;
2122
import java.nio.charset.StandardCharsets;
2223
import java.util.Arrays;
2324
import java.util.Base64;
@@ -64,7 +65,7 @@ static byte[] samlDeflate(String s) {
6465
static String samlInflate(byte[] b) {
6566
try {
6667
ByteArrayOutputStream out = new ByteArrayOutputStream();
67-
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
68+
InflaterOutputStream iout = new InflaterOutputStream(new CappedOutputStream(out), new Inflater(true));
6869
iout.write(b);
6970
iout.finish();
7071
return new String(out.toByteArray(), StandardCharsets.UTF_8);
@@ -193,4 +194,27 @@ void checkAcceptable(String ins) {
193194

194195
}
195196

197+
static class CappedOutputStream extends OutputStream {
198+
199+
private static final long MAX_SIZE = 1024 * 1024;
200+
201+
private final OutputStream delegate;
202+
203+
private int size;
204+
205+
CappedOutputStream(OutputStream delegate) {
206+
this.delegate = delegate;
207+
}
208+
209+
@Override
210+
public void write(int b) throws IOException {
211+
if (this.size >= MAX_SIZE) {
212+
throw new IOException("SAML payload exceeded maximum size of " + MAX_SIZE);
213+
}
214+
this.delegate.write(b);
215+
this.size++;
216+
}
217+
218+
}
219+
196220
}

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/BaseOpenSamlAuthenticationProvider.java

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -319,62 +319,75 @@ private void process(Saml2AuthenticationToken token, Response response) {
319319
boolean responseSigned = response.isSigned();
320320

321321
ResponseToken responseToken = new ResponseToken(response, token);
322-
Saml2ResponseValidatorResult result = this.responseSignatureValidator.convert(responseToken);
322+
Collection<Saml2Error> responseSignatureErrors = this.responseSignatureValidator.convert(responseToken)
323+
.getErrors();
324+
if (!responseSignatureErrors.isEmpty()) {
325+
reportErrors(response, responseSignatureErrors);
326+
return;
327+
}
328+
329+
Collection<Saml2Error> errors = new ArrayList<>();
323330
if (responseSigned) {
324331
this.responseElementsDecrypter.accept(responseToken);
325332
}
326333
else if (!response.getEncryptedAssertions().isEmpty()) {
327-
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
334+
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE,
328335
"Did not decrypt response [" + response.getID() + "] since it is not signed"));
329336
}
330337
if (!this.validateResponseAfterAssertions) {
331-
result = result.concat(this.responseValidator.convert(responseToken));
338+
errors.addAll(this.responseValidator.convert(responseToken).getErrors());
332339
}
333340
boolean allAssertionsSigned = true;
334341
for (Assertion assertion : response.getAssertions()) {
335342
AssertionToken assertionToken = new AssertionToken(assertion, token);
336-
result = result.concat(this.assertionSignatureValidator.convert(assertionToken));
343+
Collection<Saml2Error> assertionSignatureErrors = this.assertionSignatureValidator.convert(assertionToken)
344+
.getErrors();
345+
errors.addAll(assertionSignatureErrors);
337346
allAssertionsSigned = allAssertionsSigned && assertion.isSigned();
347+
if (!assertionSignatureErrors.isEmpty()) {
348+
continue;
349+
}
338350
if (responseSigned || assertion.isSigned()) {
339351
this.assertionElementsDecrypter.accept(new AssertionToken(assertion, token));
340352
}
341-
result = result.concat(this.assertionValidator.convert(assertionToken));
353+
errors.addAll(this.assertionValidator.convert(assertionToken).getErrors());
342354
}
343355
if (!responseSigned && !allAssertionsSigned) {
344356
String description = "Either the response or one of the assertions is unsigned. "
345357
+ "Please either sign the response or all of the assertions.";
346-
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, description));
358+
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, description));
347359
}
348360
if (this.validateResponseAfterAssertions) {
349-
result = result.concat(this.responseValidator.convert(responseToken));
361+
errors.addAll(this.responseValidator.convert(responseToken).getErrors());
350362
}
351363
else {
352364
Assertion firstAssertion = CollectionUtils.firstElement(response.getAssertions());
353365
if (firstAssertion != null && !hasName(firstAssertion)) {
354-
Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
355-
"Assertion [" + firstAssertion.getID() + "] is missing a subject");
356-
result = result.concat(error);
366+
errors.add(new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
367+
"Assertion [" + firstAssertion.getID() + "] is missing a subject"));
357368
}
358369
}
359370

360-
if (result.hasErrors()) {
361-
Collection<Saml2Error> errors = result.getErrors();
362-
if (this.logger.isTraceEnabled()) {
363-
this.logger.trace("Found " + errors.size() + " validation errors in SAML response [" + response.getID()
364-
+ "]: " + errors);
365-
}
366-
else if (this.logger.isDebugEnabled()) {
367-
this.logger
368-
.debug("Found " + errors.size() + " validation errors in SAML response [" + response.getID() + "]");
369-
}
370-
Saml2Error first = errors.iterator().next();
371-
throw new Saml2AuthenticationException(first);
372-
}
373-
else {
371+
reportErrors(response, errors);
372+
}
373+
374+
private void reportErrors(Response response, Collection<Saml2Error> errors) {
375+
if (errors.isEmpty()) {
374376
if (this.logger.isDebugEnabled()) {
375377
this.logger.debug("Successfully processed SAML Response [" + response.getID() + "]");
376378
}
379+
return;
380+
}
381+
if (this.logger.isTraceEnabled()) {
382+
this.logger.trace("Found " + errors.size() + " validation errors in SAML response [" + response.getID()
383+
+ "]: " + errors);
384+
}
385+
else if (this.logger.isDebugEnabled()) {
386+
this.logger
387+
.debug("Found " + errors.size() + " validation errors in SAML response [" + response.getID() + "]");
377388
}
389+
Saml2Error first = errors.iterator().next();
390+
throw new Saml2AuthenticationException(first);
378391
}
379392

380393
private Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseSignatureValidator() {

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.ByteArrayOutputStream;
2020
import java.io.IOException;
21+
import java.io.OutputStream;
2122
import java.nio.charset.StandardCharsets;
2223
import java.util.Arrays;
2324
import java.util.Base64;
@@ -64,7 +65,7 @@ static byte[] samlDeflate(String s) {
6465
static String samlInflate(byte[] b) {
6566
try {
6667
ByteArrayOutputStream out = new ByteArrayOutputStream();
67-
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
68+
InflaterOutputStream iout = new InflaterOutputStream(new CappedOutputStream(out), new Inflater(true));
6869
iout.write(b);
6970
iout.finish();
7071
return new String(out.toByteArray(), StandardCharsets.UTF_8);
@@ -193,4 +194,27 @@ void checkAcceptable(String ins) {
193194

194195
}
195196

197+
static class CappedOutputStream extends OutputStream {
198+
199+
private static final long MAX_SIZE = 1024 * 1024;
200+
201+
private final OutputStream delegate;
202+
203+
private int size;
204+
205+
CappedOutputStream(OutputStream delegate) {
206+
this.delegate = delegate;
207+
}
208+
209+
@Override
210+
public void write(int b) throws IOException {
211+
if (this.size >= MAX_SIZE) {
212+
throw new IOException("SAML payload exceeded maximum size of " + MAX_SIZE);
213+
}
214+
this.delegate.write(b);
215+
this.size++;
216+
}
217+
218+
}
219+
196220
}

0 commit comments

Comments
 (0)