From 2104c495970e8e851fe7c6254c2c445ff55f4087 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:55:46 -0600 Subject: [PATCH 1/2] Add tests for OAuth2PushedAuthorizationRequestUri Adds baseline test coverage for OAuth2PushedAuthorizationRequestUri, which previously had none: a round-trip via create()/parse() and an explicit parse() case on a hand-constructed request URI. See gh-19444. Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com> --- ...th2PushedAuthorizationRequestUriTests.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUriTests.java diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUriTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUriTests.java new file mode 100644 index 0000000000..cb36aa730a --- /dev/null +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUriTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.authorization.authentication; + +import java.time.Instant; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OAuth2PushedAuthorizationRequestUri}. + * + * @author Josh Cummings + */ +public class OAuth2PushedAuthorizationRequestUriTests { + + @Test + public void parseWhenValidRequestUriThenReturnsExpectedValues() { + String state = "abcXYZ123-abcXYZ123"; + long epochMilli = 1700000000000L; + String requestUri = "urn:ietf:params:oauth:request_uri:" + state + "___" + epochMilli; + + OAuth2PushedAuthorizationRequestUri parsed = OAuth2PushedAuthorizationRequestUri.parse(requestUri); + + assertThat(parsed.getRequestUri()).isEqualTo(requestUri); + assertThat(parsed.getState()).isEqualTo(state + "___" + epochMilli); + assertThat(parsed.getExpiresAt()).isEqualTo(Instant.ofEpochMilli(epochMilli)); + } + + @Test + public void createWhenParsedThenReturnsEquivalentValues() { + Instant expiresAt = Instant.ofEpochMilli(1700000000000L); + + OAuth2PushedAuthorizationRequestUri created = OAuth2PushedAuthorizationRequestUri.create(expiresAt); + OAuth2PushedAuthorizationRequestUri parsed = OAuth2PushedAuthorizationRequestUri.parse(created.getRequestUri()); + + assertThat(parsed.getRequestUri()).isEqualTo(created.getRequestUri()); + assertThat(parsed.getState()).isEqualTo(created.getState()); + assertThat(parsed.getExpiresAt()).isEqualTo(created.getExpiresAt()); + } + +} From e4fafce0661a3010a1d4248a3e38cf4a3156ddb7 Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Fri, 31 Jul 2026 12:50:26 -0600 Subject: [PATCH 2/2] Fix OAuth2PushedAuthorizationRequestUri parsing OAuth2PushedAuthorizationRequestUri uses Base64URL encoding for the state, which can produce a value containing ___, the same sequence used as the delimiter. As a result, indexOf may locate a delimiter within the state instead of the delimiter preceding the expiration timestamp. Use lastIndexOf to reliably locate the delimiter before the timestamp without changing the existing request URI format. Closes gh-19444 Signed-off-by: Andrey Litvitski --- .../OAuth2PushedAuthorizationRequestUri.java | 3 ++- .../OAuth2PushedAuthorizationRequestUriTests.java | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUri.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUri.java index 96566f0b6f..b60725131f 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUri.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUri.java @@ -27,6 +27,7 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator; * Requests. * * @author Joe Grandja + * @author Andrey Litvitski * @since 7.0 */ final class OAuth2PushedAuthorizationRequestUri { @@ -60,7 +61,7 @@ final class OAuth2PushedAuthorizationRequestUri { static OAuth2PushedAuthorizationRequestUri parse(String requestUri) { int stateStartIndex = REQUEST_URI_PREFIX.length(); - int expiresAtStartIndex = requestUri.indexOf(REQUEST_URI_DELIMITER) + REQUEST_URI_DELIMITER.length(); + int expiresAtStartIndex = requestUri.lastIndexOf(REQUEST_URI_DELIMITER) + REQUEST_URI_DELIMITER.length(); OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = new OAuth2PushedAuthorizationRequestUri(); pushedAuthorizationRequestUri.requestUri = requestUri; pushedAuthorizationRequestUri.state = requestUri.substring(stateStartIndex); diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUriTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUriTests.java index cb36aa730a..3adfc0fd5e 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUriTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUriTests.java @@ -26,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link OAuth2PushedAuthorizationRequestUri}. * * @author Josh Cummings + * @author Andrey Litvitski */ public class OAuth2PushedAuthorizationRequestUriTests { @@ -54,4 +55,17 @@ public class OAuth2PushedAuthorizationRequestUriTests { assertThat(parsed.getExpiresAt()).isEqualTo(created.getExpiresAt()); } + @Test + public void parseWhenStateContainsDelimiterThenParsesSuccessfully() { + String state = "xXMGJTZwzXIFL8i_DFu_EM8IeWC___frCWjpiF2q-xs="; + long epochMillis = 1781670640281L; + String requestUri = "urn:ietf:params:oauth:request_uri:" + state + "___" + epochMillis; + + OAuth2PushedAuthorizationRequestUri parsedUri = OAuth2PushedAuthorizationRequestUri.parse(requestUri); + + assertThat(parsedUri.getRequestUri()).isEqualTo(requestUri); + assertThat(parsedUri.getState()).isEqualTo(state + "___" + epochMillis); + assertThat(parsedUri.getExpiresAt()).isEqualTo(Instant.ofEpochMilli(epochMillis)); + } + }