diff --git a/AppAuth.xcodeproj/project.pbxproj b/AppAuth.xcodeproj/project.pbxproj index f2f828306..a0986c6c2 100644 --- a/AppAuth.xcodeproj/project.pbxproj +++ b/AppAuth.xcodeproj/project.pbxproj @@ -381,6 +381,12 @@ 34A663321E871DD40060B664 /* OIDIDToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A663271E871DD40060B664 /* OIDIDToken.m */; }; 34A663331E871DD40060B664 /* OIDIDToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A663271E871DD40060B664 /* OIDIDToken.m */; }; 34A663341E871DD40060B664 /* OIDIDToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A663271E871DD40060B664 /* OIDIDToken.m */; }; + 34A6638B1E8865090060B664 /* OIDRPProfileCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A6638A1E8865090060B664 /* OIDRPProfileCode.m */; }; + 34A6638C1E8865090060B664 /* OIDRPProfileCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A6638A1E8865090060B664 /* OIDRPProfileCode.m */; }; + 34A6638D1E8865090060B664 /* OIDRPProfileCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A6638A1E8865090060B664 /* OIDRPProfileCode.m */; }; + 34A6638E1E8865090060B664 /* OIDRPProfileCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A6638A1E8865090060B664 /* OIDRPProfileCode.m */; }; + 34A6638F1E8865090060B664 /* OIDRPProfileCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A6638A1E8865090060B664 /* OIDRPProfileCode.m */; }; + 34A663901E8865090060B664 /* OIDRPProfileCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A6638A1E8865090060B664 /* OIDRPProfileCode.m */; }; 34D5EC451E6D1AD900814354 /* OIDSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D5EC441E6D1AD900814354 /* OIDSwiftTests.swift */; }; 34FEA6AE1DB6E083005C9212 /* OIDLoopbackHTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 34FEA6AC1DB6E083005C9212 /* OIDLoopbackHTTPServer.h */; }; 34FEA6AF1DB6E083005C9212 /* OIDLoopbackHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FEA6AD1DB6E083005C9212 /* OIDLoopbackHTTPServer.m */; }; @@ -557,6 +563,8 @@ 347423F61E7F4B5600D3E6D6 /* libAppAuth-watchOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libAppAuth-watchOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 34A663261E871DD40060B664 /* OIDIDToken.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OIDIDToken.h; sourceTree = ""; }; 34A663271E871DD40060B664 /* OIDIDToken.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OIDIDToken.m; sourceTree = ""; }; + 34A6638A1E8865090060B664 /* OIDRPProfileCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OIDRPProfileCode.m; sourceTree = ""; }; + 34A663911E886AED0060B664 /* OIDRPProfileCode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OIDRPProfileCode.h; sourceTree = ""; }; 34D5EC431E6D1AD900814354 /* OIDAppAuthTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OIDAppAuthTests-Bridging-Header.h"; sourceTree = ""; }; 34D5EC441E6D1AD900814354 /* OIDSwiftTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OIDSwiftTests.swift; sourceTree = ""; }; 34FEA6AC1DB6E083005C9212 /* OIDLoopbackHTTPServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OIDLoopbackHTTPServer.h; sourceTree = ""; }; @@ -824,6 +832,8 @@ 60140F821DE43BAF00DA0DC3 /* OIDRegistrationRequestTests.m */, 60140F841DE43C8C00DA0DC3 /* OIDRegistrationResponseTests.h */, 60140F851DE43CC700DA0DC3 /* OIDRegistrationResponseTests.m */, + 34A663911E886AED0060B664 /* OIDRPProfileCode.h */, + 34A6638A1E8865090060B664 /* OIDRPProfileCode.m */, 34D5EC441E6D1AD900814354 /* OIDSwiftTests.swift */, 34D5EC431E6D1AD900814354 /* OIDAppAuthTests-Bridging-Header.h */, 0396974C1FA827AD003D1FB2 /* OIDURLSessionProviderTests.m */, @@ -1504,6 +1514,7 @@ 3417421E1C5D82D3000EF209 /* OIDServiceDiscoveryTests.m in Sources */, 3417421F1C5D82D3000EF209 /* OIDTokenRequestTests.m in Sources */, 341742181C5D82D3000EF209 /* OIDAuthorizationResponseTests.m in Sources */, + 34A6638B1E8865090060B664 /* OIDRPProfileCode.m in Sources */, 341742171C5D82D3000EF209 /* OIDAuthorizationRequestTests.m in Sources */, 0396974D1FA827AD003D1FB2 /* OIDURLSessionProviderTests.m in Sources */, 3417421A1C5D82D3000EF209 /* OIDGrantTypesTests.m in Sources */, @@ -1529,6 +1540,7 @@ 341AA50D1E7F3A9B00FCA5C6 /* OIDTokenRequestTests.m in Sources */, 341AA5091E7F3A9B00FCA5C6 /* OIDResponseTypesTests.m in Sources */, 341AA4D91E7F393500FCA5C6 /* OIDAuthorizationRequestTests.m in Sources */, + 34A6638C1E8865090060B664 /* OIDRPProfileCode.m in Sources */, 341AA5101E7F3A9B00FCA5C6 /* OIDRegistrationRequestTests.m in Sources */, 341AA5111E7F3A9B00FCA5C6 /* OIDRegistrationResponseTests.m in Sources */, 341AA5081E7F3A9B00FCA5C6 /* OIDGrantTypesTests.m in Sources */, @@ -1549,6 +1561,7 @@ 341AA5001E7F3A9400FCA5C6 /* OIDTokenRequestTests.m in Sources */, 341AA4FC1E7F3A9400FCA5C6 /* OIDResponseTypesTests.m in Sources */, 341AA4F81E7F3A3000FCA5C6 /* OIDAuthorizationRequestTests.m in Sources */, + 34A6638D1E8865090060B664 /* OIDRPProfileCode.m in Sources */, 341AA5031E7F3A9400FCA5C6 /* OIDRegistrationRequestTests.m in Sources */, 341AA5041E7F3A9400FCA5C6 /* OIDRegistrationResponseTests.m in Sources */, 341AA4FB1E7F3A9400FCA5C6 /* OIDGrantTypesTests.m in Sources */, @@ -1634,6 +1647,7 @@ 343AAA7F1E8346B400F9D36E /* OIDRegistrationRequestTests.m in Sources */, 343AAA731E8346B400F9D36E /* OIDAuthorizationRequestTests.m in Sources */, 343AAA761E8346B400F9D36E /* OIDGrantTypesTests.m in Sources */, + 34A6638E1E8865090060B664 /* OIDRPProfileCode.m in Sources */, 343AAA741E8346B400F9D36E /* OIDAuthorizationResponseTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1713,6 +1727,7 @@ 343AAB8B1E8349CE00F9D36E /* OIDRegistrationRequestTests.m in Sources */, 343AAB7F1E8349CE00F9D36E /* OIDAuthorizationRequestTests.m in Sources */, 343AAB821E8349CE00F9D36E /* OIDGrantTypesTests.m in Sources */, + 34A6638F1E8865090060B664 /* OIDRPProfileCode.m in Sources */, 343AAB801E8349CE00F9D36E /* OIDAuthorizationResponseTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1768,6 +1783,7 @@ 343AAB991E8349CF00F9D36E /* OIDRegistrationRequestTests.m in Sources */, 343AAB8D1E8349CF00F9D36E /* OIDAuthorizationRequestTests.m in Sources */, 343AAB901E8349CF00F9D36E /* OIDGrantTypesTests.m in Sources */, + 34A663901E8865090060B664 /* OIDRPProfileCode.m in Sources */, 343AAB8E1E8349CF00F9D36E /* OIDAuthorizationResponseTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/UnitTests/OIDRPProfileCode.h b/UnitTests/OIDRPProfileCode.h new file mode 100644 index 000000000..98fa3f475 --- /dev/null +++ b/UnitTests/OIDRPProfileCode.h @@ -0,0 +1,40 @@ +/*! @file OIDRPProfileCode.h + @brief AppAuth iOS SDK + @copyright + Copyright 2017 Google Inc. 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 "OIDAuthorizationUICoordinator.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OIDAuthorizationUICoordinatorNonInteractive : NSObject { + // private variables + NSURLSession *_urlSession; + __weak id _session; +} +@end + +@interface OIDRPProfileCode : XCTestCase { + // private variables + OIDAuthorizationUICoordinatorNonInteractive *_coordinator; + FILE * _logFile; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/UnitTests/OIDRPProfileCode.m b/UnitTests/OIDRPProfileCode.m new file mode 100644 index 000000000..7dd0dc4c4 --- /dev/null +++ b/UnitTests/OIDRPProfileCode.m @@ -0,0 +1,566 @@ +/*! @file OIDRPProfileCode.m + @brief AppAuth iOS SDK + @copyright + Copyright 2017 Google Inc. 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 "OIDRPProfileCode.h" + +#import "OIDAuthorizationRequest.h" +#import "OIDAuthorizationResponse.h" +#import "OIDAuthorizationService.h" +#import "OIDAuthState.h" +#import "OIDIDToken.h" +#import "OIDRegistrationRequest.h" +#import "OIDRegistrationResponse.h" +#import "OIDScopes.h" +#import "OIDServiceConfiguration.h" +#import "OIDServiceDiscovery.h" +#import "OIDTokenRequest.h" +#import "OIDTokenResponse.h" + +static NSString *const kRedirectURI = @"com.example.app:/oauth2redirect/example-provider"; + +// Open ID RP Certification test server http://openid.net/certification/rp_testing/ +static NSString *const kTestURIBase = + @"https://rp.certification.openid.net:8080/appauth-ios-macos/"; + +/*! @brief A UI Coordinator for testing, has no user agent and doesn't support user interaction. + Simply performs the authorization request as a GET request, and looks for a redirect in + the response. + */ +@interface OIDAuthorizationUICoordinatorNonInteractive () +@end + +@implementation OIDAuthorizationUICoordinatorNonInteractive + +- (BOOL)presentAuthorizationRequest:(nonnull OIDAuthorizationRequest *)request + session:(nonnull id)session { + _session = session; + NSURL *requestURL = [request authorizationRequestURL]; + NSMutableURLRequest *URLRequest = [[NSURLRequest requestWithURL:requestURL] mutableCopy]; + NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration]; + _urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; + [[_urlSession dataTaskWithRequest:URLRequest + completionHandler:^(NSData *_Nullable data, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + NSDictionary* headers = [(NSHTTPURLResponse *)response allHeaderFields]; + NSString *location = [headers objectForKey:@"Location"]; + NSURL *url = [NSURL URLWithString:location]; + [session resumeAuthorizationFlowWithURL:url]; + }] resume]; + + return YES; +} + +- (void)dismissAuthorizationAnimated:(BOOL)animated completion:(void (^)(void))completion { + if (completion) completion(); +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + willPerformHTTPRedirection:(NSHTTPURLResponse *)response + newRequest:(NSURLRequest *)request + completionHandler:(void (^)(NSURLRequest *))completionHandler { + // Disables HTTP redirection in the NSURLSession + completionHandler(NULL); +} +@end + +@interface OIDAuthorizationFlowSessionImplementation : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithRequest:(OIDAuthorizationRequest *)request + NS_DESIGNATED_INITIALIZER; + +@end + +@interface OIDRPProfileCode () + +typedef void (^PostRegistrationCallback)(OIDServiceConfiguration *configuration, + OIDRegistrationResponse *registrationResponse, + NSError *error + ); + +typedef void (^CodeExchangeCompletion)(OIDAuthorizationResponse *_Nullable authorizationResponse, + OIDTokenResponse *_Nullable tokenResponse, + NSError *tokenError + ); + +typedef void (^UserInfoCompletion)(OIDAuthState *_Nullable authState, + NSDictionary *_Nullable userInfoDictionary, + NSError *userInfo + ); + +@end + +@implementation OIDRPProfileCode + +- (void)setUp { + [super setUp]; +} + +- (void)tearDown { + [super tearDown]; + + [self endCertificationTest]; +} + +/*! @brief Performs client registration. + @param issuer The issuer to register the client with. + @param callback Completion block. + */ +- (void)doRegistrationWithIssuer:(NSURL *)issuer callback:(PostRegistrationCallback)callback { + NSURL *redirectURI = [NSURL URLWithString:kRedirectURI]; + + // discovers endpoints + [OIDAuthorizationService discoverServiceConfigurationForIssuer:issuer + completion:^(OIDServiceConfiguration *_Nullable configuration, NSError *_Nullable error) { + + if (!configuration) { + callback(nil, nil, error); + return; + } + + OIDRegistrationRequest *request = + [[OIDRegistrationRequest alloc] initWithConfiguration:configuration + redirectURIs:@[ redirectURI ] + responseTypes:nil + grantTypes:nil + subjectType:nil + tokenEndpointAuthMethod:@"client_secret_basic" + additionalParameters:@{@"id_token_signed_response_alg": + @"none", + @"contacts": + @"appauth@wdenniss.com"}]; + + [self certificationLog:@"Registration request: %@", request]; + + // performs registration request + [OIDAuthorizationService performRegistrationRequest:request + completion:^(OIDRegistrationResponse *_Nullable regResp, NSError *_Nullable error) { + if (regResp) { + callback(configuration, regResp, nil); + } else { + callback(nil, nil, error); + } + }]; + }]; +} + +/*! @brief Performs the code flow on the test server. + @param test The test ID used to configure the test server. + @param completion Completion block. + */ +- (void)codeFlowWithExchangeForTest:(NSString *)test completion:(CodeExchangeCompletion)completion { + [self codeFlowWithExchangeForTest:test scope:@[ OIDScopeOpenID ] completion:completion]; +} + +/*! @brief Performs the code flow on the test server. + @param test The test ID used to configure the test server. + @param scope Scope to use in the authorization request. + @param completion Completion block. + */ +- (void)codeFlowWithExchangeForTest:(NSString *)test + scope:(NSArray *)scope + completion:(CodeExchangeCompletion)completion { + + NSString *issuerString = [kTestURIBase stringByAppendingString:test]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Discovery and registration should complete."]; + XCTestExpectation *auth_complete = + [self expectationWithDescription:@"Authorization should complete."]; + XCTestExpectation *token_exchange = + [self expectationWithDescription:@"Token Exchange should complete."]; + + NSURL *issuer = [NSURL URLWithString:issuerString]; + + [self doRegistrationWithIssuer:issuer callback:^(OIDServiceConfiguration *configuration, + OIDRegistrationResponse *registrationResponse, + NSError *error) { + [expectation fulfill]; + XCTAssertNotNil(configuration); + XCTAssertNotNil(registrationResponse); + XCTAssertNil(error); + + if (error) { + return; + } + + NSURL *redirectURI = [NSURL URLWithString:kRedirectURI]; + // builds authentication request + OIDAuthorizationRequest *request = + [[OIDAuthorizationRequest alloc] initWithConfiguration:configuration + clientId:registrationResponse.clientID + clientSecret:registrationResponse.clientSecret + scopes:scope + redirectURL:redirectURI + responseType:OIDResponseTypeCode + additionalParameters:nil]; + + _coordinator = [[OIDAuthorizationUICoordinatorNonInteractive alloc] init]; + + [self certificationLog:@"Initiating authorization request: %@", + [request authorizationRequestURL]]; + + [OIDAuthorizationService + presentAuthorizationRequest:request + UICoordinator:_coordinator + callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse, + NSError *error) { + [auth_complete fulfill]; + XCTAssertNotNil(authorizationResponse); + XCTAssertNil(error); + + OIDTokenRequest *tokenExchangeRequest = [authorizationResponse tokenExchangeRequest]; + [OIDAuthorizationService + performTokenRequest:tokenExchangeRequest + originalAuthorizationResponse:authorizationResponse + callback:^(OIDTokenResponse *_Nullable tokenResponse, + NSError *_Nullable tokenError) { + + [token_exchange fulfill]; + + completion(authorizationResponse, tokenResponse, tokenError); + }]; + }]; + + }]; + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +/*! @brief Performs the code flow on the test server and expects a successful result. + @param test The test ID. + */ +- (void)codeFlowWithExchangeExpectSuccessForTest:(NSString *)test { + [self codeFlowWithExchangeForTest:test + completion:^(OIDAuthorizationResponse * _Nullable authorizationResponse, + OIDTokenResponse * _Nullable tokenResponse, + NSError *tokenError) { + XCTAssertNotNil(tokenResponse); + XCTAssertNil(tokenError); + // testRP_id_token_sig_none + XCTAssertNotNil(tokenResponse.idToken); + + [self certificationLog:@"PASS: Got token response: %@", tokenResponse]; + }]; +} + +- (void)testRP_response_type_code { + NSString *testName = @"rp-response_type-code"; + [self startCertificationTest:testName]; + [self codeFlowWithExchangeExpectSuccessForTest:testName]; +} + +- (void)testRP_id_token_sig_none { + NSString *testName = @"rp-id_token-sig-none"; + [self startCertificationTest:testName]; + [self codeFlowWithExchangeExpectSuccessForTest:testName]; +} + +- (void)testRP_token_endpoint_client_secret_basic { + NSString *testName = @"rp-token_endpoint-client_secret_basic"; + [self startCertificationTest:testName]; + + [self codeFlowWithExchangeExpectSuccessForTest:testName]; +} + +/*! @brief Performs the code flow on the test server and expects a failure result. + @param test The test ID. + */ +- (void)codeFlowWithExchangeExpectFailForTest:(NSString *)test { + [self codeFlowWithExchangeForTest:test + completion:^(OIDAuthorizationResponse * _Nullable authorizationResponse, + OIDTokenResponse * _Nullable tokenResponse, + NSError *tokenError) { + XCTAssertNil(tokenResponse); + XCTAssertNotNil(tokenError); + + if (tokenError) { + [self certificationLog:@"PASS: Token exchange failed with %@", tokenError]; + } + }]; +} + +- (void)testRP_id_token_aud { + NSString *testName = @"rp-id_token-aud"; + [self startCertificationTest:testName]; + [self codeFlowWithExchangeExpectFailForTest:testName]; +} + +- (void)testRP_id_token_iat { + NSString *testName = @"rp-id_token-iat"; + [self startCertificationTest:testName]; + [self codeFlowWithExchangeExpectFailForTest:testName]; +} + +- (void)testRP_id_token_sub { + NSString *testName = @"rp-id_token-sub"; + [self startCertificationTest:testName]; + [self codeFlowWithExchangeExpectFailForTest:testName]; +} + +- (void)testRP_id_token_issuer_mismatch { + NSString *testName = @"rp-id_token-issuer-mismatch"; + [self startCertificationTest:testName]; + [self codeFlowWithExchangeExpectFailForTest:testName]; +} + +- (void)testRP_nonce_invalid { + NSString *testName = @"rp-nonce-invalid"; + [self startCertificationTest:testName]; + [self codeFlowWithExchangeExpectFailForTest:testName]; +} + +/*! @brief Makes a UserInfo request then calls completion block. + @param test The test ID used to configure the test server. + @param completion Completion block. + */ +- (void)codeFlowThenUserInfoForTest:(NSString *)test completion:(UserInfoCompletion)completion { + + // Adds another expectation that codeFlowWithExchangeForTest will wait for. + XCTestExpectation *userinfoExpectation = + [self expectationWithDescription:@"Userinfo response."]; + + NSArray *scope = + @[ OIDScopeOpenID, OIDScopeProfile, OIDScopeEmail, OIDScopeAddress, OIDScopePhone ]; + [self codeFlowWithExchangeForTest:test + scope:scope + completion:^(OIDAuthorizationResponse * _Nullable authorizationResponse, + OIDTokenResponse * _Nullable tokenResponse, + NSError *tokenError) { + XCTAssertNotNil(tokenResponse); + XCTAssertNil(tokenError); + + [self certificationLog:@"Got access token: %@", tokenResponse.accessToken]; + + OIDAuthState *authState = + [[OIDAuthState alloc] initWithAuthorizationResponse:authorizationResponse + tokenResponse:tokenResponse]; + + NSURL *userinfoEndpoint = + authState.lastAuthorizationResponse.request.configuration.discoveryDocument.userinfoEndpoint; + XCTAssertNotNil(userinfoEndpoint); + + [authState performActionWithFreshTokens:^(NSString *_Nonnull accessToken, + NSString *_Nonnull idToken, + NSError *_Nullable error) { + XCTAssertNil(error); + + // creates request to the userinfo endpoint, with access token in the Authorization header + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:userinfoEndpoint]; + NSString *authorizationHeaderValue = [NSString stringWithFormat:@"Bearer %@", accessToken]; + [request addValue:authorizationHeaderValue forHTTPHeaderField:@"Authorization"]; + + NSURLSessionConfiguration *configuration = + [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration + delegate:nil + delegateQueue:nil]; + + [self certificationLog:@"Performing UserInfo request to: %@", userinfoEndpoint]; + [self certificationLog:@"- Headers: %@", request.allHTTPHeaderFields]; + + // performs HTTP request + NSURLSessionDataTask *postDataTask = + [session dataTaskWithRequest:request + completionHandler:^(NSData *_Nullable data, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + dispatch_async(dispatch_get_main_queue(), ^() { + [userinfoExpectation fulfill]; + XCTAssertNil(error); + XCTAssert([response isKindOfClass:[NSHTTPURLResponse class]]); + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + XCTAssert( (int)httpResponse.statusCode == 200); + id jsonDictionaryOrArray = + [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; + completion(authState, jsonDictionaryOrArray, error); + }); + }]; + + [postDataTask resume]; + }]; + }]; +} + +- (void)testRP_userinfo_bearer_header { + NSString *testName = @"rp-userinfo-bearer-header"; + [self startCertificationTest:testName]; + [self codeFlowThenUserInfoForTest:testName + completion:^(OIDAuthState * _Nullable authState, + NSDictionary * _Nullable userInfoDictionary, + NSError *userInfoError) { + XCTAssertNotNil(userInfoDictionary); + [self certificationLog:@"PASS: User info dictionary: %@", userInfoDictionary]; + }]; +} + +- (void)testRP_userinfo_bad_sub_claim { + NSString *testName = @"rp-userinfo-bad-sub-claim"; + [self startCertificationTest:testName]; + + [self codeFlowThenUserInfoForTest:testName + completion:^(OIDAuthState * _Nullable authState, + NSDictionary * _Nullable userInfoDictionary, + NSError *userInfo) { + + NSString *sub = userInfoDictionary[@"sub"]; + XCTAssertNotNil(sub); + OIDIDToken *idToken = + [[OIDIDToken alloc] initWithIDTokenString:authState.lastTokenResponse.idToken]; + XCTAssertNotNil(idToken); + XCTAssertNotEqual(sub, idToken.subject); + + if (![sub isEqualToString:idToken.subject]) { + [self certificationLog:@"PASS: UserInfo subject '%@' does not match id token subject '%@'", + sub, + idToken.subject]; + } + }]; +} + +- (void)testRP_scope_userinfo_claims { + NSString *testName = @"rp-scope-userinfo-claims"; + [self startCertificationTest:testName]; + [self codeFlowThenUserInfoForTest:testName + completion:^(OIDAuthState * _Nullable authState, + NSDictionary * _Nullable userInfoDictionary, + NSError *userInfo) { + + [self certificationLog:@"User info dictionary: %@", userInfoDictionary]; + + XCTAssertNotNil(userInfoDictionary[@"name"]); + XCTAssertNotNil(userInfoDictionary[@"email"]); + XCTAssertNotNil(userInfoDictionary[@"email_verified"]); + XCTAssertNotNil(userInfoDictionary[@"address"]); + XCTAssertNotNil(userInfoDictionary[@"phone_number"]); + if (userInfoDictionary[@"name"] + && userInfoDictionary[@"email"] + && userInfoDictionary[@"email_verified"] + && userInfoDictionary[@"address"] + && userInfoDictionary[@"phone_number"]) { + [self certificationLog:@"PASS: name, email, email_verified, address, phone_number " + "claims present"]; + } + }]; +} + +- (void)testRP_id_token_kid_absent_single_jwks { + NSString *testName = @"rp-id_token-kid-absent-single-jwks"; + [self skippedTest:testName]; +} +- (void)testRP_id_token_kid_absent_multiple_jwks { + NSString *testName = @"rp-id_token-kid-absent-multiple-jwks"; + [self skippedTest:testName]; +} +- (void)testRP_rp_id_token_bad_sig_rs256 { + NSString *testName = @"rp-id_token-bad-sig-rs256"; + [self skippedTest:testName]; +} + +- (void)testRP_id_token_sig_rs256 { + NSString *testName = @"rp-id_token-sig-rs256"; + [self skippedTest:testName]; +} + +- (void)skippedTest:(NSString *)testName { + [self startCertificationTest:testName]; + + NSString *issuerString = [kTestURIBase stringByAppendingString:testName]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Discovery and registration should complete."]; + + NSURL *issuer = [NSURL URLWithString:issuerString]; + + [self doRegistrationWithIssuer:issuer callback:^(OIDServiceConfiguration *configuration, + OIDRegistrationResponse *registrationResponse, + NSError *error) { + [expectation fulfill]; + + XCTAssertNil(registrationResponse); + XCTAssertNotNil(error); + + if (error) { + [self certificationLog:@"Registration error: %@", error]; + [self certificationLog:@"SKIP. With id_token_signed_response_alg set to `none` in registration, error recieved and test skipped."]; + } + + }]; + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + + +/*! @brief Creates a log file to record the certification logs. + @param testName The test ID used to configure the test server. + */ +- (void)startCertificationTest:(NSString *)testName { + if (_logFile) { + [self endCertificationTest]; + } + + NSString* filename = [NSString stringWithFormat:@"%@.txt", testName]; + + NSString *documentsDirectory = + NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0]; + NSString *codeDir = [documentsDirectory stringByAppendingPathComponent:@"code"]; + [[NSFileManager defaultManager] createDirectoryAtPath:codeDir + withIntermediateDirectories:NO + attributes:nil + error:nil]; + NSString *pathForLog = [codeDir stringByAppendingPathComponent:filename]; + + NSLog(@"Writing logs for test %@ to %@", testName, pathForLog); + _logFile = fopen([pathForLog cStringUsingEncoding:NSASCIIStringEncoding], "w"); + NSAssert(_logFile, @"Unable to create log file"); + + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss"; + NSString *dateString = [dateFormatter stringFromDate:[NSDate date]]; + [self certificationLog:@"# Starting test `%@` at %@ for AppAuth for iOS and macOS", + testName, + dateString]; +} + +/*! @brief Logs string to the certification log. + */ +- (void)certificationLog:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2) { + NSAssert(_logFile, @"No active log"); + + // Gets log message as a string. + va_list argp; + va_start(argp, format); + NSString *log = [[NSString alloc] initWithFormat:format arguments:argp]; + va_end(argp); + + // Logs to file. + fprintf(_logFile, "%s\n", [log UTF8String]); +} + +/*! @brief Closes the certification log file. + */ +- (void)endCertificationTest { + // Adds a newline. + [self certificationLog:@""]; + fclose(_logFile); + _logFile = NULL; +} + +@end +