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). diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index 3ce33e059..f7a9e2422 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 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 { + 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 @@ -307,12 +317,25 @@ - (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 %@. 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 " + "document. Response: %@", + _request.state, + response]; + } else { + userInfo[NSLocalizedDescriptionKey] = + [NSString stringWithFormat:@"State in the end session response does not match the " + "request, expecting %@ but got %@. Response: %@", + _request.state, + response.state, + response]; + } response = nil; responseError = [NSError errorWithDomain:OIDOAuthAuthorizationErrorDomain code:OIDErrorCodeOAuthAuthorizationClientError