From cc635ed339003089684acbf18e574b29fc802334 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 18 Nov 2015 18:09:52 +0000 Subject: [PATCH] Expired token and no means to renew: - Implemented 40140 - renew token (forced) - Implemented ARTCodeErrors for NSError - Changed ARTAblyErrorDomain to use Reverse-DNS style domain - RSC14c with invalid authUrl --- ably-ios/ARTAuth.m | 14 ++++++++-- ably-ios/ARTAuthOptions.m | 8 +++++- ably-ios/ARTAuthTokenDetails.h | 2 +- ably-ios/ARTRest.m | 33 +++++++++++++++++------ ably-ios/ARTStatus.h | 10 +++++++ ably-ios/ARTStatus.m | 3 ++- ably-ios/ARTTypes.h | 3 ++- ablySpec/RestClient.swift | 38 ++++++++++++++++++++++++++ ablySpec/TestUtilities.swift | 49 +++++++++++++++++++++++++--------- 9 files changed, 134 insertions(+), 26 deletions(-) diff --git a/ably-ios/ARTAuth.m b/ably-ios/ARTAuth.m index 5d2280eb1..2f53d9443 100644 --- a/ably-ios/ARTAuth.m +++ b/ably-ios/ARTAuth.m @@ -17,6 +17,7 @@ #import "ARTAuthTokenParams.h" #import "ARTAuthTokenRequest.h" #import "ARTEncoder.h" +#import "ARTStatus.h" @implementation ARTAuth { __weak ARTRest *_rest; @@ -132,11 +133,16 @@ - (void)requestToken:(ARTAuthTokenParams *)tokenParams withOptions:(ARTAuthOptio // The values supersede matching client library configured params and options. ARTAuthOptions *mergedOptions = [self mergeOptions:authOptions]; ARTAuthTokenParams *currentTokenParams = [self mergeParams:tokenParams]; - + + if (!mergedOptions.key) { + callback(nil, [NSError errorWithDomain:ARTAblyErrorDomain code:ARTCodeErrorAPIKeyMissing + userInfo:@{ NSLocalizedDescriptionKey: NSLocalizedString(@"API Key is missing", nil) }]); + } + if (mergedOptions.authUrl) { NSMutableURLRequest *request = [self buildRequest:mergedOptions withParams:currentTokenParams]; - [_rest.logger debug:@"%@ %@", request.HTTPMethod, request.URL]; + [self.logger debug:@"ARTAuth: using authUrl (%@ %@)", request.HTTPMethod, request.URL]; [_rest executeRequest:request withAuthOption:ARTAuthenticationUseBasic completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { if (error) { @@ -153,6 +159,10 @@ - (void)requestToken:(ARTAuthTokenParams *)tokenParams withOptions:(ARTAuthOptio [self createTokenRequest:currentTokenParams options:mergedOptions callback:callback]; }; + if (tokenRequestFactory == mergedOptions.authCallback) { + [self.logger debug:@"ARTAuth: using authCallback"]; + } + tokenRequestFactory(currentTokenParams, ^(ARTAuthTokenRequest *tokenRequest, NSError *error) { if (error) { callback(nil, error); diff --git a/ably-ios/ARTAuthOptions.m b/ably-ios/ARTAuthOptions.m index 2927a2d19..55d9ea8a6 100644 --- a/ably-ios/ARTAuthOptions.m +++ b/ably-ios/ARTAuthOptions.m @@ -108,7 +108,13 @@ - (ARTAuthOptions *)mergeWith:(ARTAuthOptions *)precedenceOptions { } - (BOOL)isBasicAuth { - return self.useTokenAuth == false && self.key != nil && self.clientId == nil; + return self.useTokenAuth == false && + self.key != nil && + self.clientId == nil && + self.token == nil && + self.tokenDetails == nil && + self.authUrl == nil && + self.authCallback == nil; } - (BOOL)isMethodPOST { diff --git a/ably-ios/ARTAuthTokenDetails.h b/ably-ios/ARTAuthTokenDetails.h index d852278eb..a0f893589 100644 --- a/ably-ios/ARTAuthTokenDetails.h +++ b/ably-ios/ARTAuthTokenDetails.h @@ -43,7 +43,7 @@ ART_ASSUME_NONNULL_BEGIN - (instancetype)init UNAVAILABLE_ATTRIBUTE; - (instancetype)initWithToken:(NSString *)token; -- (instancetype)initWithToken:(NSString *)token expires:(NSDate *)expires issued:(NSDate *)issued capability:(NSString *)capability clientId:(NSString *)clientId; +- (instancetype)initWithToken:(NSString *)token expires:(art_nullable NSDate *)expires issued:(art_nullable NSDate *)issued capability:(art_nullable NSString *)capability clientId:(art_nullable NSString *)clientId; @end diff --git a/ably-ios/ARTRest.m b/ably-ios/ARTRest.m index f286bd87e..737cd0293 100644 --- a/ably-ios/ARTRest.m +++ b/ably-ios/ARTRest.m @@ -12,7 +12,7 @@ #import "ARTChannelCollection.h" #import "ARTDataQuery+Private.h" #import "ARTPaginatedResult+Private.h" -#import "ARTAuth.h" +#import "ARTAuth+Private.h" #import "ARTHttp.h" #import "ARTEncoder.h" #import "ARTJsonEncoder.h" @@ -103,7 +103,10 @@ - (void)executeRequest:(NSMutableURLRequest *)request withAuthOption:(ARTAuthent [self executeRequest:request completion:callback]; break; case ARTAuthenticationOn: - [self executeRequestWithAuthentication:request withMethod:self.auth.method completion:callback]; + [self executeRequestWithAuthentication:request withMethod:self.auth.method force:NO completion:callback]; + break; + case ARTAuthenticationNewToken: + [self executeRequestWithAuthentication:request withMethod:self.auth.method force:YES completion:callback]; break; case ARTAuthenticationUseBasic: [self executeRequestWithAuthentication:request withMethod:ARTAuthMethodBasic completion:callback]; @@ -112,7 +115,11 @@ - (void)executeRequest:(NSMutableURLRequest *)request withAuthOption:(ARTAuthent } - (void)executeRequestWithAuthentication:(NSMutableURLRequest *)request withMethod:(ARTAuthMethod)method completion:(ARTHttpRequestCallback)callback { - [self calculateAuthorization:method completion:^(NSString *authorization, NSError *error) { + [self executeRequestWithAuthentication:request withMethod:method force:NO completion:callback]; +} + +- (void)executeRequestWithAuthentication:(NSMutableURLRequest *)request withMethod:(ARTAuthMethod)method force:(BOOL)force completion:(ARTHttpRequestCallback)callback { + [self calculateAuthorization:method force:force completion:^(NSString *authorization, NSError *error) { if (error && callback) { callback(nil, nil, error); } else { @@ -129,8 +136,10 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(ARTHttpRequest if (response.statusCode >= 400) { NSError *error = [self->_encoders[response.MIMEType] decodeError:data]; if (error.code == 40140) { - // TODO: request token or error if no token information - NSAssert(false, @"Request token or error if no token information"); + // Send it again, requesting a new token (forward callback) + [self.logger debug:@"ARTRest: requesting new token"]; + [self executeRequest:request withAuthOption:ARTAuthenticationNewToken completion:callback]; + return; } else if (callback) { callback(nil, nil, error); } @@ -141,20 +150,28 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(ARTHttpRequest } - (void)calculateAuthorization:(ARTAuthMethod)method completion:(void (^)(NSString *authorization, NSError *error))callback { + [self calculateAuthorization:method force:NO completion:callback]; +} + +- (void)calculateAuthorization:(ARTAuthMethod)method force:(BOOL)force completion:(void (^)(NSString *authorization, NSError *error))callback { [self.logger debug:@"ARTRest: calculating authorization %lu", (unsigned long)method]; // FIXME: use encoder and should be managed on ARTAuth if (method == ARTAuthMethodBasic) { // Include key Base64 encoded in an Authorization header (RFC7235) NSData *keyData = [self.options.key dataUsingEncoding:NSUTF8StringEncoding]; NSString *keyBase64 = [keyData base64EncodedStringWithOptions:0]; - callback([NSString stringWithFormat:@"Basic %@", keyBase64], nil); + if (callback) callback([NSString stringWithFormat:@"Basic %@", keyBase64], nil); } else { - [self.auth authorise:nil options:self.options force:NO callback:^(ARTAuthTokenDetails *tokenDetails, NSError *error) { + [self.auth authorise:nil options:self.options force:force callback:^(ARTAuthTokenDetails *tokenDetails, NSError *error) { + if (error) { + if (callback) callback(nil, error); + return; + } NSData *tokenData = [tokenDetails.token dataUsingEncoding:NSUTF8StringEncoding]; NSString *tokenBase64 = [tokenData base64EncodedStringWithOptions:0]; [self.logger verbose:@"ARTRest: authorization bearer in Base64 %@", tokenBase64]; - callback([NSString stringWithFormat:@"Bearer %@", tokenBase64], nil); + if (callback) callback([NSString stringWithFormat:@"Bearer %@", tokenBase64], nil); }]; } } diff --git a/ably-ios/ARTStatus.h b/ably-ios/ARTStatus.h index 87de10256..93fbfa2a9 100644 --- a/ably-ios/ARTStatus.h +++ b/ably-ios/ARTStatus.h @@ -25,6 +25,16 @@ typedef NS_ENUM(NSUInteger, ARTState) { ARTStateError = 99999 }; +/** + ARTCodeErrors + + The list of all public error codes returned under the error domain ARTAblyErrorDomain + */ +typedef CF_ENUM(NSUInteger, ARTCodeError) { + // FIXME: check hard coded errors + ARTCodeErrorAPIKeyMissing = 80001 +}; + ART_ASSUME_NONNULL_BEGIN FOUNDATION_EXPORT NSString *const ARTAblyErrorDomain; diff --git a/ably-ios/ARTStatus.m b/ably-ios/ARTStatus.m index 2025eec7c..161d4c63c 100644 --- a/ably-ios/ARTStatus.m +++ b/ably-ios/ARTStatus.m @@ -10,7 +10,8 @@ #import "ARTStatus.h" -NSString *const ARTAblyErrorDomain = @"ARTAblyErrorDomain"; +// Reverse-DNS style domain +NSString *const ARTAblyErrorDomain = @"io.ably.cocoa"; @implementation ARTErrorInfo diff --git a/ably-ios/ARTTypes.h b/ably-ios/ARTTypes.h index 7c94c6207..87401ebef 100644 --- a/ably-ios/ARTTypes.h +++ b/ably-ios/ARTTypes.h @@ -22,7 +22,8 @@ typedef NS_ENUM(NSUInteger, ARTAuthentication) { ARTAuthenticationOff, ARTAuthenticationOn, - ARTAuthenticationUseBasic + ARTAuthenticationUseBasic, + ARTAuthenticationNewToken }; typedef NS_ENUM(NSUInteger, ARTAuthMethod) { diff --git a/ablySpec/RestClient.swift b/ablySpec/RestClient.swift index b374c0908..9d81b5212 100644 --- a/ablySpec/RestClient.swift +++ b/ablySpec/RestClient.swift @@ -288,6 +288,44 @@ class RestClient: QuickSpec { } } + // RSC14c + fit("should error when expired token and no means to renew") { + let client = ARTRest(options: AblyTests.commonAppSetup()) + let auth = client.auth + + let tokenParams = ARTAuthTokenParams() + tokenParams.ttl = 3.0 //Seconds + + waitUntil(timeout: testTimeout) { done in + auth.requestToken(tokenParams, withOptions: nil) { tokenDetails, error in + if let e = error { + XCTFail(e.description) + done() + } + else if let currentTokenDetails = tokenDetails { + let options = AblyTests.clientOptions() + options.key = client.options.key + + // Expired token + options.tokenDetails = ARTAuthTokenDetails(token: currentTokenDetails.token, expires: currentTokenDetails.expires?.dateByAddingTimeInterval(testTimeout), issued: currentTokenDetails.issued, capability: currentTokenDetails.capability, clientId: currentTokenDetails.clientId) + + options.authUrl = NSURL(string: "http://test-auth.ably.io") + + let rest = ARTRest(options: options) + + // Delay for token expiration + delay(tokenParams.ttl) { + // 40140 - token expired and will not recover because authUrl is invalid + publishTestMessage(rest) { error in + expect(error).toNot(beNil()) + done() + } + } + } + } + } + } + } //RestClient } } \ No newline at end of file diff --git a/ablySpec/TestUtilities.swift b/ablySpec/TestUtilities.swift index 5e43bf74b..52a80ef58 100644 --- a/ablySpec/TestUtilities.swift +++ b/ablySpec/TestUtilities.swift @@ -49,8 +49,7 @@ class AblyTests { class var jsonRestOptions: ARTClientOptions { get { - let options = ARTClientOptions() - options.environment = "sandbox" + let options = AblyTests.clientOptions() options.binary = false return options } @@ -89,7 +88,6 @@ class AblyTests { if debug { print(response) - options.logLevel = .Verbose } let key = response["keys"][0] @@ -105,6 +103,15 @@ class AblyTests { class func commonAppSetup(debug debug: Bool = false) -> ARTClientOptions { return AblyTests.setupOptions(AblyTests.jsonRestOptions, debug: debug) } + + class func clientOptions(debug debug: Bool = false) -> ARTClientOptions { + let options = ARTClientOptions() + options.environment = "sandbox" + if debug { + options.logLevel = .Verbose + } + return options + } } @@ -130,21 +137,33 @@ func querySyslog(forLogsAfter startingTime: NSDate? = nil) -> AnyGenerator()> + var error: NSError? = NSError(domain: "", code: -1, userInfo: nil) + + convenience init(client: ARTRest, failOnError: Bool) { + self.init(client: client, completion: nil) + } + + init(client: ARTRest, completion: Optional<(NSError?)->()>) { client.channels.get("test").publish("message") { error in self.error = error - if failOnError { - XCTFail("Got error '\(error)'") + if let callback = completion { + callback(error) + } + else if let e = error { + XCTFail("Got error '\(e)'") } } } + } /// Publish message +func publishTestMessage(client: ARTRest, completion: Optional<(NSError?)->()>) -> PublishTestMessage { + return PublishTestMessage(client: client, completion: completion) +} + func publishTestMessage(client: ARTRest, failOnError: Bool = true) -> PublishTestMessage { return PublishTestMessage(client: client, failOnError: failOnError) } @@ -230,7 +249,7 @@ func extractBodyAsJSON(request: NSMutableURLRequest?) -> Result { } /* - Records each request for test purpose. + Records each request and response for test purpose. */ @objc class MockHTTPExecutor: NSObject, ARTHTTPExecutor { @@ -240,10 +259,16 @@ class MockHTTPExecutor: NSObject, ARTHTTPExecutor { var logger: ARTLog? var requests: [NSMutableURLRequest] = [] + var responses: [NSHTTPURLResponse] = [] func executeRequest(request: NSMutableURLRequest, completion callback: ARTHttpRequestCallback?) { self.requests.append(request) - self.executor.executeRequest(request, completion: callback) + self.executor.executeRequest(request, completion: { response, data, error in + if let httpResponse = response { + self.responses.append(httpResponse) + } + callback?(response, data, error) + }) } }