From dbb0e3fcad4d7672aea1d89f0bcd826f4296474d Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:31:09 -0700 Subject: [PATCH 1/7] g-orchestrated: distinguish missing vs mismatched state in end session error --- Sources/AppAuthCore/OIDAuthorizationService.m | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index 3ce33e059..669040c02 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -307,12 +307,31 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullabl // verifies that the state in the response matches the state in the request, or both are nil if (!OIDIsEqualIncludingNil(_request.state, response.state)) { NSMutableDictionary *userInfo = [query.dictionaryValue mutableCopy]; - userInfo[NSLocalizedDescriptionKey] = - [NSString stringWithFormat:@"State mismatch, expecting %@ but got %@ in authorization " - "response %@", - _request.state, - response.state, - response]; + if (!response.state) { + userInfo[NSLocalizedDescriptionKey] = + [NSString stringWithFormat:@"The end session response is missing the state parameter, " + "expecting %@ but got nil. This commonly means the " + "configured end_session_endpoint is not an OpenID Connect " + "RP-Initiated Logout endpoint - some providers advertise a " + "legacy or SAML single-logout endpoint under that key, and " + "such endpoints do not echo the OAuth state parameter. " + "Verify the end_session_endpoint in the provider's " + "discovery document. If the provider genuinely cannot " + "return state, the caller may construct " + "OIDEndSessionRequest with an explicit nil state to omit " + "it, which disables state validation and the CSRF " + "protection it provides, in end session response %@", + _request.state, + response]; + } else { + userInfo[NSLocalizedDescriptionKey] = + [NSString stringWithFormat:@"State mismatch, expecting %@ but got %@ in end session " + "response %@. The state in the response does not match " + "the state in the request.", + _request.state, + response.state, + response]; + } response = nil; responseError = [NSError errorWithDomain:OIDOAuthAuthorizationErrorDomain code:OIDErrorCodeOAuthAuthorizationClientError From d513ea8234e223837f1c925739f8174c4b7eee30 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:31:12 -0700 Subject: [PATCH 2/7] g-orchestrated: CHANGELOG entry for state validation error diagnostics --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00ee98e6a..aaf49792c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# UNRELEASED +- Improved the state validation error messages to distinguish a missing `state` parameter from a mismatched one. The end session message now notes that a provider's `end_session_endpoint` may point at a legacy or SAML single-logout endpoint, which does not echo `state`. Addresses issue #956. + # 3.0.0 - BREAKING: Updates made to support Xcode 27. ([#972](https://github.com/openid/AppAuth-iOS/pull/972), [#973](https://github.com/openid/AppAuth-iOS/pull/973)) -- Raised minimum deployment targets to iOS 15.0, macOS 12.0, tvOS 15.0 and watchOS 9.0 (minimum for Xcode 27). From f67441675b8846f050bd73b8980f560946b2a975 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:34:32 -0700 Subject: [PATCH 3/7] g-orchestrated: distinguish missing vs mismatched state in authorization error --- Sources/AppAuthCore/OIDAuthorizationService.m | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index 669040c02..cfcad849b 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -170,12 +170,22 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullabl // verifies that the state in the response matches the state in the request, or both are nil if (!OIDIsEqualIncludingNil(_request.state, response.state)) { NSMutableDictionary *userInfo = [query.dictionaryValue mutableCopy]; - userInfo[NSLocalizedDescriptionKey] = - [NSString stringWithFormat:@"State mismatch, expecting %@ but got %@ in authorization " - "response %@", - _request.state, - response.state, - response]; + if (response.state == nil) { + userInfo[NSLocalizedDescriptionKey] = + [NSString stringWithFormat:@"The authorization response is missing the state parameter, " + "expecting %@. RFC 6749 section 4.1.2 requires the " + "authorization server to return the exact state value sent " + "in the request. Authorization response: %@", + _request.state, + response]; + } else { + userInfo[NSLocalizedDescriptionKey] = + [NSString stringWithFormat:@"State mismatch, expecting %@ but got %@ in authorization " + "response %@", + _request.state, + response.state, + response]; + } response = nil; responseError = [NSError errorWithDomain:OIDOAuthAuthorizationErrorDomain code:OIDErrorCodeOAuthAuthorizationClientError From 0d8da48c41d1adca5b25be31fae4b0c3d0336eb4 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:52:39 -0700 Subject: [PATCH 4/7] g-orchestrated: reword end session state remedy for nonnull state parameter --- Sources/AppAuthCore/OIDAuthorizationService.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index cfcad849b..a33bd5276 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -327,10 +327,10 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullabl "such endpoints do not echo the OAuth state parameter. " "Verify the end_session_endpoint in the provider's " "discovery document. If the provider genuinely cannot " - "return state, the caller may construct " - "OIDEndSessionRequest with an explicit nil state to omit " - "it, which disables state validation and the CSRF " - "protection it provides, in end session response %@", + "return state, the OIDEndSessionRequest may be " + "constructed without a state value, which disables " + "state validation and the CSRF protection it provides, " + "in end session response %@", _request.state, response]; } else { From 2bda74c2b80fc26bff9217c3ba98d11cfe0947d0 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:00:55 -0700 Subject: [PATCH 5/7] g-orchestrated: tighten state validation messages and drop unsupported remedy --- Sources/AppAuthCore/OIDAuthorizationService.m | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index a33bd5276..e0c79efb1 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -173,11 +173,11 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullabl if (response.state == nil) { userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"The authorization response is missing the state parameter, " - "expecting %@. RFC 6749 section 4.1.2 requires the " - "authorization server to return the exact state value sent " - "in the request. Authorization response: %@", - _request.state, - response]; + "expecting %@. RFC 6749 section 4.1.2 requires the " + "authorization server to echo the exact state value from " + "the request. Response: %@", + _request.state, + response]; } else { userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"State mismatch, expecting %@ but got %@ in authorization " @@ -320,24 +320,18 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullabl if (!response.state) { userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"The end session response is missing the state parameter, " - "expecting %@ but got nil. This commonly means the " - "configured end_session_endpoint is not an OpenID Connect " - "RP-Initiated Logout endpoint - some providers advertise a " - "legacy or SAML single-logout endpoint under that key, and " - "such endpoints do not echo the OAuth state parameter. " - "Verify the end_session_endpoint in the provider's " - "discovery document. If the provider genuinely cannot " - "return state, the OIDEndSessionRequest may be " - "constructed without a state value, which disables " - "state validation and the CSRF protection it provides, " - "in end session response %@", + "expecting %@. This usually means end_session_endpoint is " + "not an OpenID Connect RP-Initiated Logout endpoint; some " + "providers advertise a legacy or SAML single-logout " + "endpoint under that key, and those do not echo state. " + "Check end_session_endpoint in the provider's discovery " + "document. Response: %@", _request.state, response]; } else { userInfo[NSLocalizedDescriptionKey] = - [NSString stringWithFormat:@"State mismatch, expecting %@ but got %@ in end session " - "response %@. The state in the response does not match " - "the state in the request.", + [NSString stringWithFormat:@"State in the end session response does not match the " + "request, expecting %@ but got %@. Response: %@", _request.state, response.state, response]; From 69cca08916727a5a6279b9b9be5523db2a7999d3 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:12:57 -0700 Subject: [PATCH 6/7] g-orchestrated: soften end session endpoint cause to may rather than usually --- Sources/AppAuthCore/OIDAuthorizationService.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index e0c79efb1..fd0dd6ec2 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -320,8 +320,8 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullabl if (!response.state) { userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"The end session response is missing the state parameter, " - "expecting %@. This usually means end_session_endpoint is " - "not an OpenID Connect RP-Initiated Logout endpoint; some " + "expecting %@. This may mean end_session_endpoint is not " + "an OpenID Connect RP-Initiated Logout endpoint; some " "providers advertise a legacy or SAML single-logout " "endpoint under that key, and those do not echo state. " "Check end_session_endpoint in the provider's discovery " From fd6f698f961316cc55b0cacca88813b1e446d9fa Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:28:18 -0700 Subject: [PATCH 7/7] g-orchestrated: cite OpenID Connect Core alongside RFC 6749 for state echo --- Sources/AppAuthCore/OIDAuthorizationService.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index fd0dd6ec2..f7a9e2422 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -173,9 +173,9 @@ - (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL error:(NSError *_Nullabl if (response.state == nil) { userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"The authorization response is missing the state parameter, " - "expecting %@. RFC 6749 section 4.1.2 requires the " - "authorization server to echo the exact state value from " - "the request. Response: %@", + "expecting %@. RFC 6749 section 4.1.2 and OpenID Connect " + "Core section 3.1.2.5 require the authorization server to " + "echo the exact state value from the request. Response: %@", _request.state, response]; } else {