From 11e680b2ef9dddfc23a95b27373f5e2bcb999eb5 Mon Sep 17 00:00:00 2001 From: William Denniss Date: Fri, 5 Jan 2018 10:34:45 -0800 Subject: [PATCH 1/3] Add nonce to AuthorizationRequest --- Source/OIDAuthState.m | 4 +- Source/OIDAuthorizationRequest.h | 49 +++++++++++++++++++++++- Source/OIDAuthorizationRequest.m | 40 +++++++++++++++++++ Source/OIDAuthorizationService.h | 9 +++++ Source/OIDAuthorizationService.m | 9 +++++ UnitTests/OIDAuthorizationRequestTests.m | 11 ++++++ 6 files changed, 119 insertions(+), 3 deletions(-) diff --git a/Source/OIDAuthState.m b/Source/OIDAuthState.m index 4270a066d..47dca0e02 100644 --- a/Source/OIDAuthState.m +++ b/Source/OIDAuthState.m @@ -125,7 +125,8 @@ @implementation OIDAuthState OIDTokenRequest *tokenExchangeRequest = [authorizationResponse tokenExchangeRequest]; [OIDAuthorizationService - performTokenRequest:tokenExchangeRequest + performTokenRequest:tokenExchangeRequest + originalAuthorizationResponse:authorizationResponse callback:^(OIDTokenResponse *_Nullable tokenResponse, NSError *_Nullable tokenError) { OIDAuthState *authState; @@ -473,6 +474,7 @@ - (void)performActionWithFreshTokens:(OIDAuthStateAction)action OIDTokenRequest *tokenRefreshRequest = [self tokenRefreshRequestWithAdditionalParameters:additionalParameters]; [OIDAuthorizationService performTokenRequest:tokenRefreshRequest + originalAuthorizationResponse:_lastAuthorizationResponse callback:^(OIDTokenResponse *_Nullable response, NSError *_Nullable error) { dispatch_async(dispatch_get_main_queue(), ^() { diff --git a/Source/OIDAuthorizationRequest.h b/Source/OIDAuthorizationRequest.h index c9611ea15..5c30f2bf6 100644 --- a/Source/OIDAuthorizationRequest.h +++ b/Source/OIDAuthorizationRequest.h @@ -47,6 +47,7 @@ extern NSString *const OIDOAuthorizationRequestCodeChallengeMethodS256; NSString *_scope; NSURL *_redirectURL; NSString *_state; + NSString *_nonce; NSString *_codeVerifier; NSString *_codeChallenge; NSString *_codeChallengeMethod; @@ -108,6 +109,17 @@ extern NSString *const OIDOAuthorizationRequestCodeChallengeMethodS256; */ @property(nonatomic, readonly, nullable) NSString *state; +/*! @brief String value used to associate a Client session with an ID Token, and to mitigate replay + attacks. The value is passed through unmodified from the Authentication Request to the ID + Token. Sufficient entropy MUST be present in the nonce values used to prevent attackers from + guessing values. + @remarks nonce + @discussion If this value is not explicitly set, this library will automatically add nonce and + perform appropriate validation of the nonce in the ID Token. + @see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + */ +@property(nonatomic, readonly, nullable) NSString *nonce; + /*! @brief The PKCE code verifier. @remarks code_verifier @discussion The code verifier itself is not included in the authorization request that is sent @@ -160,8 +172,8 @@ extern NSString *const OIDOAuthorizationRequestCodeChallengeMethodS256; responseType:(NSString *)responseType additionalParameters:(nullable NSDictionary *)additionalParameters; -/*! @brief Creates an authorization request with opinionated defaults (a secure @c state, and - PKCE with S256 as the @c code_challenge_method). +/*! @brief Creates an authorization request with opinionated defaults (a secure @c state, @c nonce, + and PKCE with S256 as the @c code_challenge_method). @param configuration The service's configuration. @param clientID The client identifier. @param clientSecret The client secret. @@ -181,6 +193,37 @@ extern NSString *const OIDOAuthorizationRequestCodeChallengeMethodS256; responseType:(NSString *)responseType additionalParameters:(nullable NSDictionary *)additionalParameters; +/*! @brief Deprecated, replaced with @c OIDAuthState.initWithConfiguration:clientId:clientSecret:scope:redirectURL:responseType:state:nonce:codeVerifier:codeChallenge:codeChallengeMethod:additionalParameters:. + @param configuration The service's configuration. + @param clientID The client identifier. + @param scope A scope string per the OAuth2 spec (a space-delimited set of scopes). + @param redirectURL The client's redirect URI. + @param responseType The expected response type. + @param state An opaque value used by the client to maintain state between the request and + callback. + @param codeVerifier The PKCE code verifier. See @c OIDAuthorizationRequest.generateCodeVerifier. + @param codeChallenge The PKCE code challenge, calculated from the code verifier such as with + @c OIDAuthorizationRequest.codeChallengeS256ForVerifier:. + @param codeChallengeMethod The PKCE code challenge method. + ::OIDOAuthorizationRequestCodeChallengeMethodS256 when + @c OIDAuthorizationRequest.codeChallengeS256ForVerifier: is used to create the code + challenge. + @param additionalParameters The client's additional authorization parameters. + */ +- (instancetype) + initWithConfiguration:(OIDServiceConfiguration *)configuration + clientId:(NSString *)clientID + clientSecret:(nullable NSString *)clientSecret + scope:(nullable NSString *)scope + redirectURL:(nullable NSURL *)redirectURL + responseType:(NSString *)responseType + state:(nullable NSString *)state + codeVerifier:(nullable NSString *)codeVerifier + codeChallenge:(nullable NSString *)codeChallenge + codeChallengeMethod:(nullable NSString *)codeChallengeMethod + additionalParameters:(nullable NSDictionary *)additionalParameters +__deprecated_msg("Replaced with OIDAuthState.initWithConfiguration:clientId:clientSecret:scope:redirectURL:responseType:state:nonce:codeVerifier:codeChallenge:codeChallengeMethod:additionalParameters:"); + /*! @brief Designated initializer. @param configuration The service's configuration. @param clientID The client identifier. @@ -189,6 +232,7 @@ extern NSString *const OIDOAuthorizationRequestCodeChallengeMethodS256; @param responseType The expected response type. @param state An opaque value used by the client to maintain state between the request and callback. + @param nonce String value used to associate a Client session with an ID Token. @param codeVerifier The PKCE code verifier. See @c OIDAuthorizationRequest.generateCodeVerifier. @param codeChallenge The PKCE code challenge, calculated from the code verifier such as with @c OIDAuthorizationRequest.codeChallengeS256ForVerifier:. @@ -206,6 +250,7 @@ extern NSString *const OIDOAuthorizationRequestCodeChallengeMethodS256; redirectURL:(nullable NSURL *)redirectURL responseType:(NSString *)responseType state:(nullable NSString *)state + nonce:(nullable NSString *)nonce codeVerifier:(nullable NSString *)codeVerifier codeChallenge:(nullable NSString *)codeChallenge codeChallengeMethod:(nullable NSString *)codeChallengeMethod diff --git a/Source/OIDAuthorizationRequest.m b/Source/OIDAuthorizationRequest.m index 73052b09d..2d5e15f23 100644 --- a/Source/OIDAuthorizationRequest.m +++ b/Source/OIDAuthorizationRequest.m @@ -55,6 +55,10 @@ */ static NSString *const kStateKey = @"state"; +/*! @brief Key used to encode the @c nonce property for @c NSSecureCoding, and on the URL request. + */ +static NSString *const kNonceKey = @"nonce"; + /*! @brief Key used to encode the @c codeVerifier property for @c NSSecureCoding. */ static NSString *const kCodeVerifierKey = @"code_verifier"; @@ -98,6 +102,7 @@ @implementation OIDAuthorizationRequest @synthesize scope = _scope; @synthesize redirectURL = _redirectURL; @synthesize state = _state; +@synthesize nonce = _nonce; @synthesize codeVerifier = _codeVerifier; @synthesize codeChallenge = _codeChallenge; @synthesize codeChallengeMethod = _codeChallengeMethod; @@ -120,6 +125,7 @@ - (instancetype)initWithConfiguration:(OIDServiceConfiguration *)configuration redirectURL:(NSURL *)redirectURL responseType:(NSString *)responseType state:(nullable NSString *)state + nonce:(nullable NSString *)nonce codeVerifier:(nullable NSString *)codeVerifier codeChallenge:(nullable NSString *)codeChallenge codeChallengeMethod:(nullable NSString *)codeChallengeMethod @@ -142,6 +148,7 @@ - (instancetype)initWithConfiguration:(OIDServiceConfiguration *)configuration return nil; } _state = [state copy]; + _nonce = [nonce copy]; _codeVerifier = [codeVerifier copy]; _codeChallenge = [codeChallenge copy]; _codeChallengeMethod = [codeChallengeMethod copy]; @@ -152,6 +159,32 @@ - (instancetype)initWithConfiguration:(OIDServiceConfiguration *)configuration return self; } +// Deprecated +- (instancetype)initWithConfiguration:(OIDServiceConfiguration *)configuration + clientId:(NSString *)clientID + clientSecret:(nullable NSString *)clientSecret + scope:(nullable NSString *)scope + redirectURL:(NSURL *)redirectURL + responseType:(NSString *)responseType + state:(nullable NSString *)state + codeVerifier:(nullable NSString *)codeVerifier + codeChallenge:(nullable NSString *)codeChallenge + codeChallengeMethod:(nullable NSString *)codeChallengeMethod + additionalParameters:(nullable NSDictionary *)additionalParameters { + return [self initWithConfiguration:configuration + clientId:clientID + clientSecret:clientSecret + scope:scope + redirectURL:redirectURL + responseType:responseType + state:state + nonce:nil + codeVerifier:codeVerifier + codeChallenge:codeChallenge + codeChallengeMethod:OIDOAuthorizationRequestCodeChallengeMethodS256 + additionalParameters:additionalParameters]; +} + - (instancetype) initWithConfiguration:(OIDServiceConfiguration *)configuration clientId:(NSString *)clientID @@ -172,6 +205,7 @@ - (instancetype)initWithConfiguration:(OIDServiceConfiguration *)configuration redirectURL:redirectURL responseType:responseType state:[[self class] generateState] + nonce:[[self class] generateState] codeVerifier:codeVerifier codeChallenge:codeChallenge codeChallengeMethod:OIDOAuthorizationRequestCodeChallengeMethodS256 @@ -225,6 +259,7 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { NSString *scope = [aDecoder decodeObjectOfClass:[NSString class] forKey:kScopeKey]; NSURL *redirectURL = [aDecoder decodeObjectOfClass:[NSURL class] forKey:kRedirectURLKey]; NSString *state = [aDecoder decodeObjectOfClass:[NSString class] forKey:kStateKey]; + NSString *nonce = [aDecoder decodeObjectOfClass:[NSString class] forKey:kNonceKey]; NSString *codeVerifier = [aDecoder decodeObjectOfClass:[NSString class] forKey:kCodeVerifierKey]; NSString *codeChallenge = [aDecoder decodeObjectOfClass:[NSString class] forKey:kCodeChallengeKey]; @@ -245,6 +280,7 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { redirectURL:redirectURL responseType:responseType state:state + nonce:nonce codeVerifier:codeVerifier codeChallenge:codeChallenge codeChallengeMethod:codeChallengeMethod @@ -260,6 +296,7 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_scope forKey:kScopeKey]; [aCoder encodeObject:_redirectURL forKey:kRedirectURLKey]; [aCoder encodeObject:_state forKey:kStateKey]; + [aCoder encodeObject:_nonce forKey:kNonceKey]; [aCoder encodeObject:_codeVerifier forKey:kCodeVerifierKey]; [aCoder encodeObject:_codeChallenge forKey:kCodeChallengeKey]; [aCoder encodeObject:_codeChallengeMethod forKey:kCodeChallengeMethodKey]; @@ -318,6 +355,9 @@ - (NSURL *)authorizationRequestURL { if (_state) { [query addParameter:kStateKey value:_state]; } + if (_nonce) { + [query addParameter:kNonceKey value:_nonce]; + } if (_codeChallenge) { [query addParameter:kCodeChallengeKey value:_codeChallenge]; } diff --git a/Source/OIDAuthorizationService.h b/Source/OIDAuthorizationService.h index 6ffa5e241..ef07529fc 100644 --- a/Source/OIDAuthorizationService.h +++ b/Source/OIDAuthorizationService.h @@ -131,6 +131,15 @@ typedef void (^OIDRegistrationCompletion)(OIDRegistrationResponse *_Nullable reg */ + (void)performTokenRequest:(OIDTokenRequest *)request callback:(OIDTokenCallback)callback; +/*! @brief Performs a token request. + @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 Performs a registration request. @param request The registration request. @param completion The method called when the request has completed or failed. diff --git a/Source/OIDAuthorizationService.m b/Source/OIDAuthorizationService.m index 473517841..b34b910ca 100644 --- a/Source/OIDAuthorizationService.m +++ b/Source/OIDAuthorizationService.m @@ -266,6 +266,15 @@ + (void)discoverServiceConfigurationForDiscoveryURL:(NSURL *)discoveryURL #pragma mark - Token Endpoint + (void)performTokenRequest:(OIDTokenRequest *)request callback:(OIDTokenCallback)callback { + return [[self class] performTokenRequest:request + originalAuthorizationResponse:nil + callback:callback]; +} + ++ (void)performTokenRequest:(OIDTokenRequest *)request + originalAuthorizationResponse:(OIDAuthorizationResponse *_Nullable)authorizationResponse + callback:(OIDTokenCallback)callback { + NSURLRequest *URLRequest = [request URLRequest]; NSURLSession *session = [OIDURLSessionProvider session]; [[session dataTaskWithRequest:URLRequest diff --git a/UnitTests/OIDAuthorizationRequestTests.m b/UnitTests/OIDAuthorizationRequestTests.m index 9486440be..972b130ee 100644 --- a/UnitTests/OIDAuthorizationRequestTests.m +++ b/UnitTests/OIDAuthorizationRequestTests.m @@ -63,6 +63,10 @@ */ static NSString *const kTestState = @"State"; +/*! @brief Test value for the @c nonce property. + */ +static NSString *const kTestNonce = @"Nonce"; + /*! @brief Test value for the @c codeVerifier property. */ static NSString *const kTestCodeVerifier = @"code verifier"; @@ -142,6 +146,7 @@ + (OIDAuthorizationRequest *)testInstance { redirectURL:[NSURL URLWithString:kTestRedirectURL] responseType:kTestResponseType state:kTestState + nonce:kTestNonce codeVerifier:kTestCodeVerifier codeChallenge:[[self class] codeChallenge] codeChallengeMethod:[[self class] codeChallengeMethod] @@ -159,6 +164,7 @@ + (OIDAuthorizationRequest *)testInstanceCodeFlow { redirectURL:[NSURL URLWithString:kTestRedirectURL] responseType:OIDResponseTypeCode state:kTestState + nonce:kTestNonce codeVerifier:kTestCodeVerifier codeChallenge:[[self class] codeChallenge] codeChallengeMethod:[[self class] codeChallengeMethod] @@ -176,6 +182,7 @@ + (OIDAuthorizationRequest *)testInstanceCodeFlowClientAuth { redirectURL:[NSURL URLWithString:kTestRedirectURL] responseType:OIDResponseTypeCode state:kTestState + nonce:kTestNonce codeVerifier:kTestCodeVerifier codeChallenge:[[self class] codeChallenge] codeChallengeMethod:[[self class] codeChallengeMethod] @@ -240,6 +247,7 @@ - (void)testCopying { XCTAssertEqualObjects(request.clientSecret, kTestClientSecret); XCTAssertEqualObjects(request.redirectURL, [NSURL URLWithString:kTestRedirectURL]); XCTAssertEqualObjects(request.state, kTestState); + XCTAssertEqualObjects(request.nonce, kTestNonce); XCTAssertEqualObjects(request.codeVerifier, kTestCodeVerifier); XCTAssertEqualObjects(request.codeChallenge, [[self class] codeChallenge]); XCTAssertEqualObjects(request.codeChallengeMethod, [[self class] codeChallengeMethod]); @@ -435,6 +443,7 @@ - (void)testSupportedResponseTypes { redirectURL:[NSURL URLWithString:kTestRedirectURL] responseType:@"code id_token" state:kTestState + nonce:kTestNonce codeVerifier:kTestCodeVerifier codeChallenge:[[self class] codeChallenge] codeChallengeMethod:[[self class] codeChallengeMethod] @@ -449,6 +458,7 @@ - (void)testSupportedResponseTypes { redirectURL:[NSURL URLWithString:kTestRedirectURL] responseType:@"code token id_token" state:kTestState + nonce:kTestNonce codeVerifier:kTestCodeVerifier codeChallenge:[[self class] codeChallenge] codeChallengeMethod:[[self class] codeChallengeMethod] @@ -463,6 +473,7 @@ - (void)testSupportedResponseTypes { redirectURL:[NSURL URLWithString:kTestRedirectURL] responseType:@"code" state:kTestState + nonce:kTestNonce codeVerifier:kTestCodeVerifier codeChallenge:[[self class] codeChallenge] codeChallengeMethod:[[self class] codeChallengeMethod] From 89b5473f31d61b07713549e761b14a12d6ebe7c9 Mon Sep 17 00:00:00 2001 From: William Denniss Date: Fri, 5 Jan 2018 12:25:40 -0800 Subject: [PATCH 2/3] Implement ID Token parsing and validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit – New OIDIDToken object to parse ID Tokens. – Token exchange now validates the ID Token pursuant to OpenID Connect Core Section 3.1.3.7. (see code comments for details). --- AppAuth.xcodeproj/project.pbxproj | 28 ++++++ Source/AppAuth.h | 1 + Source/Framework/AppAuth.h | 1 + Source/OIDAuthState.m | 9 +- Source/OIDAuthorizationService.m | 118 ++++++++++++++++++++++ Source/OIDError.h | 7 ++ Source/OIDIDToken.h | 101 +++++++++++++++++++ Source/OIDIDToken.m | 158 ++++++++++++++++++++++++++++++ Source/OIDServiceConfiguration.h | 23 +++++ Source/OIDServiceConfiguration.m | 39 +++++++- Source/OIDTokenResponse.h | 9 +- 11 files changed, 485 insertions(+), 9 deletions(-) create mode 100644 Source/OIDIDToken.h create mode 100644 Source/OIDIDToken.m diff --git a/AppAuth.xcodeproj/project.pbxproj b/AppAuth.xcodeproj/project.pbxproj index f721a6391..623f7c6a1 100644 --- a/AppAuth.xcodeproj/project.pbxproj +++ b/AppAuth.xcodeproj/project.pbxproj @@ -365,6 +365,18 @@ 34AF736B1FB4E4B30022335F /* OIDURLSessionProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 039697451FA8258D003D1FB2 /* OIDURLSessionProvider.m */; }; 34AF736C1FB4E4B40022335F /* OIDURLSessionProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 039697451FA8258D003D1FB2 /* OIDURLSessionProvider.m */; }; 34AF736D1FB4E4B40022335F /* OIDURLSessionProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 039697451FA8258D003D1FB2 /* OIDURLSessionProvider.m */; }; + 34A663291E871DD40060B664 /* OIDIDToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 34A663261E871DD40060B664 /* OIDIDToken.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34A6632A1E871DD40060B664 /* OIDIDToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 34A663261E871DD40060B664 /* OIDIDToken.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34A6632B1E871DD40060B664 /* OIDIDToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 34A663261E871DD40060B664 /* OIDIDToken.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34A6632C1E871DD40060B664 /* OIDIDToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 34A663261E871DD40060B664 /* OIDIDToken.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34A6632D1E871DD40060B664 /* OIDIDToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A663271E871DD40060B664 /* OIDIDToken.m */; }; + 34A6632E1E871DD40060B664 /* OIDIDToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A663271E871DD40060B664 /* OIDIDToken.m */; }; + 34A6632F1E871DD40060B664 /* OIDIDToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A663271E871DD40060B664 /* OIDIDToken.m */; }; + 34A663301E871DD40060B664 /* OIDIDToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A663271E871DD40060B664 /* OIDIDToken.m */; }; + 34A663311E871DD40060B664 /* OIDIDToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A663271E871DD40060B664 /* OIDIDToken.m */; }; + 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 */; }; 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 */; }; @@ -560,6 +572,8 @@ 345AE745202D526800738D22 /* OIDExternalUserAgentIOSCustomBrowser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OIDExternalUserAgentIOSCustomBrowser.m; path = iOS/OIDExternalUserAgentIOSCustomBrowser.m; sourceTree = ""; }; 345AE746202D526800738D22 /* OIDExternalUserAgentIOSCustomBrowser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OIDExternalUserAgentIOSCustomBrowser.h; path = iOS/OIDExternalUserAgentIOSCustomBrowser.h; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -779,6 +793,8 @@ 60140F7B1DE42E1000DA0DC3 /* OIDRegistrationRequest.m */, 341741C51C5D8243000EF209 /* OIDGrantTypes.h */, 341741C61C5D8243000EF209 /* OIDGrantTypes.m */, + 34A663261E871DD40060B664 /* OIDIDToken.h */, + 34A663271E871DD40060B664 /* OIDIDToken.m */, 341741C71C5D8243000EF209 /* OIDResponseTypes.h */, 341741C81C5D8243000EF209 /* OIDResponseTypes.m */, 341741C91C5D8243000EF209 /* OIDScopes.h */, @@ -902,6 +918,7 @@ 343AAAE81E83499000F9D36E /* OIDAuthStateChangeDelegate.h in Headers */, 345AE749202D526900738D22 /* OIDExternalUserAgentIOSCustomBrowser.h in Headers */, 343AAA6B1E83465500F9D36E /* AppAuth.h in Headers */, + 34A663291E871DD40060B664 /* OIDIDToken.h in Headers */, 343AAAF21E83499000F9D36E /* OIDResponseTypes.h in Headers */, 343AAAF71E83499000F9D36E /* OIDTokenRequest.h in Headers */, 343AAAF41E83499000F9D36E /* OIDScopeUtilities.h in Headers */, @@ -948,6 +965,7 @@ 343AAB9B1E834A8800F9D36E /* AppAuth.h in Headers */, 343AAB001E83499100F9D36E /* OIDAuthStateChangeDelegate.h in Headers */, 343AAB081E83499100F9D36E /* OIDRegistrationRequest.h in Headers */, + 34A6632A1E871DD40060B664 /* OIDIDToken.h in Headers */, 343AAB101E83499100F9D36E /* OIDTokenResponse.h in Headers */, 343AAAFC1E83499100F9D36E /* OIDAuthorizationResponse.h in Headers */, 343AAB0C1E83499100F9D36E /* OIDScopeUtilities.h in Headers */, @@ -979,6 +997,7 @@ 343AAB9C1E834A8900F9D36E /* AppAuth.h in Headers */, 343AAB181E83499200F9D36E /* OIDAuthStateChangeDelegate.h in Headers */, 343AAB201E83499200F9D36E /* OIDRegistrationRequest.h in Headers */, + 34A6632B1E871DD40060B664 /* OIDIDToken.h in Headers */, 343AAB281E83499200F9D36E /* OIDTokenResponse.h in Headers */, 343AAB141E83499200F9D36E /* OIDAuthorizationResponse.h in Headers */, 343AAB241E83499200F9D36E /* OIDScopeUtilities.h in Headers */, @@ -1018,6 +1037,7 @@ A6339DAD20321AEB0043D1C9 /* OIDAuthorizationFlowSession.h in Headers */, 343AAB3E1E83499200F9D36E /* OIDServiceDiscovery.h in Headers */, A6DEAB9E2018E4AE0022AC32 /* OIDExternalUserAgent.h in Headers */, + 34A6632C1E871DD40060B664 /* OIDIDToken.h in Headers */, 343AAADE1E83494400F9D36E /* OIDAuthorizationService+Mac.h in Headers */, 343AAB301E83499200F9D36E /* OIDAuthStateChangeDelegate.h in Headers */, 343AAB381E83499200F9D36E /* OIDRegistrationRequest.h in Headers */, @@ -1470,6 +1490,7 @@ 340DAE581D5821A100EC285B /* OIDExternalUserAgentMac.m in Sources */, 340DAE5A1D5821AB00EC285B /* OIDAuthorizationRequest.m in Sources */, 347423E41E7F3C4000D3E6D6 /* OIDAuthorizationResponse.m in Sources */, + 34A6632E1E871DD40060B664 /* OIDIDToken.m in Sources */, 340DAE591D5821A100EC285B /* OIDAuthState+Mac.m in Sources */, 34AF73671FB4E4B00022335F /* OIDURLSessionProvider.m in Sources */, 341310D01E6F944B00D5DEE5 /* OIDURLQueryComponent.m in Sources */, @@ -1484,6 +1505,7 @@ files = ( A6DEABAA2018E5B50022AC32 /* OIDExternalUserAgentIOS.m in Sources */, 341741E01C5D8243000EF209 /* OIDErrorUtilities.m in Sources */, + 34A6632D1E871DD40060B664 /* OIDIDToken.m in Sources */, 341741EA1C5D8243000EF209 /* OIDTokenUtilities.m in Sources */, 341741E21C5D8243000EF209 /* OIDGrantTypes.m in Sources */, 60140F7C1DE42E1000DA0DC3 /* OIDRegistrationRequest.m in Sources */, @@ -1585,6 +1607,7 @@ 341310D71E6F944D00D5DEE5 /* OIDRegistrationRequest.m in Sources */, 341310DD1E6F944D00D5DEE5 /* OIDServiceDiscovery.m in Sources */, 341E70991DE18796004353C1 /* OIDAuthorizationResponse.m in Sources */, + 34A6632F1E871DD40060B664 /* OIDIDToken.m in Sources */, 341310DB1E6F944D00D5DEE5 /* OIDScopeUtilities.m in Sources */, 341310D61E6F944D00D5DEE5 /* OIDRegistrationResponse.m in Sources */, 341310D31E6F944D00D5DEE5 /* OIDError.m in Sources */, @@ -1607,6 +1630,7 @@ files = ( A6DEABAF2018E5D80022AC32 /* OIDExternalUserAgentIOS.m in Sources */, 343AAA881E83478900F9D36E /* OIDFieldMapping.m in Sources */, + 34A663311E871DD40060B664 /* OIDIDToken.m in Sources */, 343AAA841E83478900F9D36E /* OIDAuthState.m in Sources */, 343AAA701E83467D00F9D36E /* OIDAuthState+IOS.m in Sources */, 343AAA921E83478900F9D36E /* OIDTokenResponse.m in Sources */, @@ -1666,6 +1690,7 @@ 343AAB741E8349B000F9D36E /* OIDRegistrationRequest.m in Sources */, 343AAB7A1E8349B000F9D36E /* OIDServiceDiscovery.m in Sources */, 343AAB6C1E8349B000F9D36E /* OIDAuthorizationResponse.m in Sources */, + 34A663321E871DD40060B664 /* OIDIDToken.m in Sources */, 343AAB781E8349B000F9D36E /* OIDScopeUtilities.m in Sources */, 343AAB731E8349B000F9D36E /* OIDRegistrationResponse.m in Sources */, 343AAB701E8349B000F9D36E /* OIDError.m in Sources */, @@ -1694,6 +1719,7 @@ 343AAB601E8349B000F9D36E /* OIDRegistrationRequest.m in Sources */, 343AAB661E8349B000F9D36E /* OIDServiceDiscovery.m in Sources */, 343AAB581E8349B000F9D36E /* OIDAuthorizationResponse.m in Sources */, + 34A663331E871DD40060B664 /* OIDIDToken.m in Sources */, 343AAB641E8349B000F9D36E /* OIDScopeUtilities.m in Sources */, 343AAB5F1E8349B000F9D36E /* OIDRegistrationResponse.m in Sources */, 343AAB5C1E8349B000F9D36E /* OIDError.m in Sources */, @@ -1756,6 +1782,7 @@ 343AAB491E8349AF00F9D36E /* OIDErrorUtilities.m in Sources */, 343AAADB1E83493D00F9D36E /* OIDExternalUserAgentMac.m in Sources */, 343AAB471E8349AF00F9D36E /* OIDClientMetadataParameters.m in Sources */, + 34A663341E871DD40060B664 /* OIDIDToken.m in Sources */, 343AAB461E8349AF00F9D36E /* OIDAuthState.m in Sources */, 34AF736D1FB4E4B40022335F /* OIDURLSessionProvider.m in Sources */, 343AAB561E8349AF00F9D36E /* OIDURLQueryComponent.m in Sources */, @@ -1797,6 +1824,7 @@ 347424081E7F4BA000D3E6D6 /* OIDRegistrationRequest.m in Sources */, 3474240E1E7F4BA000D3E6D6 /* OIDServiceDiscovery.m in Sources */, 347424001E7F4BA000D3E6D6 /* OIDAuthorizationResponse.m in Sources */, + 34A663301E871DD40060B664 /* OIDIDToken.m in Sources */, 3474240C1E7F4BA000D3E6D6 /* OIDScopeUtilities.m in Sources */, 347424071E7F4BA000D3E6D6 /* OIDRegistrationResponse.m in Sources */, 347424041E7F4BA000D3E6D6 /* OIDError.m in Sources */, diff --git a/Source/AppAuth.h b/Source/AppAuth.h index 277fc7488..dd30d9935 100644 --- a/Source/AppAuth.h +++ b/Source/AppAuth.h @@ -29,6 +29,7 @@ #import "OIDExternalUserAgentRequest.h" #import "OIDExternalUserAgentSession.h" #import "OIDGrantTypes.h" +#import "OIDIDToken.h" #import "OIDRegistrationRequest.h" #import "OIDRegistrationResponse.h" #import "OIDResponseTypes.h" diff --git a/Source/Framework/AppAuth.h b/Source/Framework/AppAuth.h index 49879abc6..37ece0202 100644 --- a/Source/Framework/AppAuth.h +++ b/Source/Framework/AppAuth.h @@ -37,6 +37,7 @@ FOUNDATION_EXPORT const unsigned char AppAuthVersionString[]; #import #import #import +#import #import #import #import diff --git a/Source/OIDAuthState.m b/Source/OIDAuthState.m index 47dca0e02..1902afb1d 100644 --- a/Source/OIDAuthState.m +++ b/Source/OIDAuthState.m @@ -124,10 +124,9 @@ @implementation OIDAuthState // code exchange OIDTokenRequest *tokenExchangeRequest = [authorizationResponse tokenExchangeRequest]; - [OIDAuthorizationService - performTokenRequest:tokenExchangeRequest - originalAuthorizationResponse:authorizationResponse - callback:^(OIDTokenResponse *_Nullable tokenResponse, + [OIDAuthorizationService performTokenRequest:tokenExchangeRequest + originalAuthorizationResponse:authorizationResponse + callback:^(OIDTokenResponse *_Nullable tokenResponse, NSError *_Nullable tokenError) { OIDAuthState *authState; if (tokenResponse) { @@ -137,7 +136,7 @@ @implementation OIDAuthState tokenResponse:tokenResponse]; } callback(authState, tokenError); - }]; + }]; } else { // implicit or hybrid flow (hybrid flow assumes code is not for this // client) diff --git a/Source/OIDAuthorizationService.m b/Source/OIDAuthorizationService.m index b34b910ca..7c747b803 100644 --- a/Source/OIDAuthorizationService.m +++ b/Source/OIDAuthorizationService.m @@ -25,6 +25,7 @@ #import "OIDAuthorizationFlowSession.h" #import "OIDExternalUserAgent.h" #import "OIDExternalUserAgentSession.h" +#import "OIDIDToken.h" #import "OIDRegistrationRequest.h" #import "OIDRegistrationResponse.h" #import "OIDServiceConfiguration.h" @@ -362,6 +363,123 @@ + (void)performTokenRequest:(OIDTokenRequest *)request return; } + // If an ID Token is included in the response, validates the ID Token following the rules + // in OpenID Connect Core Section 3.1.3.7 for features that AppAuth directly supports + // (which excludes rules #1, #4, #5, #7, #8, #12, and #13). Regarding rule #6, ID Tokens + // received by this class are received via direct communication between the Client and the Token + // Endpoint, thus we are exercising the option to rely only on the TLS validation. AppAuth + // has a zero dependencies policy, and verifying the JWT signature would add a dependency. + // Users of the library are welcome to perform the JWT signature verification themselves should + // they wish. + if (tokenResponse.idToken) { + OIDIDToken *idToken = [[OIDIDToken alloc] initWithIDTokenString:tokenResponse.idToken]; + if (!idToken) { + NSError *invalidIDToken = + [OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenParsingError + underlyingError:nil + description:@"ID Token parsing failed"]; + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil, invalidIDToken); + }); + return; + } + + // OpenID Connect Core Section 3.1.3.7. rule #1 + // Not supported: AppAuth does not support JWT encryption. + + // OpenID Connect Core Section 3.1.3.7. rule #2 + // Validates that the issuer in the ID Token matches that of the discovery document. + NSURL *issuer = tokenResponse.request.configuration.issuer; + if (issuer && ![idToken.issuer isEqual:issuer]) { + NSError *invalidIDToken = + [OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenFailedValidationError + underlyingError:nil + description:@"Issuer mismatch"]; + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil, invalidIDToken); + }); + return; + } + + // OpenID Connect Core Section 3.1.3.7. rule #3 + // Validates that the audience of the ID Token matches the client ID. + NSString *clientID = tokenResponse.request.clientID; + if (![idToken.audience containsObject:clientID]) { + NSError *invalidIDToken = + [OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenFailedValidationError + underlyingError:nil + description:@"Audience mismatch"]; + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil, invalidIDToken); + }); + return; + } + + // OpenID Connect Core Section 3.1.3.7. rules #4 & #5 + // Not supported. + + // OpenID Connect Core Section 3.1.3.7. rule #6 + // As noted above, AppAuth only supports the code flow which results in direct communication + // of the ID Token from the Token Endpoint to the Client, and we are exercising the option to + // use TSL server validation instead of checking the token signature. Users may additionally + // check the token signature should they wish. + + // OpenID Connect Core Section 3.1.3.7. rules #7 & #8 + // Not applicable. See rule #6. + + // OpenID Connect Core Section 3.1.3.7. rule #9 + // Validates that the current time is before the expiry time. + NSTimeInterval expiresAtDifference = [idToken.expiresAt timeIntervalSinceNow]; + if (expiresAtDifference < 0) { + NSError *invalidIDToken = + [OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenFailedValidationError + underlyingError:nil + description:@"ID Token expired"]; + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil, invalidIDToken); + }); + return; + } + + // OpenID Connect Core Section 3.1.3.7. rule #10 + // Validates that the issued at time is not more than +/- 5 minutes on the current time. + NSTimeInterval issuedAtDifference = [idToken.issuedAt timeIntervalSinceNow]; + if (fabs(issuedAtDifference) > 300) { + NSError *invalidIDToken = + [OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenFailedValidationError + underlyingError:nil + description:@"Issued at time is more than 5 minutes before or after " + "the current time"]; + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil, invalidIDToken); + }); + return; + } + + // Only relevant for the authorization_code response type + if ([tokenResponse.request.grantType isEqual:OIDGrantTypeAuthorizationCode]) { + // OpenID Connect Core Section 3.1.3.7. rule #11 + // Validates the nonce. + NSString *nonce = authorizationResponse.request.nonce; + if (nonce && ![idToken.nonce isEqual:nonce]) { + NSError *invalidIDToken = + [OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenFailedValidationError + underlyingError:nil + description:@"Nonce mismatch"]; + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil, invalidIDToken); + }); + return; + } + } + + // OpenID Connect Core Section 3.1.3.7. rules #12 + // ACR is not directly supported by AppAuth. + + // OpenID Connect Core Section 3.1.3.7. rules #12 + // max_age is not directly supported by AppAuth. + } + // Success dispatch_async(dispatch_get_main_queue(), ^{ callback(tokenResponse, nil); diff --git a/Source/OIDError.h b/Source/OIDError.h index 229802cc0..85b71e267 100644 --- a/Source/OIDError.h +++ b/Source/OIDError.h @@ -144,6 +144,13 @@ typedef NS_ENUM(NSInteger, OIDErrorCode) { */ OIDErrorCodeJSONSerializationError = -13, + /*! @brief The ID Token did not parse. + */ + OIDErrorCodeIDTokenParsingError = -14, + + /*! @brief The ID Token did not pass validation (e.g. issuer, audience checks). + */ + OIDErrorCodeIDTokenFailedValidationError = -15, }; /*! @brief Enum of all possible OAuth error codes as defined by RFC6749 diff --git a/Source/OIDIDToken.h b/Source/OIDIDToken.h new file mode 100644 index 000000000..d39f3802e --- /dev/null +++ b/Source/OIDIDToken.h @@ -0,0 +1,101 @@ +/*! @file OIDIDToken.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 + +NS_ASSUME_NONNULL_BEGIN + +/*! @brief A convenience class that parses an ID Token and extracts the claims _but does not_ + verify its signature. AppAuth only supports the OpenID Code flow, meaning ID Tokens + received by AppAuth are sent from the token endpoint on a TLS protected channel, + offering some assurances as to the origin of the token. You may wish to additionally + verify the ID Token signature using a JWT signature verification library of your + choosing. + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + @see https://tools.ietf.org/html/rfc7519 + @see https://jwt.io/ + */ +@interface OIDIDToken : NSObject { + // property variables + NSDictionary *_header; + NSDictionary *_claims; + NSURL *_issuer; + NSString *_subject; + NSArray *_audience; + NSDate *_expiresAt; + NSDate *_issuedAt; + NSString *_nonce; +} + +/*! @internal + @brief Unavailable. Please use @c initWithAuthorizationResponse:. + */ +- (instancetype)init NS_UNAVAILABLE; + +/*! @brief Parses the given ID Token string. + @param idToken The ID Token spring. + */ +- (nullable instancetype)initWithIDTokenString:(NSString *)idToken; + +/*! @brief The header JWT values. + */ +@property(nonatomic, readonly) NSDictionary *header; + +/*! @brief All ID Token claims. + */ +@property(nonatomic, readonly) NSDictionary *claims; + +/*! @brief Issuer Identifier for the Issuer of the response. + @remarks iss + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +@property(nonatomic, readonly) NSURL *issuer; + +/*! @brief Subject Identifier. + @remarks sub + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +@property(nonatomic, readonly) NSString *subject; + +/*! @brief Audience(s) that this ID Token is intended for. + @remarks aud + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +@property(nonatomic, readonly) NSArray *audience; + +/*! @brief Expiration time on or after which the ID Token MUST NOT be accepted for processing. + @remarks exp + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +@property(nonatomic, readonly) NSDate *expiresAt; + +/*! @brief Time at which the JWT was issued. + @remarks iat + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +@property(nonatomic, readonly) NSDate *issuedAt; + +/*! @brief String value used to associate a Client session with an ID Token, and to mitigate replay + attacks. + @remarks nonce + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +@property(nonatomic, readonly, nullable) NSString *nonce; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/OIDIDToken.m b/Source/OIDIDToken.m new file mode 100644 index 000000000..94989d323 --- /dev/null +++ b/Source/OIDIDToken.m @@ -0,0 +1,158 @@ +/*! @file OIDIDToken.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 "OIDIDToken.h" + +/*! Field keys associated with an ID Token. */ +static NSString *const kIssKey = @"iss"; +static NSString *const kSubKey = @"sub"; +static NSString *const kAudKey = @"aud"; +static NSString *const kExpKey = @"exp"; +static NSString *const kIatKey = @"iat"; +static NSString *const kNonceKey = @"nonce"; + +#import "OIDFieldMapping.h" + +@implementation OIDIDToken + +@synthesize header = _header; +@synthesize claims = _claims; +@synthesize issuer = _issuer; +@synthesize subject = _subject; +@synthesize audience = _audience; +@synthesize expiresAt = _expiresAt; +@synthesize issuedAt = _issuedAt; +@synthesize nonce = _nonce; + +- (instancetype)initWithIDTokenString:(NSString *)idToken { + self = [super init]; + NSArray *sections = [idToken componentsSeparatedByString:@"."]; + + // The header and claims sections are required. + if (sections.count <= 1) { + return nil; + } + + _header = [[self class] parseJWTSection:sections[0]]; + _claims = [[self class] parseJWTSection:sections[1]]; + if (!_header || !_claims) { + return nil; + } + + [OIDFieldMapping remainingParametersWithMap:[[self class] fieldMap] + parameters:_claims + instance:self]; + + // Required fields. + if (!_issuer || !_audience || !_subject || !_expiresAt || !_issuedAt) { + return nil; + } + + return self; +} + +/*! @brief Returns a mapping of incoming parameters to instance variables. + @return A mapping of incoming parameters to instance variables. + */ ++ (NSDictionary *)fieldMap { + static NSMutableDictionary *fieldMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + fieldMap = [NSMutableDictionary dictionary]; + + fieldMap[kIssKey] = + [[OIDFieldMapping alloc] initWithName:@"_issuer" + type:[NSURL class] + conversion:[OIDFieldMapping URLConversion]]; + fieldMap[kSubKey] = + [[OIDFieldMapping alloc] initWithName:@"_subject" type:[NSString class]]; + fieldMap[kAudKey] = + [[OIDFieldMapping alloc] initWithName:@"_audience" + type:[NSArray class] + conversion:^id _Nullable(NSObject *_Nullable value) { + if ([value isKindOfClass:[NSArray class]]) { + return value; + } + if ([value isKindOfClass:[NSString class]]) { + return @[value]; + } + return nil; + }]; + fieldMap[kExpKey] = + [[OIDFieldMapping alloc] initWithName:@"_expiresAt" + type:[NSDate class] + conversion:^id _Nullable(NSObject *_Nullable value) { + if (![value isKindOfClass:[NSNumber class]]) { + return value; + } + NSNumber *valueAsNumber = (NSNumber *)value; + return [NSDate dateWithTimeIntervalSince1970:valueAsNumber.longLongValue]; + }]; + fieldMap[kIatKey] = + [[OIDFieldMapping alloc] initWithName:@"_issuedAt" + type:[NSDate class] + conversion:^id _Nullable(NSObject *_Nullable value) { + if (![value isKindOfClass:[NSNumber class]]) { + return value; + } + NSNumber *valueAsNumber = (NSNumber *)value; + return [NSDate dateWithTimeIntervalSince1970:valueAsNumber.longLongValue]; + }]; + fieldMap[kNonceKey] = + [[OIDFieldMapping alloc] initWithName:@"_nonce" type:[NSString class]]; + }); + return fieldMap; +} + ++ (NSDictionary *)parseJWTSection:(NSString *)sectionString { + NSData *decodedData = [[self class] base64urlNoPaddingDecode:sectionString]; + + // Parses JSON. + NSError *error; + id object = [NSJSONSerialization JSONObjectWithData:decodedData options:0 error:&error]; + if (error) { + NSLog(@"Error %@ parsing token payload %@", error, sectionString); + } + if ([object isKindOfClass:[NSDictionary class]]) { + return (NSDictionary *)object; + } + + return nil; +} + ++ (NSData *)base64urlNoPaddingDecode:(NSString *)base64urlNoPaddingString { + NSMutableString *body = [base64urlNoPaddingString mutableCopy]; + + // Converts base64url to base64. + NSRange range = NSMakeRange(0, base64urlNoPaddingString.length); + [body replaceOccurrencesOfString:@"-" withString:@"+" options:NSLiteralSearch range:range]; + [body replaceOccurrencesOfString:@"_" withString:@"/" options:NSLiteralSearch range:range]; + + // Converts base64 no padding to base64 with padding + while (body.length % 4 != 0) { + [body appendString:@"="]; + } + + // Decodes base64 string. + NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:body options:0]; + return decodedData; +} + +@end + + diff --git a/Source/OIDServiceConfiguration.h b/Source/OIDServiceConfiguration.h index 513e5d970..8c33698e4 100644 --- a/Source/OIDServiceConfiguration.h +++ b/Source/OIDServiceConfiguration.h @@ -36,6 +36,7 @@ typedef void (^OIDServiceConfigurationCreated) // property variables NSURL *_authorizationEndpoint; NSURL *_tokenEndpoint; + NSURL *_issuer; NSURL *_registrationEndpoint; OIDServiceDiscovery *_discoveryDocument; } @@ -48,6 +49,10 @@ typedef void (^OIDServiceConfigurationCreated) */ @property(nonatomic, readonly) NSURL *tokenEndpoint; +/*! @brief The OpenID Connect issuer. + */ +@property(nonatomic, readonly, nullable) NSURL *issuer; + /*! @brief The dynamic client registration endpoint URI. */ @property(nonatomic, readonly, nullable) NSURL *registrationEndpoint; @@ -76,6 +81,24 @@ typedef void (^OIDServiceConfigurationCreated) tokenEndpoint:(NSURL *)tokenEndpoint registrationEndpoint:(nullable NSURL *)registrationEndpoint; +/*! @param authorizationEndpoint The authorization endpoint URI. + @param tokenEndpoint The token exchange and refresh endpoint URI. + @param issuer The OpenID Connect issuer. + */ +- (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint + tokenEndpoint:(NSURL *)tokenEndpoint + issuer:(nullable NSURL *)issuer; + +/*! @param authorizationEndpoint The authorization endpoint URI. + @param tokenEndpoint The token exchange and refresh endpoint URI. + @param issuer The OpenID Connect issuer. + @param registrationEndpoint The dynamic client registration endpoint URI. + */ +- (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint + tokenEndpoint:(NSURL *)tokenEndpoint + issuer:(nullable NSURL *)issuer + registrationEndpoint:(nullable NSURL *)registrationEndpoint; + /*! @param discoveryDocument The discovery document from which to extract the required OAuth configuration. */ diff --git a/Source/OIDServiceConfiguration.m b/Source/OIDServiceConfiguration.m index d3121c28b..efc792e0b 100644 --- a/Source/OIDServiceConfiguration.m +++ b/Source/OIDServiceConfiguration.m @@ -30,6 +30,10 @@ */ static NSString *const kTokenEndpointKey = @"tokenEndpoint"; +/*! @brief The key for the @c issuer property. + */ +static NSString *const kIssuerKey = @"issuer"; + /*! @brief The key for the @c registrationEndpoint property. */ static NSString *const kRegistrationEndpointKey = @"registrationEndpoint"; @@ -44,6 +48,7 @@ @interface OIDServiceConfiguration () - (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint tokenEndpoint:(NSURL *)tokenEndpoint + issuer:(nullable NSURL *)issuer registrationEndpoint:(nullable NSURL *)registrationEndpoint discoveryDocument:(nullable OIDServiceDiscovery *)discoveryDocument NS_DESIGNATED_INITIALIZER; @@ -54,18 +59,19 @@ @implementation OIDServiceConfiguration @synthesize authorizationEndpoint = _authorizationEndpoint; @synthesize tokenEndpoint = _tokenEndpoint; +@synthesize issuer = _issuer; @synthesize registrationEndpoint = _registrationEndpoint; @synthesize discoveryDocument = _discoveryDocument; - (instancetype)init OID_UNAVAILABLE_USE_INITIALIZER(@selector( initWithAuthorizationEndpoint: - tokenEndpoint: - registrationEndpoint:) + tokenEndpoint:) ); - (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint tokenEndpoint:(NSURL *)tokenEndpoint + issuer:(nullable NSURL *)issuer registrationEndpoint:(nullable NSURL *)registrationEndpoint discoveryDocument:(nullable OIDServiceDiscovery *)discoveryDocument { @@ -73,6 +79,7 @@ - (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint if (self) { _authorizationEndpoint = [authorizationEndpoint copy]; _tokenEndpoint = [tokenEndpoint copy]; + _issuer = [issuer copy]; _registrationEndpoint = [registrationEndpoint copy]; _discoveryDocument = [discoveryDocument copy]; } @@ -83,15 +90,38 @@ - (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint tokenEndpoint:(NSURL *)tokenEndpoint { return [self initWithAuthorizationEndpoint:authorizationEndpoint tokenEndpoint:tokenEndpoint + issuer:nil + registrationEndpoint:nil + discoveryDocument:nil]; +} + +- (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint + tokenEndpoint:(NSURL *)tokenEndpoint + registrationEndpoint:(nullable NSURL *)registrationEndpoint { + return [self initWithAuthorizationEndpoint:authorizationEndpoint + tokenEndpoint:tokenEndpoint + issuer:nil + registrationEndpoint:registrationEndpoint + discoveryDocument:nil]; +} + +- (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint + tokenEndpoint:(NSURL *)tokenEndpoint + issuer:(nullable NSURL *)issuer { + return [self initWithAuthorizationEndpoint:authorizationEndpoint + tokenEndpoint:tokenEndpoint + issuer:issuer registrationEndpoint:nil discoveryDocument:nil]; } - (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint tokenEndpoint:(NSURL *)tokenEndpoint + issuer:(nullable NSURL *)issuer registrationEndpoint:(nullable NSURL *)registrationEndpoint { return [self initWithAuthorizationEndpoint:authorizationEndpoint tokenEndpoint:tokenEndpoint + issuer:issuer registrationEndpoint:registrationEndpoint discoveryDocument:nil]; } @@ -99,6 +129,7 @@ - (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint - (instancetype)initWithDiscoveryDocument:(OIDServiceDiscovery *) discoveryDocument { return [self initWithAuthorizationEndpoint:discoveryDocument.authorizationEndpoint tokenEndpoint:discoveryDocument.tokenEndpoint + issuer:discoveryDocument.issuer registrationEndpoint:discoveryDocument.registrationEndpoint discoveryDocument:discoveryDocument]; } @@ -124,6 +155,8 @@ - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { forKey:kAuthorizationEndpointKey]; NSURL *tokenEndpoint = [aDecoder decodeObjectOfClass:[NSURL class] forKey:kTokenEndpointKey]; + NSURL *issuer = [aDecoder decodeObjectOfClass:[NSURL class] + forKey:kIssuerKey]; NSURL *registrationEndpoint = [aDecoder decodeObjectOfClass:[NSURL class] forKey:kRegistrationEndpointKey]; // We don't accept nil authorizationEndpoints or tokenEndpoints. @@ -136,6 +169,7 @@ - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { return [self initWithAuthorizationEndpoint:authorizationEndpoint tokenEndpoint:tokenEndpoint + issuer:issuer registrationEndpoint:registrationEndpoint discoveryDocument:discoveryDocument]; } @@ -143,6 +177,7 @@ - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_authorizationEndpoint forKey:kAuthorizationEndpointKey]; [aCoder encodeObject:_tokenEndpoint forKey:kTokenEndpointKey]; + [aCoder encodeObject:_issuer forKey:kIssuerKey]; [aCoder encodeObject:_registrationEndpoint forKey:kRegistrationEndpointKey]; [aCoder encodeObject:_discoveryDocument forKey:kDiscoveryDocumentKey]; } diff --git a/Source/OIDTokenResponse.h b/Source/OIDTokenResponse.h index dc5d9766c..fcad9dde1 100644 --- a/Source/OIDTokenResponse.h +++ b/Source/OIDTokenResponse.h @@ -67,12 +67,17 @@ NS_ASSUME_NONNULL_BEGIN /*! @brief ID Token value associated with the authenticated session. Always present for the authorization code grant exchange when OpenID Connect is used, optional for responses to - access token refresh requests. + access token refresh requests. Note that AppAuth does NOT verify the JWT signature. Users + of AppAuth are encouraged to verifying the JWT signature using the validation library of + their choosing. @remarks id_token @see http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse @see http://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken - */ + @see https://jwt.io + @discussion @c OIDIDToken can be used to parse the ID Token and extract the claims. As noted, + this class does not verify the JWT signature. +*/ @property(nonatomic, readonly, nullable) NSString *idToken; /*! @brief The refresh token, which can be used to obtain new access tokens using the same From e30ea9dc01fbfb9dc479b7b766f00e73e2946a0b Mon Sep 17 00:00:00 2001 From: William Denniss Date: Fri, 5 Jan 2018 12:26:10 -0800 Subject: [PATCH 3/3] Implement OpenID Connect RP Conformance Tests for Code Flow --- AppAuth.xcodeproj/project.pbxproj | 16 + UnitTests/OIDRPProfileCode.h | 40 +++ UnitTests/OIDRPProfileCode.m | 562 ++++++++++++++++++++++++++++++ 3 files changed, 618 insertions(+) create mode 100644 UnitTests/OIDRPProfileCode.h create mode 100644 UnitTests/OIDRPProfileCode.m diff --git a/AppAuth.xcodeproj/project.pbxproj b/AppAuth.xcodeproj/project.pbxproj index 623f7c6a1..abaefbd6e 100644 --- a/AppAuth.xcodeproj/project.pbxproj +++ b/AppAuth.xcodeproj/project.pbxproj @@ -377,6 +377,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 */; }; @@ -574,6 +580,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 = ""; }; @@ -847,6 +855,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 */, @@ -1543,6 +1553,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 */, @@ -1568,6 +1579,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 */, @@ -1588,6 +1600,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 */, @@ -1674,6 +1687,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; @@ -1753,6 +1767,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; @@ -1808,6 +1823,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..07ef3e8c6 --- /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 "OIDExternalUserAgent.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..45db3909a --- /dev/null +++ b/UnitTests/OIDRPProfileCode.m @@ -0,0 +1,562 @@ +/*! @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 "OIDExternalUserAgentSession.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)presentExternalUserAgentRequest:(id )request + session:(id)session { + _session = session; + NSURL *requestURL = [request externalUserAgentRequestURL]; + 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 resumeExternalUserAgentFlowWithURL:url]; + }] resume]; + + return YES; +} + +- (void)dismissExternalUserAgentAnimated:(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 + externalUserAgent:_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 +