GIDAuthenticationTest.m 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import <XCTest/XCTest.h>
  15. #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDAuthentication.h"
  16. #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
  17. #import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
  18. #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
  19. #import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
  20. #import "GoogleSignIn/Sources/GIDSignInPreferences.h"
  21. #import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
  22. #import "GoogleSignIn/Tests/Unit/OIDTokenRequest+Testing.h"
  23. #import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
  24. #ifdef SWIFT_PACKAGE
  25. @import AppAuth;
  26. @import GoogleUtilities_MethodSwizzler;
  27. @import GoogleUtilities_SwizzlerTestHelpers;
  28. @import GTMAppAuth;
  29. @import GTMSessionFetcherCore;
  30. @import OCMock;
  31. #else
  32. #import <AppAuth/OIDAuthState.h>
  33. #import <AppAuth/OIDAuthorizationRequest.h>
  34. #import <AppAuth/OIDAuthorizationResponse.h>
  35. #import <AppAuth/OIDAuthorizationService.h>
  36. #import <AppAuth/OIDError.h>
  37. #import <AppAuth/OIDIDToken.h>
  38. #import <AppAuth/OIDServiceConfiguration.h>
  39. #import <AppAuth/OIDTokenRequest.h>
  40. #import <AppAuth/OIDTokenResponse.h>
  41. #import <GoogleUtilities/GULSwizzler.h>
  42. #import <GoogleUtilities/GULSwizzler+Unswizzle.h>
  43. #import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
  44. #import <GTMSessionFetcher/GTMSessionFetcher.h>
  45. #import <OCMock/OCMock.h>
  46. #endif
  47. static NSString *const kClientID = @"87654321.googleusercontent.com";
  48. static NSString *const kNewAccessToken = @"new_access_token";
  49. static NSString *const kUserEmail = @"foo@gmail.com";
  50. static NSTimeInterval const kExpireTime = 442886117;
  51. static NSTimeInterval const kNewExpireTime = 442886123;
  52. static NSTimeInterval const kNewExpireTime2 = 442886124;
  53. static NSTimeInterval const kTimeAccuracy = 10;
  54. // The system name in old iOS versions.
  55. static NSString *const kOldIOSName = @"iPhone OS";
  56. // The system name in new iOS versions.
  57. static NSString *const kNewIOSName = @"iOS";
  58. // List of observed properties of the class being tested.
  59. static NSString *const kObservedProperties[] = {
  60. @"accessToken",
  61. @"accessTokenExpirationDate",
  62. @"idToken",
  63. @"idTokenExpirationDate"
  64. };
  65. static const NSUInteger kNumberOfObservedProperties =
  66. sizeof(kObservedProperties) / sizeof(*kObservedProperties);
  67. // Bit position for notification change type bitmask flags.
  68. // Must match the list of observed properties above.
  69. typedef NS_ENUM(NSUInteger, ChangeType) {
  70. kChangeTypeAccessTokenPrior,
  71. kChangeTypeAccessToken,
  72. kChangeTypeAccessTokenExpirationDatePrior,
  73. kChangeTypeAccessTokenExpirationDate,
  74. kChangeTypeIDTokenPrior,
  75. kChangeTypeIDToken,
  76. kChangeTypeIDTokenExpirationDatePrior,
  77. kChangeTypeIDTokenExpirationDate,
  78. kChangeTypeEnd // not a real change type but an end mark for calculating |kChangeAll|
  79. };
  80. static const NSUInteger kChangeNone = 0u;
  81. static const NSUInteger kChangeAll = (1u << kChangeTypeEnd) - 1u;
  82. #if __has_feature(c_static_assert) || __has_extension(c_static_assert)
  83. _Static_assert(kChangeTypeEnd == (sizeof(kObservedProperties) / sizeof(*kObservedProperties)) * 2,
  84. "List of observed properties must match list of change notification enums");
  85. #endif
  86. @interface GIDAuthenticationTest : XCTestCase
  87. @end
  88. @implementation GIDAuthenticationTest {
  89. // Whether the auth object has ID token or not.
  90. BOOL _hasIDToken;
  91. // Fake data used to generate the expiration date of the access token.
  92. NSTimeInterval _accessTokenExpireTime;
  93. // Fake data used to generate the expiration date of the ID token.
  94. NSTimeInterval _idTokenExpireTime;
  95. // Fake data used to generate the additional token request parameters.
  96. NSDictionary *_additionalTokenRequestParameters;
  97. // The saved token fetch handler.
  98. OIDTokenCallback _tokenFetchHandler;
  99. // The saved token request.
  100. OIDTokenRequest *_tokenRequest;
  101. // All GIDAuthentication objects that are observed.
  102. NSMutableArray *_observedAuths;
  103. // Bitmask flags for observed changes, as specified in |ChangeType|.
  104. NSUInteger _changesObserved;
  105. // The fake system name used for testing.
  106. NSString *_fakeSystemName;
  107. }
  108. - (void)setUp {
  109. _hasIDToken = YES;
  110. _accessTokenExpireTime = kAccessTokenExpiresIn;
  111. _idTokenExpireTime = kExpireTime;
  112. _additionalTokenRequestParameters = nil;
  113. _tokenFetchHandler = nil;
  114. _tokenRequest = nil;
  115. [GULSwizzler swizzleClass:[OIDAuthorizationService class]
  116. selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:)
  117. isClassSelector:YES
  118. withBlock:^(id sender,
  119. OIDTokenRequest *request,
  120. OIDAuthorizationResponse *authorizationResponse,
  121. OIDTokenCallback callback) {
  122. XCTAssertNotNil(authorizationResponse.request.clientID);
  123. XCTAssertNotNil(authorizationResponse.request.configuration.tokenEndpoint);
  124. XCTAssertNil(self->_tokenFetchHandler); // only one on-going fetch allowed
  125. self->_tokenFetchHandler = [callback copy];
  126. self->_tokenRequest = [request copy];
  127. return nil;
  128. }];
  129. _observedAuths = [[NSMutableArray alloc] init];
  130. _changesObserved = 0;
  131. _fakeSystemName = kNewIOSName;
  132. #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
  133. [GULSwizzler swizzleClass:[UIDevice class]
  134. selector:@selector(systemName)
  135. isClassSelector:NO
  136. withBlock:^(id sender) { return self->_fakeSystemName; }];
  137. #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
  138. }
  139. - (void)tearDown {
  140. [GULSwizzler unswizzleClass:[OIDAuthorizationService class]
  141. selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:)
  142. isClassSelector:YES];
  143. #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
  144. [GULSwizzler unswizzleClass:[UIDevice class]
  145. selector:@selector(systemName)
  146. isClassSelector:NO];
  147. #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
  148. for (GIDAuthentication *auth in _observedAuths) {
  149. for (unsigned int i = 0; i < kNumberOfObservedProperties; ++i) {
  150. [auth removeObserver:self forKeyPath:kObservedProperties[i]];
  151. }
  152. }
  153. _observedAuths = nil;
  154. }
  155. #pragma mark - Tests
  156. - (void)testInitWithAuthState {
  157. OIDAuthState *authState = [OIDAuthState testInstance];
  158. GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState];
  159. XCTAssertEqualObjects(auth.clientID, authState.lastAuthorizationResponse.request.clientID);
  160. XCTAssertEqualObjects(auth.accessToken, authState.lastTokenResponse.accessToken);
  161. XCTAssertEqualObjects(auth.accessTokenExpirationDate,
  162. authState.lastTokenResponse.accessTokenExpirationDate);
  163. XCTAssertEqualObjects(auth.refreshToken, authState.refreshToken);
  164. XCTAssertEqualObjects(auth.idToken, authState.lastTokenResponse.idToken);
  165. OIDIDToken *idToken = [[OIDIDToken alloc]
  166. initWithIDTokenString:authState.lastTokenResponse.idToken];
  167. XCTAssertEqualObjects(auth.idTokenExpirationDate, [idToken expiresAt]);
  168. }
  169. - (void)testInitWithAuthStateNoIDToken {
  170. OIDAuthState *authState = [OIDAuthState testInstanceWithIDToken:nil];
  171. GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState];
  172. XCTAssertEqualObjects(auth.clientID, authState.lastAuthorizationResponse.request.clientID);
  173. XCTAssertEqualObjects(auth.accessToken, authState.lastTokenResponse.accessToken);
  174. XCTAssertEqualObjects(auth.accessTokenExpirationDate,
  175. authState.lastTokenResponse.accessTokenExpirationDate);
  176. XCTAssertEqualObjects(auth.refreshToken, authState.refreshToken);
  177. XCTAssertNil(auth.idToken);
  178. XCTAssertNil(auth.idTokenExpirationDate);
  179. }
  180. - (void)testAuthState {
  181. OIDAuthState *authState = [OIDAuthState testInstance];
  182. GIDAuthentication *auth = [[GIDAuthentication alloc] initWithAuthState:authState];
  183. OIDAuthState *authStateReturned = auth.authState;
  184. XCTAssertEqual(authState, authStateReturned);
  185. }
  186. - (void)testCoding {
  187. if (@available(iOS 11, macOS 10.13, *)) {
  188. GIDAuthentication *auth = [self auth];
  189. NSData *data = [NSKeyedArchiver archivedDataWithRootObject:auth requiringSecureCoding:YES error:nil];
  190. GIDAuthentication *newAuth = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDAuthentication class]
  191. fromData:data
  192. error:nil];
  193. XCTAssertEqualObjects(auth, newAuth);
  194. XCTAssertTrue([GIDAuthentication supportsSecureCoding]);
  195. } else {
  196. XCTSkip(@"Required API is not available for this test.");
  197. }
  198. }
  199. #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
  200. // Deprecated in iOS 13 and macOS 10.14
  201. - (void)testLegacyCoding {
  202. GIDAuthentication *auth = [self auth];
  203. NSData *data = [NSKeyedArchiver archivedDataWithRootObject:auth];
  204. GIDAuthentication *newAuth = [NSKeyedUnarchiver unarchiveObjectWithData:data];
  205. XCTAssertEqualObjects(auth, newAuth);
  206. XCTAssertTrue([GIDAuthentication supportsSecureCoding]);
  207. }
  208. #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
  209. - (void)testFetcherAuthorizer {
  210. // This is really hard to test without assuming how GTMAppAuthFetcherAuthorization works
  211. // internally, so let's just take the shortcut here by asserting we get a
  212. // GTMAppAuthFetcherAuthorization object.
  213. GIDAuthentication *auth = [self auth];
  214. #pragma clang diagnostic push
  215. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  216. id<GTMFetcherAuthorizationProtocol> fetcherAuthroizer = auth.fetcherAuthorizer;
  217. #pragma clang diagnostic pop
  218. XCTAssertTrue([fetcherAuthroizer isKindOfClass:[GTMAppAuthFetcherAuthorization class]]);
  219. XCTAssertTrue([fetcherAuthroizer canAuthorize]);
  220. }
  221. - (void)testDoWithFreshTokensWithBothExpired {
  222. // Both tokens expired 10 seconds ago.
  223. [self setExpireTimeForAccessToken:-10 IDToken:-10];
  224. [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
  225. }
  226. - (void)testDoWithFreshTokensWithAccessTokenExpired {
  227. // Access token expired 10 seconds ago while ID token to expire in 10 minutes.
  228. [self setExpireTimeForAccessToken:-10 IDToken:10 * 60];
  229. [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
  230. }
  231. - (void)testDoWithFreshTokensWithIDTokenToExpire {
  232. // Access token to expire in 10 minutes while ID token to expire in 10 seconds.
  233. [self setExpireTimeForAccessToken:10 * 60 IDToken:10];
  234. [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
  235. }
  236. - (void)testDoWithFreshTokensWithBothFresh {
  237. // Both tokens to expire in 10 minutes.
  238. [self setExpireTimeForAccessToken:10 * 60 IDToken:10 * 60];
  239. [self verifyTokensNotRefreshedWithMethod:@selector(doWithFreshTokens:)];
  240. }
  241. - (void)testDoWithFreshTokensWithAccessTokenExpiredAndNoIDToken {
  242. _hasIDToken = NO;
  243. [self setExpireTimeForAccessToken:-10 IDToken:10 * 60]; // access token expired 10 seconds ago
  244. [self verifyTokensRefreshedWithMethod:@selector(doWithFreshTokens:)];
  245. }
  246. - (void)testDoWithFreshTokensWithAccessTokenFreshAndNoIDToken {
  247. _hasIDToken = NO;
  248. [self setExpireTimeForAccessToken:10 * 60 IDToken:-10]; // access token to expire in 10 minutes
  249. [self verifyTokensNotRefreshedWithMethod:@selector(doWithFreshTokens:)];
  250. }
  251. - (void)testDoWithFreshTokensError {
  252. [self setTokensExpireTime:-10]; // expired 10 seconds ago
  253. GIDAuthentication *auth = [self observedAuth];
  254. XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
  255. [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
  256. [expectation fulfill];
  257. XCTAssertNil(authentication);
  258. XCTAssertNotNil(error);
  259. }];
  260. _tokenFetchHandler(nil, [self fakeError]);
  261. [self waitForExpectationsWithTimeout:1 handler:nil];
  262. [self assertOldTokensInAuth:auth];
  263. }
  264. - (void)testDoWithFreshTokensQueue {
  265. GIDAuthentication *auth = [self observedAuth];
  266. XCTestExpectation *firstExpectation =
  267. [self expectationWithDescription:@"First callback is called"];
  268. [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
  269. [firstExpectation fulfill];
  270. [self assertNewTokensInAuth:authentication];
  271. XCTAssertNil(error);
  272. }];
  273. XCTestExpectation *secondExpectation =
  274. [self expectationWithDescription:@"Second callback is called"];
  275. [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
  276. [secondExpectation fulfill];
  277. [self assertNewTokensInAuth:authentication];
  278. XCTAssertNil(error);
  279. }];
  280. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  281. [self waitForExpectationsWithTimeout:1 handler:nil];
  282. [self assertNewTokensInAuth:auth];
  283. }
  284. #pragma mark - EMM Support
  285. #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
  286. - (void)testEMMSupport {
  287. _additionalTokenRequestParameters = @{
  288. @"emm_support" : @"xyz",
  289. };
  290. GIDAuthentication *auth = [self auth];
  291. [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
  292. NSError * _Nullable error) {}];
  293. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  294. NSDictionary *expectedParameters = @{
  295. @"emm_support" : @"xyz",
  296. @"device_os" : [NSString stringWithFormat:@"%@ %@",
  297. _fakeSystemName, [UIDevice currentDevice].systemVersion],
  298. kSDKVersionLoggingParameter : GIDVersion(),
  299. kEnvironmentLoggingParameter : GIDEnvironment(),
  300. };
  301. XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
  302. expectedParameters);
  303. }
  304. - (void)testSystemNameNormalization {
  305. _fakeSystemName = kOldIOSName;
  306. _additionalTokenRequestParameters = @{
  307. @"emm_support" : @"xyz",
  308. };
  309. GIDAuthentication *auth = [self auth];
  310. [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
  311. NSError * _Nullable error) {}];
  312. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  313. NSDictionary *expectedParameters = @{
  314. @"emm_support" : @"xyz",
  315. @"device_os" : [NSString stringWithFormat:@"%@ %@",
  316. kNewIOSName, [UIDevice currentDevice].systemVersion],
  317. kSDKVersionLoggingParameter : GIDVersion(),
  318. kEnvironmentLoggingParameter : GIDEnvironment(),
  319. };
  320. XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
  321. expectedParameters);
  322. }
  323. - (void)testEMMPasscodeInfo {
  324. _additionalTokenRequestParameters = @{
  325. @"emm_support" : @"xyz",
  326. @"device_os" : @"old one",
  327. @"emm_passcode_info" : @"something",
  328. };
  329. GIDAuthentication *auth = [self auth];
  330. [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
  331. NSError * _Nullable error) {}];
  332. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  333. NSDictionary *expectedParameters = @{
  334. @"emm_support" : @"xyz",
  335. @"device_os" : [NSString stringWithFormat:@"%@ %@",
  336. _fakeSystemName, [UIDevice currentDevice].systemVersion],
  337. @"emm_passcode_info" : [GIDMDMPasscodeState passcodeState].info,
  338. kSDKVersionLoggingParameter : GIDVersion(),
  339. kEnvironmentLoggingParameter : GIDEnvironment(),
  340. };
  341. XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
  342. expectedParameters);
  343. }
  344. - (void)testEMMError {
  345. // Set expectations.
  346. NSDictionary *errorJSON = @{ @"error" : @"EMM Specific Error" };
  347. NSError *emmError = [NSError errorWithDomain:@"anydomain"
  348. code:12345
  349. userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
  350. id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
  351. [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
  352. __block void (^completion)(void);
  353. [[[mockEMMErrorHandler expect] andReturnValue:@YES]
  354. handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
  355. completion = arg;
  356. return YES;
  357. }]];
  358. // Start testing.
  359. _additionalTokenRequestParameters = @{
  360. @"emm_support" : @"xyz",
  361. };
  362. GIDAuthentication *auth = [self auth];
  363. XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"];
  364. notCalled.inverted = YES;
  365. XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"];
  366. [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
  367. [notCalled fulfill];
  368. [called fulfill];
  369. XCTAssertNil(authentication);
  370. XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain);
  371. XCTAssertEqual(error.code, kGIDSignInErrorCodeEMM);
  372. }];
  373. _tokenFetchHandler(nil, emmError);
  374. // Verify and clean up.
  375. [mockEMMErrorHandler verify];
  376. [mockEMMErrorHandler stopMocking];
  377. [self waitForExpectations:@[ notCalled ] timeout:1];
  378. completion();
  379. [self waitForExpectations:@[ called ] timeout:1];
  380. [self assertOldTokensInAuth:auth];
  381. }
  382. - (void)testNonEMMError {
  383. // Set expectations.
  384. NSDictionary *errorJSON = @{ @"error" : @"Not EMM Specific Error" };
  385. NSError *emmError = [NSError errorWithDomain:@"anydomain"
  386. code:12345
  387. userInfo:@{ OIDOAuthErrorResponseErrorKey : errorJSON }];
  388. id mockEMMErrorHandler = OCMStrictClassMock([GIDEMMErrorHandler class]);
  389. [[[mockEMMErrorHandler stub] andReturn:mockEMMErrorHandler] sharedInstance];
  390. __block void (^completion)(void);
  391. [[[mockEMMErrorHandler expect] andReturnValue:@NO]
  392. handleErrorFromResponse:errorJSON completion:[OCMArg checkWithBlock:^(id arg) {
  393. completion = arg;
  394. return YES;
  395. }]];
  396. // Start testing.
  397. _additionalTokenRequestParameters = @{
  398. @"emm_support" : @"xyz",
  399. };
  400. GIDAuthentication *auth = [self auth];
  401. XCTestExpectation *notCalled = [self expectationWithDescription:@"Callback is not called"];
  402. notCalled.inverted = YES;
  403. XCTestExpectation *called = [self expectationWithDescription:@"Callback is called"];
  404. [auth doWithFreshTokens:^(GIDAuthentication *authentication, NSError *error) {
  405. [notCalled fulfill];
  406. [called fulfill];
  407. XCTAssertNil(authentication);
  408. XCTAssertEqualObjects(error.domain, @"anydomain");
  409. XCTAssertEqual(error.code, 12345);
  410. }];
  411. _tokenFetchHandler(nil, emmError);
  412. // Verify and clean up.
  413. [mockEMMErrorHandler verify];
  414. [mockEMMErrorHandler stopMocking];
  415. [self waitForExpectations:@[ notCalled ] timeout:1];
  416. completion();
  417. [self waitForExpectations:@[ called ] timeout:1];
  418. [self assertOldTokensInAuth:auth];
  419. }
  420. - (void)testCodingPreserveEMMParameters {
  421. _additionalTokenRequestParameters = @{
  422. @"emm_support" : @"xyz",
  423. @"device_os" : @"old one",
  424. @"emm_passcode_info" : @"something",
  425. };
  426. NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[self auth]];
  427. GIDAuthentication *auth = [NSKeyedUnarchiver unarchiveObjectWithData:data];
  428. [auth doWithFreshTokens:^(GIDAuthentication * _Nonnull authentication,
  429. NSError * _Nullable error) {}];
  430. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  431. NSDictionary *expectedParameters = @{
  432. @"emm_support" : @"xyz",
  433. @"device_os" : [NSString stringWithFormat:@"%@ %@",
  434. [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion],
  435. @"emm_passcode_info" : [GIDMDMPasscodeState passcodeState].info,
  436. kSDKVersionLoggingParameter : GIDVersion(),
  437. kEnvironmentLoggingParameter : GIDEnvironment(),
  438. };
  439. XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
  440. expectedParameters);
  441. }
  442. #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
  443. #pragma mark - NSKeyValueObserving
  444. - (void)observeValueForKeyPath:(NSString *)keyPath
  445. ofObject:(id)object
  446. change:(NSDictionary *)change
  447. context:(void *)context {
  448. GIDAuthentication *auth = (GIDAuthentication *)object;
  449. ChangeType changeType;
  450. if ([keyPath isEqualToString:@"accessToken"]) {
  451. if (change[NSKeyValueChangeNotificationIsPriorKey]) {
  452. XCTAssertEqualObjects(auth.accessToken, kAccessToken);
  453. changeType = kChangeTypeAccessTokenPrior;
  454. } else {
  455. XCTAssertEqualObjects(auth.accessToken, kNewAccessToken);
  456. changeType = kChangeTypeAccessToken;
  457. }
  458. } else if ([keyPath isEqualToString:@"accessTokenExpirationDate"]) {
  459. if (change[NSKeyValueChangeNotificationIsPriorKey]) {
  460. [self assertDate:auth.accessTokenExpirationDate equalTime:_accessTokenExpireTime];
  461. changeType = kChangeTypeAccessTokenExpirationDatePrior;
  462. } else {
  463. [self assertDate:auth.accessTokenExpirationDate equalTime:kNewExpireTime];
  464. changeType = kChangeTypeAccessTokenExpirationDate;
  465. }
  466. } else if ([keyPath isEqualToString:@"idToken"]) {
  467. if (change[NSKeyValueChangeNotificationIsPriorKey]) {
  468. XCTAssertEqualObjects(auth.idToken, [self idToken]);
  469. changeType = kChangeTypeIDTokenPrior;
  470. } else {
  471. XCTAssertEqualObjects(auth.idToken, [self idTokenNew]);
  472. changeType = kChangeTypeIDToken;
  473. }
  474. } else if ([keyPath isEqualToString:@"idTokenExpirationDate"]) {
  475. if (change[NSKeyValueChangeNotificationIsPriorKey]) {
  476. if (_hasIDToken) {
  477. [self assertDate:auth.idTokenExpirationDate equalTime:_idTokenExpireTime];
  478. }
  479. changeType = kChangeTypeIDTokenExpirationDatePrior;
  480. } else {
  481. if (_hasIDToken) {
  482. [self assertDate:auth.idTokenExpirationDate equalTime:kNewExpireTime2];
  483. }
  484. changeType = kChangeTypeIDTokenExpirationDate;
  485. }
  486. } else {
  487. XCTFail(@"unexpected keyPath");
  488. return; // so compiler knows |changeType| is always assigned
  489. }
  490. NSUInteger changeMask = 1u << changeType;
  491. XCTAssertFalse(_changesObserved & changeMask); // each change type should only fire once
  492. _changesObserved |= changeMask;
  493. }
  494. #pragma mark - Helpers
  495. - (GIDAuthentication *)auth {
  496. NSString *idToken = [self idToken];
  497. NSNumber *accessTokenExpiresIn =
  498. @(_accessTokenExpireTime - [[NSDate date] timeIntervalSinceReferenceDate]);
  499. OIDTokenRequest *tokenRequest =
  500. [OIDTokenRequest testInstanceWithAdditionalParameters:_additionalTokenRequestParameters];
  501. OIDTokenResponse *tokenResponse =
  502. [OIDTokenResponse testInstanceWithIDToken:idToken
  503. accessToken:kAccessToken
  504. expiresIn:accessTokenExpiresIn
  505. tokenRequest:tokenRequest];
  506. return [[GIDAuthentication alloc]
  507. initWithAuthState:[OIDAuthState testInstanceWithTokenResponse:tokenResponse]];
  508. }
  509. - (NSString *)idTokenWithExpireTime:(NSTimeInterval)expireTime {
  510. if (!_hasIDToken) {
  511. return nil;
  512. }
  513. return [OIDTokenResponse idTokenWithSub:kUserID exp:@(expireTime + NSTimeIntervalSince1970)];
  514. }
  515. - (NSString *)idToken {
  516. return [self idTokenWithExpireTime:_idTokenExpireTime];
  517. }
  518. - (NSString *)idTokenNew {
  519. return [self idTokenWithExpireTime:kNewExpireTime2];
  520. }
  521. // Return the auth object that has certain property changes observed.
  522. - (GIDAuthentication *)observedAuth {
  523. GIDAuthentication *auth = [self auth];
  524. for (unsigned int i = 0; i < kNumberOfObservedProperties; ++i) {
  525. [auth addObserver:self
  526. forKeyPath:kObservedProperties[i]
  527. options:NSKeyValueObservingOptionPrior
  528. context:NULL];
  529. }
  530. [_observedAuths addObject:auth];
  531. return auth;
  532. }
  533. - (OIDTokenResponse *)tokenResponseWithNewTokens {
  534. NSNumber *expiresIn = @(kNewExpireTime - [NSDate timeIntervalSinceReferenceDate]);
  535. return [OIDTokenResponse testInstanceWithIDToken:(_hasIDToken ? [self idTokenNew] : nil)
  536. accessToken:kNewAccessToken
  537. expiresIn:expiresIn
  538. tokenRequest:_tokenRequest ?: nil];
  539. }
  540. - (NSError *)fakeError {
  541. return [NSError errorWithDomain:@"fake.domain" code:-1 userInfo:nil];
  542. }
  543. - (void)assertDate:(NSDate *)date equalTime:(NSTimeInterval)time {
  544. XCTAssertEqualWithAccuracy([date timeIntervalSinceReferenceDate], time, kTimeAccuracy);
  545. }
  546. - (void)assertOldAccessTokenInAuth:(GIDAuthentication *)auth {
  547. XCTAssertEqualObjects(auth.accessToken, kAccessToken);
  548. [self assertDate:auth.accessTokenExpirationDate equalTime:_accessTokenExpireTime];
  549. XCTAssertEqual(_changesObserved, kChangeNone);
  550. }
  551. - (void)assertNewAccessTokenInAuth:(GIDAuthentication *)auth {
  552. XCTAssertEqualObjects(auth.accessToken, kNewAccessToken);
  553. [self assertDate:auth.accessTokenExpirationDate equalTime:kNewExpireTime];
  554. XCTAssertEqual(_changesObserved, kChangeAll);
  555. }
  556. - (void)assertOldTokensInAuth:(GIDAuthentication *)auth {
  557. [self assertOldAccessTokenInAuth:auth];
  558. XCTAssertEqualObjects(auth.idToken, [self idToken]);
  559. if (_hasIDToken) {
  560. [self assertDate:auth.idTokenExpirationDate equalTime:_idTokenExpireTime];
  561. }
  562. }
  563. - (void)assertNewTokensInAuth:(GIDAuthentication *)auth {
  564. [self assertNewAccessTokenInAuth:auth];
  565. XCTAssertEqualObjects(auth.idToken, [self idTokenNew]);
  566. if (_hasIDToken) {
  567. [self assertDate:auth.idTokenExpirationDate equalTime:kNewExpireTime2];
  568. }
  569. }
  570. - (void)setTokensExpireTime:(NSTimeInterval)fromNow {
  571. [self setExpireTimeForAccessToken:fromNow IDToken:fromNow];
  572. }
  573. - (void)setExpireTimeForAccessToken:(NSTimeInterval)accessExpire IDToken:(NSTimeInterval)idExpire {
  574. _accessTokenExpireTime = [[NSDate date] timeIntervalSinceReferenceDate] + accessExpire;
  575. _idTokenExpireTime = [[NSDate date] timeIntervalSinceReferenceDate] + idExpire;
  576. }
  577. - (void)verifyTokensRefreshedWithMethod:(SEL)sel {
  578. GIDAuthentication *auth = [self observedAuth];
  579. XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
  580. #pragma clang diagnostic push
  581. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  582. // We know the method doesn't return anything, so there is no risk of leaking.
  583. [auth performSelector:sel withObject:^(GIDAuthentication *authentication, NSError *error) {
  584. #pragma clang diagnostic pop
  585. [expectation fulfill];
  586. [self assertNewTokensInAuth:authentication];
  587. XCTAssertNil(error);
  588. }];
  589. _tokenFetchHandler([self tokenResponseWithNewTokens], nil);
  590. [self waitForExpectationsWithTimeout:1 handler:nil];
  591. [self assertNewTokensInAuth:auth];
  592. }
  593. - (void)verifyTokensNotRefreshedWithMethod:(SEL)sel {
  594. GIDAuthentication *auth = [self observedAuth];
  595. XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"];
  596. #pragma clang diagnostic push
  597. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  598. // We know the method doesn't return anything, so there is no risk of leaking.
  599. [auth performSelector:sel withObject:^(GIDAuthentication *authentication, NSError *error) {
  600. #pragma clang diagnostic pop
  601. [expectation fulfill];
  602. [self assertOldTokensInAuth:authentication];
  603. XCTAssertNil(error);
  604. }];
  605. XCTAssertNil(_tokenFetchHandler);
  606. [self waitForExpectationsWithTimeout:1 handler:nil];
  607. [self assertOldTokensInAuth:auth];
  608. }
  609. @end