From ac1e7f8ddb06208145be2cf729ae61dc5453e62d Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:33:15 -0700 Subject: [PATCH 1/4] g-orchestrated: add instance methods to OIDAuthorizationService --- Sources/AppAuthCore/OIDAuthorizationService.h | 49 +++++++++++++++++-- Sources/AppAuthCore/OIDAuthorizationService.m | 35 +++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.h b/Sources/AppAuthCore/OIDAuthorizationService.h index c8fee5358..3adf94b33 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.h +++ b/Sources/AppAuthCore/OIDAuthorizationService.h @@ -90,10 +90,12 @@ typedef void (^OIDRegistrationCompletion)(OIDRegistrationResponse *_Nullable reg */ @property(nonatomic, readonly) OIDServiceConfiguration *configuration; -/*! @internal - @brief Unavailable. This class should not be initialized. +/*! @brief Creates a service whose instance methods perform the same operations as the class + methods of this class. + @discussion Hold an instance where you would otherwise call the class methods directly, so + that tests can substitute a subclass such as @c OIDFakeAuthorizationService. */ -- (instancetype)init NS_UNAVAILABLE; +- (instancetype)init; /*! @brief Convenience method for creating an authorization service configuration from an OpenID Connect compliant issuer URL. @@ -165,6 +167,47 @@ typedef void (^OIDRegistrationCompletion)(OIDRegistrationResponse *_Nullable reg + (void)performRegistrationRequest:(OIDRegistrationRequest *)request completion:(OIDRegistrationCompletion)completion; +/*! @brief Instance counterpart of @c +presentAuthorizationRequest:externalUserAgent:callback:. + @param request The authorization request. + @param externalUserAgent Generic external user-agent that can present an authorization + request. + @param callback The method called when the request has completed or failed. + */ +- (id)presentAuthorizationRequest:(OIDAuthorizationRequest *)request + externalUserAgent:(id)externalUserAgent + callback:(OIDAuthorizationCallback)callback; + +/*! @brief Instance counterpart of @c +presentEndSessionRequest:externalUserAgent:callback:. + @param request The end-session logout request. + @param externalUserAgent Generic external user-agent that can present user-agent requests. + @param callback The method called when the request has completed or failed. + */ +- (id)presentEndSessionRequest:(OIDEndSessionRequest *)request + externalUserAgent:(id)externalUserAgent + callback:(OIDEndSessionCallback)callback; + +/*! @brief Instance counterpart of @c +performTokenRequest:callback:. + @param request The token request. + @param callback The method called when the request has completed or failed. + */ +- (void)performTokenRequest:(OIDTokenRequest *)request callback:(OIDTokenCallback)callback; + +/*! @brief Instance counterpart of @c +performTokenRequest:originalAuthorizationResponse:callback:. + @param request The token request. + @param authorizationResponse The original authorization response related to this token request. + @param callback The method called when the request has completed or failed. + */ +- (void)performTokenRequest:(OIDTokenRequest *)request + originalAuthorizationResponse:(OIDAuthorizationResponse *_Nullable)authorizationResponse + callback:(OIDTokenCallback)callback; + +/*! @brief Instance counterpart of @c +performRegistrationRequest:completion:. + @param request The registration request. + @param completion The method called when the request has completed or failed. + */ +- (void)performRegistrationRequest:(OIDRegistrationRequest *)request + completion:(OIDRegistrationCompletion)completion; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index d99ade780..dc3bf1c8a 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -864,6 +864,41 @@ + (void)performRegistrationRequest:(OIDRegistrationRequest *)request }] resume]; } +#pragma mark - Instance API + +- (id)presentAuthorizationRequest:(OIDAuthorizationRequest *)request + externalUserAgent:(id)externalUserAgent + callback:(OIDAuthorizationCallback)callback { + return [OIDAuthorizationService presentAuthorizationRequest:request + externalUserAgent:externalUserAgent + callback:callback]; +} + +- (id)presentEndSessionRequest:(OIDEndSessionRequest *)request + externalUserAgent:(id)externalUserAgent + callback:(OIDEndSessionCallback)callback { + return [OIDAuthorizationService presentEndSessionRequest:request + externalUserAgent:externalUserAgent + callback:callback]; +} + +- (void)performTokenRequest:(OIDTokenRequest *)request callback:(OIDTokenCallback)callback { + [self performTokenRequest:request originalAuthorizationResponse:nil callback:callback]; +} + +- (void)performTokenRequest:(OIDTokenRequest *)request + originalAuthorizationResponse:(OIDAuthorizationResponse *_Nullable)authorizationResponse + callback:(OIDTokenCallback)callback { + [OIDAuthorizationService performTokenRequest:request + originalAuthorizationResponse:authorizationResponse + callback:callback]; +} + +- (void)performRegistrationRequest:(OIDRegistrationRequest *)request + completion:(OIDRegistrationCompletion)completion { + [OIDAuthorizationService performRegistrationRequest:request completion:completion]; +} + @end NS_ASSUME_NONNULL_END From 1999dbc0d2009a4d3be3080de520f4e285990997 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:42:31 -0700 Subject: [PATCH 2/4] g-orchestrated: tidy the OIDAuthorizationService instance API and test it --- CHANGELOG.md | 1 + Sources/AppAuthCore/OIDAuthorizationService.h | 20 +-- Sources/AppAuthCore/OIDAuthorizationService.m | 14 +- .../OIDAuthorizationServiceInstanceTests.m | 159 ++++++++++++++++++ 4 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 UnitTests/OIDAuthorizationServiceInstanceTests.m diff --git a/CHANGELOG.md b/CHANGELOG.md index 8615f2de5..07ff1918a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Discovery documents with a JSON `null` or a non-string value in an optional URL-valued field no longer raise `NSInvalidArgumentException`; the property reads as `nil`. The same value in a required URL field now fails with `OIDErrorCodeInvalidDiscoveryDocument`. An `authorization_endpoint` that does not parse now fails the document too, like the other required URL fields. - Deprecated CocoaPods distribution. AppAuth 3.0.0 is the last release published to CocoaPods; CocoaPods trunk becomes read-only on December 2, 2026. See [MIGRATING_FROM_COCOAPODS.md](MIGRATING_FROM_COCOAPODS.md) for moving to Swift Package Manager. Addresses issue #962. - Fix a crash (`NSInvalidArgumentException`) when a token, registration or device authorization endpoint answers with a JSON array instead of an object. The callback now gets an error. Addresses issue #987. +- Added instance-method counterparts of the `OIDAuthorizationService` class methods, and made `-init` public, so the service can be injected and faked in tests. # 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)) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.h b/Sources/AppAuthCore/OIDAuthorizationService.h index 3adf94b33..36c87cfe7 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.h +++ b/Sources/AppAuthCore/OIDAuthorizationService.h @@ -90,10 +90,8 @@ typedef void (^OIDRegistrationCompletion)(OIDRegistrationResponse *_Nullable reg */ @property(nonatomic, readonly) OIDServiceConfiguration *configuration; -/*! @brief Creates a service whose instance methods perform the same operations as the class - methods of this class. - @discussion Hold an instance where you would otherwise call the class methods directly, so - that tests can substitute a subclass such as @c OIDFakeAuthorizationService. +/*! @brief Creates a service whose instance methods call the matching class methods. + @discussion Tests can hand in a subclass that fakes the instance methods. */ - (instancetype)init; @@ -173,18 +171,20 @@ typedef void (^OIDRegistrationCompletion)(OIDRegistrationResponse *_Nullable reg request. @param callback The method called when the request has completed or failed. */ -- (id)presentAuthorizationRequest:(OIDAuthorizationRequest *)request - externalUserAgent:(id)externalUserAgent - callback:(OIDAuthorizationCallback)callback; +- (id) + presentAuthorizationRequest:(OIDAuthorizationRequest *)request + externalUserAgent:(id)externalUserAgent + callback:(OIDAuthorizationCallback)callback; /*! @brief Instance counterpart of @c +presentEndSessionRequest:externalUserAgent:callback:. @param request The end-session logout request. @param externalUserAgent Generic external user-agent that can present user-agent requests. @param callback The method called when the request has completed or failed. */ -- (id)presentEndSessionRequest:(OIDEndSessionRequest *)request - externalUserAgent:(id)externalUserAgent - callback:(OIDEndSessionCallback)callback; +- (id) + presentEndSessionRequest:(OIDEndSessionRequest *)request + externalUserAgent:(id)externalUserAgent + callback:(OIDEndSessionCallback)callback; /*! @brief Instance counterpart of @c +performTokenRequest:callback:. @param request The token request. diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index dc3bf1c8a..9d401ac4d 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -866,17 +866,19 @@ + (void)performRegistrationRequest:(OIDRegistrationRequest *)request #pragma mark - Instance API -- (id)presentAuthorizationRequest:(OIDAuthorizationRequest *)request - externalUserAgent:(id)externalUserAgent - callback:(OIDAuthorizationCallback)callback { +- (id) + presentAuthorizationRequest:(OIDAuthorizationRequest *)request + externalUserAgent:(id)externalUserAgent + callback:(OIDAuthorizationCallback)callback { return [OIDAuthorizationService presentAuthorizationRequest:request externalUserAgent:externalUserAgent callback:callback]; } -- (id)presentEndSessionRequest:(OIDEndSessionRequest *)request - externalUserAgent:(id)externalUserAgent - callback:(OIDEndSessionCallback)callback { +- (id) + presentEndSessionRequest:(OIDEndSessionRequest *)request + externalUserAgent:(id)externalUserAgent + callback:(OIDEndSessionCallback)callback { return [OIDAuthorizationService presentEndSessionRequest:request externalUserAgent:externalUserAgent callback:callback]; diff --git a/UnitTests/OIDAuthorizationServiceInstanceTests.m b/UnitTests/OIDAuthorizationServiceInstanceTests.m new file mode 100644 index 000000000..b00ad3180 --- /dev/null +++ b/UnitTests/OIDAuthorizationServiceInstanceTests.m @@ -0,0 +1,159 @@ +/*! @file OIDAuthorizationServiceInstanceTests.m + @brief AppAuth iOS SDK + @copyright + Copyright 2026 The AppAuth Authors. All Rights Reserved. + @copydetails + 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 + + http://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. + */ + +#import + +#import "OIDAuthorizationRequestTests.h" +#import "OIDEndSessionRequestTests.h" +#import "OIDTokenRequestTests.h" + +#if SWIFT_PACKAGE +@import AppAuthCore; +#else +#import "Sources/AppAuthCore/OIDAuthorizationResponse.h" +#import "Sources/AppAuthCore/OIDAuthorizationService.h" +#import "Sources/AppAuthCore/OIDEndSessionResponse.h" +#import "Sources/AppAuthCore/OIDError.h" +#import "Sources/AppAuthCore/OIDExternalUserAgent.h" +#import "Sources/AppAuthCore/OIDExternalUserAgentSession.h" +#import "Sources/AppAuthCore/OIDTokenRequest.h" +#import "Sources/AppAuthCore/OIDTokenResponse.h" +#endif + +// Ignore warnings about "Use of GNU statement expression extension" which is raised by our use of +// the XCTAssert___ macros. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wgnu" + +NS_ASSUME_NONNULL_BEGIN + +/*! @brief An external user agent that refuses to present anything. + */ +@interface OIDRefusingExternalUserAgent : NSObject +@end + +@implementation OIDRefusingExternalUserAgent + +- (BOOL)presentExternalUserAgentRequest:(id)request + session:(id)session { + return NO; +} + +- (void)dismissExternalUserAgentAnimated:(BOOL)animated completion:(void (^)(void))completion { + if (completion) { + completion(); + } +} + +@end + +/*! @brief Records the three-argument token request instead of performing it. + */ +@interface OIDTokenRecordingAuthorizationService : OIDAuthorizationService + +/*! @brief How many three-argument token requests were made. */ +@property(nonatomic, readonly) NSUInteger tokenRequestCount; + +/*! @brief The request passed to the most recent three-argument token request. */ +@property(nonatomic, readonly, nullable) OIDTokenRequest *lastTokenRequest; + +/*! @brief The authorization response passed to the most recent three-argument token request. */ +@property(nonatomic, readonly, nullable) + OIDAuthorizationResponse *lastOriginalAuthorizationResponse; + +@end + +@implementation OIDTokenRecordingAuthorizationService + +- (void)performTokenRequest:(OIDTokenRequest *)request + originalAuthorizationResponse:(nullable OIDAuthorizationResponse *)authorizationResponse + callback:(OIDTokenCallback)callback { + _tokenRequestCount++; + _lastTokenRequest = request; + _lastOriginalAuthorizationResponse = authorizationResponse; +} + +@end + +/*! @brief Unit tests for the instance methods of @c OIDAuthorizationService. + */ +@interface OIDAuthorizationServiceInstanceTests : XCTestCase +@end + +@implementation OIDAuthorizationServiceInstanceTests + +/*! @brief A refused authorization request fails with @c OIDErrorCodeSafariOpenError. */ +- (void)testPresentAuthorizationRequestFailsWhenUserAgentRefuses { + OIDAuthorizationService *service = [[OIDAuthorizationService alloc] init]; + OIDRefusingExternalUserAgent *userAgent = [[OIDRefusingExternalUserAgent alloc] init]; + __block OIDAuthorizationResponse *receivedResponse; + __block NSError *receivedError; + id session = + [service presentAuthorizationRequest:[OIDAuthorizationRequestTests testInstance] + externalUserAgent:userAgent + callback:^(OIDAuthorizationResponse *_Nullable response, + NSError *_Nullable error) { + receivedResponse = response; + receivedError = error; + }]; + XCTAssertNotNil(session); + XCTAssertNil(receivedResponse); + XCTAssertEqualObjects(receivedError.domain, OIDGeneralErrorDomain); + XCTAssertEqual(receivedError.code, OIDErrorCodeSafariOpenError); +} + +/*! @brief A refused end session request fails with @c OIDErrorCodeSafariOpenError. */ +- (void)testPresentEndSessionRequestFailsWhenUserAgentRefuses { + OIDAuthorizationService *service = [[OIDAuthorizationService alloc] init]; + OIDRefusingExternalUserAgent *userAgent = [[OIDRefusingExternalUserAgent alloc] init]; + __block OIDEndSessionResponse *receivedResponse; + __block NSError *receivedError; + id session = + [service presentEndSessionRequest:[OIDEndSessionRequestTests testInstance] + externalUserAgent:userAgent + callback:^(OIDEndSessionResponse *_Nullable response, + NSError *_Nullable error) { + receivedResponse = response; + receivedError = error; + }]; + XCTAssertNotNil(session); + XCTAssertNil(receivedResponse); + XCTAssertEqualObjects(receivedError.domain, OIDGeneralErrorDomain); + XCTAssertEqual(receivedError.code, OIDErrorCodeSafariOpenError); +} + +/*! @brief The two-argument token request goes through the three-argument one, with no + authorization response. + */ +- (void)testTwoArgumentTokenRequestCallsThreeArgumentTokenRequest { + OIDTokenRecordingAuthorizationService *service = + [[OIDTokenRecordingAuthorizationService alloc] init]; + OIDTokenRequest *request = [OIDTokenRequestTests testInstance]; + [service performTokenRequest:request + callback:^(OIDTokenResponse *_Nullable response, + NSError *_Nullable error) {}]; + XCTAssertEqual(service.tokenRequestCount, 1u); + XCTAssertEqual(service.lastTokenRequest, request); + XCTAssertNil(service.lastOriginalAuthorizationResponse); +} + +@end + +NS_ASSUME_NONNULL_END + +#pragma GCC diagnostic pop From d128508bd0e753730b7251179442d879f97180d3 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:43:54 -0700 Subject: [PATCH 3/4] g-orchestrated: implement the public OIDAuthorizationService init --- Sources/AppAuthCore/OIDAuthorizationService.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.m b/Sources/AppAuthCore/OIDAuthorizationService.m index 9d401ac4d..dce7467c7 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.m +++ b/Sources/AppAuthCore/OIDAuthorizationService.m @@ -866,6 +866,10 @@ + (void)performRegistrationRequest:(OIDRegistrationRequest *)request #pragma mark - Instance API +- (instancetype)init { + return [super init]; +} + - (id) presentAuthorizationRequest:(OIDAuthorizationRequest *)request externalUserAgent:(id)externalUserAgent From 340b94f2fec5d1f877cf8703ff9c1c318ae70238 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 2 Nov 2026 14:13:58 -0800 Subject: [PATCH 4/4] g-orchestrated: deprecate the unused OIDAuthorizationService configuration property --- CHANGELOG.md | 1 + Sources/AppAuthCore/OIDAuthorizationService.h | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07ff1918a..fa17ca1c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Deprecated CocoaPods distribution. AppAuth 3.0.0 is the last release published to CocoaPods; CocoaPods trunk becomes read-only on December 2, 2026. See [MIGRATING_FROM_COCOAPODS.md](MIGRATING_FROM_COCOAPODS.md) for moving to Swift Package Manager. Addresses issue #962. - Fix a crash (`NSInvalidArgumentException`) when a token, registration or device authorization endpoint answers with a JSON array instead of an object. The callback now gets an error. Addresses issue #987. - Added instance-method counterparts of the `OIDAuthorizationService` class methods, and made `-init` public, so the service can be injected and faked in tests. +- Deprecated `OIDAuthorizationService.configuration`. It is never set and is always `nil`; use the request's `configuration` instead. It will be removed in 4.0. # 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)) diff --git a/Sources/AppAuthCore/OIDAuthorizationService.h b/Sources/AppAuthCore/OIDAuthorizationService.h index 36c87cfe7..08524d3c0 100644 --- a/Sources/AppAuthCore/OIDAuthorizationService.h +++ b/Sources/AppAuthCore/OIDAuthorizationService.h @@ -82,13 +82,13 @@ typedef void (^OIDRegistrationCompletion)(OIDRegistrationResponse *_Nullable reg */ @interface OIDAuthorizationService : NSObject -/*! @brief The service's configuration. - @remarks Each authorization service is initialized with a configuration. This configuration - specifies how to connect to a particular OAuth provider. Clients should use separate - authorization service instances for each provider they wish to integrate with. - Configurations may be created manually, or via an OpenID Connect Discovery Document. +/*! @brief Unused, and always nil. + @discussion No initializer sets this, and nothing in AppAuth reads it. Each request carries + its own configuration (@c request.configuration). This property will be removed in + AppAuth 4.0. */ -@property(nonatomic, readonly) OIDServiceConfiguration *configuration; +@property(nonatomic, readonly) OIDServiceConfiguration *configuration + __deprecated_msg("Always nil. Use the configuration on the request instead."); /*! @brief Creates a service whose instance methods call the matching class methods. @discussion Tests can hand in a subclass that fakes the instance methods.