| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you 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 "CMISOAuthAuthenticationProvider.h" |
| #import "CMISOAuthToken.h" |
| #import "CMISLog.h" |
| #import "CMISBindingSession.h" |
| #import "CMISErrors.h" |
| #import "CMISHttpResponse.h" |
| #import "CMISHttpRequest.h" |
| #import "CMISURLUtil.h" |
| #import "CMISDictionaryUtil.h" |
| #import "CMISDefaultNetworkProvider.h" |
| #import "CMISOAuthHttpRequest.h" |
| #import "CMISOAuthHttpResponse.h" |
| |
| @interface CMISOAuthAuthenticationProvider () |
| |
| @property (nonatomic, strong, readwrite) CMISOAuthToken *token; |
| @property (nonatomic, strong) NSNumber *defaultTokenLifetime; |
| |
| @property (nonatomic, weak) CMISBindingSession *session; |
| |
| @property (nonatomic, strong) NSOperationQueue *operationQueue; |
| |
| @end |
| |
| @interface CMISOAuthFetchOperation : NSOperation { |
| BOOL executing; |
| BOOL finished; |
| } |
| |
| @property (nonatomic, weak) CMISOAuthAuthenticationProvider *authProvider; |
| @property (nonatomic, weak) NSThread *originalThread; |
| @property (nonatomic, copy) void (^operationCompletionBlock)(NSString *accessToken, NSError *error); |
| |
| - (instancetype)initWithOAuthProvider:(CMISOAuthAuthenticationProvider *)authProvider completionBlock:(void(^)(NSString *accessToken, NSError *error))completionBlock; |
| - (void)completeOperation; |
| |
| @end |
| |
| @implementation CMISOAuthAuthenticationProvider |
| |
| - (instancetype)init |
| { |
| self = [super init]; |
| if (self) { |
| self.defaultTokenLifetime = [NSNumber numberWithInt:3600]; |
| |
| self.operationQueue = [[NSOperationQueue alloc] init]; |
| /* We need a serial queue as we need to make sure that only one thread at a time requests or refreshes the token |
| * We cannot use a NSLock object to synchronize or else we would run into a deadlock because of the asynchronous call to the server */ |
| [self.operationQueue setMaxConcurrentOperationCount:1]; |
| if ([self.operationQueue respondsToSelector:@selector(setQualityOfService:)]) { |
| // Supported since iOS8 |
| [self.operationQueue setQualityOfService:NSQualityOfServiceBackground]; |
| } |
| } |
| return self; |
| } |
| |
| - (void)setSession:(CMISBindingSession *)session |
| { |
| _session = session; |
| |
| if (self.token == nil) { |
| // get predefined access token |
| NSString *accessToken = nil; |
| if ([[self.session objectForKey:kCMISSessionParameterOAuthAccessToken] isKindOfClass:NSString.class]) { |
| accessToken = (NSString *)[self.session objectForKey:kCMISSessionParameterOAuthAccessToken]; |
| } |
| |
| // get predefined refresh token |
| NSString *refreshToken = nil; |
| if ([[self.session objectForKey:kCMISSessionParameterOAuthRefreshToken] isKindOfClass:NSString.class]) { |
| refreshToken = (NSString *)[self.session objectForKey:kCMISSessionParameterOAuthRefreshToken]; |
| } |
| |
| // get predefined expiration timestamp |
| NSNumber *expirationTimestamp = [NSNumber numberWithInt:0]; |
| if ([[self.session objectForKey:kCMISSessionParameterOAuthExpirationTimestamp] isKindOfClass:NSString.class]) { |
| expirationTimestamp = [NSNumber numberWithLongLong:[[self.session objectForKey:kCMISSessionParameterOAuthExpirationTimestamp] longLongValue]]; |
| } else if ([[self.session objectForKey:kCMISSessionParameterOAuthExpirationTimestamp] isKindOfClass:NSNumber.class]) { |
| expirationTimestamp = (NSNumber *)[self.session objectForKey:kCMISSessionParameterOAuthExpirationTimestamp]; |
| } |
| |
| // get default token lifetime |
| if ([[self.session objectForKey:kCMISSessionParameterOAuthDefaultTokenLifetime] isKindOfClass:NSString.class]) { |
| self.defaultTokenLifetime = [NSNumber numberWithLongLong:[[self.session objectForKey:kCMISSessionParameterOAuthDefaultTokenLifetime] longLongValue]]; |
| } else if ([[self.session objectForKey:kCMISSessionParameterOAuthDefaultTokenLifetime] isKindOfClass:NSNumber.class]) { |
| self.defaultTokenLifetime = (NSNumber *)[self.session objectForKey:kCMISSessionParameterOAuthDefaultTokenLifetime]; |
| } |
| |
| self.token = [[CMISOAuthToken alloc] initWithAccessToken:accessToken refreshToken:refreshToken expirationTimestamp:expirationTimestamp]; |
| } |
| } |
| |
| - (void)asyncHttpHeadersToApply:(void(^)(NSDictionary *headers, NSError *cmisError))completionBlock |
| { |
| CMISOAuthFetchOperation *oAuthFetchOperation = [[CMISOAuthFetchOperation alloc] initWithOAuthProvider:self completionBlock:^(NSString *accessToken, NSError *error) { |
| if (error) { |
| completionBlock(nil, error); |
| } else { |
| NSMutableDictionary *headers = [[super httpHeadersToApply] mutableCopy]; //TODO check if httpHeadersToApply should be called |
| if(!headers) { |
| headers = [NSMutableDictionary new]; |
| } |
| |
| [headers setObject:[NSSet setWithObject:[NSString stringWithFormat:@"Bearer %@", accessToken]] forKey:@"Authorization"]; |
| |
| completionBlock(headers, nil); |
| } |
| }]; |
| |
| [self.operationQueue addOperation:oAuthFetchOperation]; |
| } |
| |
| |
| @end |
| |
| @implementation CMISOAuthFetchOperation |
| |
| - (instancetype)initWithOAuthProvider:(CMISOAuthAuthenticationProvider *)authProvider completionBlock:(void (^)(NSString *, NSError *))completionBlock |
| { |
| self = [super init]; |
| if (self) { |
| self.authProvider = authProvider; |
| self.operationCompletionBlock = completionBlock; |
| self.originalThread = [NSThread currentThread]; |
| } |
| return self; |
| } |
| |
| - (void)main |
| { |
| // do not invoke super! |
| |
| if (self.isCancelled) { |
| NSError *error = [CMISErrors createCMISErrorWithCode:kCMISErrorCodeCancelled detailedDescription:@"Could not get token as CMISOAuthFetchOperation was cancelled"]; |
| [self performSelector:@selector(executeCompletionBlockError:) onThread:self.originalThread withObject:error waitUntilDone:NO]; |
| } else { |
| [self accessTokenWithCompletionBlock:^(NSError *error) { |
| // call the completion block on the original thread |
| if (self.originalThread) { |
| if (error) { |
| [self performSelector:@selector(executeCompletionBlockError:) onThread:self.originalThread withObject:error waitUntilDone:NO]; |
| } else { |
| [self performSelector:@selector(executeCompletionBlockAccessToken:) onThread:self.originalThread withObject:self.authProvider.token.accessToken waitUntilDone:NO]; |
| } |
| } |
| }]; |
| } |
| } |
| |
| - (void)accessTokenWithCompletionBlock:(void(^)(NSError *error))completionBlock |
| { |
| void (^continueWithUpdatedToken)(NSError*) = ^(NSError *error) { |
| |
| [self.authProvider.delegate cmisOAuthAuthenticationProvider:self.authProvider didUpdateToken:self.authProvider.token withError:error]; |
| |
| completionBlock(error); |
| }; |
| |
| if (!self.authProvider.token.accessToken) { |
| if (!self.authProvider.token.refreshToken) { |
| [self requestToken:continueWithUpdatedToken]; |
| } else { |
| [self refreshToken:continueWithUpdatedToken]; |
| } |
| } else if ([self.authProvider.token isExpired]) { |
| [self refreshToken:continueWithUpdatedToken]; |
| } else { // we already have a valid token |
| completionBlock(nil); |
| } |
| } |
| |
| - (void)requestToken:(void(^)(NSError *error))completionBlock |
| { |
| if ([CMISLog sharedInstance].logLevel == CMISLogLevelDebug) { |
| CMISLogDebug(@"Requesting new OAuth access token."); |
| } |
| |
| [self makeRequestIsRefresh:NO completionBlock:^(NSError *error) { |
| if ([CMISLog sharedInstance].logLevel == CMISLogLevelTrace) { |
| CMISLogTrace(@"%@", [self.authProvider.token description]); |
| } |
| |
| completionBlock(error); |
| }]; |
| } |
| |
| - (void)refreshToken:(void(^)(NSError *error))completionBlock |
| { |
| if ([CMISLog sharedInstance].logLevel == CMISLogLevelDebug) { |
| CMISLogDebug(@"Refreshing OAuth access token."); |
| } |
| |
| [self makeRequestIsRefresh:YES completionBlock:^(NSError *error) { |
| if ([CMISLog sharedInstance].logLevel == CMISLogLevelTrace) { |
| CMISLogTrace(@"%@", [self.authProvider.token description]); |
| } |
| |
| completionBlock(error); |
| }]; |
| } |
| |
| - (void)makeRequestIsRefresh:(BOOL)isRefresh completionBlock:(void(^)(NSError *error))completionBlock |
| { |
| id tokenEndpoint = [self.authProvider.session objectForKey:kCMISSessionParameterOAuthTokenEndpoint]; |
| if (![tokenEndpoint isKindOfClass:NSString.class]) { |
| completionBlock([CMISErrors createCMISErrorWithCode:kCMISErrorCodeConnection detailedDescription:@"Token endpoint not set!"]); |
| } |
| |
| if (isRefresh && !self.authProvider.token.refreshToken) { |
| completionBlock([CMISErrors createCMISErrorWithCode:kCMISErrorCodeConnection detailedDescription:@"No refresh token!"]); |
| } |
| |
| NSDictionary *headers = @{@"Content-Type" : @"application/x-www-form-urlencoded; charset=UTF-8"}; |
| |
| NSMutableData *requestBody = [NSMutableData new]; |
| // compile request |
| if (isRefresh) { |
| [self appendString:@"grant_type=refresh_token" toData:requestBody]; |
| |
| [self appendString:@"&refresh_token=" toData:requestBody]; |
| [self appendString:[CMISURLUtil encodeUrlParameterValue:self.authProvider.token.refreshToken] toData:requestBody]; |
| } else { |
| [self appendString:@"grant_type=authorization_code" toData:requestBody]; |
| |
| id code = [self.authProvider.session objectForKey:kCMISSessionParameterOAuthCode]; |
| if (code) { |
| [self appendString:@"&code=" toData:requestBody]; |
| [self appendString:[CMISURLUtil encodeUrlParameterValue:[code description]] toData:requestBody]; |
| } |
| |
| id redirectUri = [self.authProvider.session objectForKey:kCMISSessionParameterOAuthRedirectUri]; |
| if (redirectUri) { |
| [self appendString:@"&redirect_uri=" toData:requestBody]; |
| [self appendString:[CMISURLUtil encodeUrlParameterValue:[redirectUri description]] toData:requestBody]; |
| } |
| } |
| |
| id clientId = [self.authProvider.session objectForKey:kCMISSessionParameterOAuthClientId]; |
| if (clientId) { |
| [self appendString:@"&client_id=" toData:requestBody]; |
| [self appendString:[CMISURLUtil encodeUrlParameterValue:[clientId description]] toData:requestBody]; |
| } |
| |
| id clientSecret = [self.authProvider.session objectForKey:kCMISSessionParameterOAuthClientSecret]; |
| if (clientSecret) { |
| [self appendString:@"&client_secret=" toData:requestBody]; |
| [self appendString:[CMISURLUtil encodeUrlParameterValue:[clientSecret description]] toData:requestBody]; |
| } |
| |
| if ([CMISLog sharedInstance].logLevel == CMISLogLevelTrace) { |
| CMISLogTrace(@"Request body: %@", [[NSString alloc] initWithData:requestBody encoding:NSUTF8StringEncoding]); |
| } |
| |
| // request token |
| __weak typeof(self) weakSelf = self; |
| [self invoke:[NSURL URLWithString:(NSString *)tokenEndpoint] |
| httpMethod:HTTP_POST |
| session:self.authProvider.session |
| body:requestBody |
| headers:headers |
| cmisRequest:nil |
| completionBlock:^(CMISHttpResponse *httpResponse, NSError *error) { |
| NSError *cmisError = nil; |
| if (error) { |
| completionBlock(error); |
| } else { |
| // parse response |
| NSDictionary *jsonDictionary = [CMISOAuthHttpResponse parseResponse:httpResponse error:&cmisError]; |
| |
| if(cmisError) { // there was an error parsing the response |
| completionBlock(cmisError); |
| } else { |
| id tokenType = [jsonDictionary cmis_objectForKeyNotNull:@"token_type"]; |
| if (![tokenType isKindOfClass:NSString.class] || [@"bearer" caseInsensitiveCompare:(NSString *) tokenType] != NSOrderedSame) { |
| cmisError = [CMISErrors createCMISErrorWithCode:kCMISErrorCodeConnection detailedDescription:[NSString stringWithFormat:@"Unsupported OAuth token type: %@", tokenType]]; |
| completionBlock(cmisError); |
| return; |
| } |
| |
| id jsonAccessToken = [jsonDictionary cmis_objectForKeyNotNull:@"access_token"]; |
| if (![jsonAccessToken isKindOfClass:NSString.class]) { |
| cmisError = [CMISErrors createCMISErrorWithCode:kCMISErrorCodeConnection detailedDescription:@"Invalid OAuth access_token!"]; |
| completionBlock(cmisError); |
| return; |
| |
| } |
| |
| id jsonRefreshToken = [jsonDictionary cmis_objectForKeyNotNull:@"refresh_token"]; |
| if (jsonRefreshToken && ![jsonRefreshToken isKindOfClass:NSString.class]) { |
| cmisError = [CMISErrors createCMISErrorWithCode:kCMISErrorCodeConnection detailedDescription:@"Invalid OAuth refresh_token!"]; |
| completionBlock(cmisError); |
| return; |
| } |
| |
| NSNumber *expiresIn = weakSelf.authProvider.defaultTokenLifetime; |
| id jsonExpiresIn = [jsonDictionary cmis_objectForKeyNotNull:@"expires_in"]; |
| if (jsonExpiresIn) { |
| if ([jsonExpiresIn isKindOfClass:NSNumber.class]) { |
| expiresIn = (NSNumber *)jsonExpiresIn; |
| } else if ([jsonExpiresIn isKindOfClass:NSString.class]) { |
| expiresIn = [NSNumber numberWithLongLong:[(NSString *)jsonExpiresIn longLongValue]]; |
| } else { |
| cmisError = [CMISErrors createCMISErrorWithCode:kCMISErrorCodeConnection detailedDescription:@"Invalid OAuth expires_in value!"]; |
| completionBlock(cmisError); |
| return; |
| } |
| } |
| |
| if ([expiresIn longLongValue] <= 0) { |
| expiresIn = self.authProvider.defaultTokenLifetime; |
| } |
| |
| weakSelf.authProvider.token = [[CMISOAuthToken alloc] initWithAccessToken:jsonAccessToken refreshToken:jsonRefreshToken expirationTimestamp:[NSNumber numberWithLongLong:([expiresIn longLongValue] * 1000 + [[NSDate date] timeIntervalSince1970] * 1000)]]; // seconds to milliseconds |
| completionBlock(nil); // success |
| } |
| } |
| }]; |
| } |
| |
| - (void)invoke:(NSURL *)url httpMethod:(CMISHttpRequestMethod)httpRequestMethod session:(CMISBindingSession *)session body:(NSData *)body headers:(NSDictionary *)headers cmisRequest:(CMISRequest *)cmisRequest completionBlock:(void (^)(CMISHttpResponse *, NSError *))completionBlock |
| { |
| NSMutableURLRequest *urlRequest = [CMISDefaultNetworkProvider createRequestForUrl:url |
| httpMethod:httpRequestMethod |
| session:session]; |
| if (!cmisRequest.isCancelled) { |
| CMISHttpRequest* request = [CMISOAuthHttpRequest startRequest:urlRequest |
| httpMethod:httpRequestMethod |
| requestBody:body |
| headers:headers |
| session:session |
| completionBlock:completionBlock]; |
| if (request) { |
| cmisRequest.httpRequest = request; |
| } |
| } else { |
| if (completionBlock) { |
| completionBlock(nil, [CMISErrors createCMISErrorWithCode:kCMISErrorCodeCancelled |
| detailedDescription:@"Request was cancelled"]); |
| } |
| } |
| } |
| |
| - (void)appendString:(NSString *)string toData:(NSMutableData *)data |
| { |
| [data appendData:[string dataUsingEncoding:NSUTF8StringEncoding]]; |
| } |
| |
| - (BOOL)isConcurrent { |
| return NO; |
| } |
| |
| - (BOOL)isExecuting { |
| return executing; |
| } |
| |
| - (BOOL)isFinished { |
| return finished; |
| } |
| |
| - (void)completeOperation { |
| [self willChangeValueForKey:@"isFinished"]; |
| [self willChangeValueForKey:@"isExecuting"]; |
| |
| executing = NO; |
| finished = YES; |
| |
| [self didChangeValueForKey:@"isExecuting"]; |
| [self didChangeValueForKey:@"isFinished"]; |
| } |
| |
| - (void)executeCompletionBlockAccessToken:(NSString *)accessToken { |
| [self executeCompletionBlockAccessToken:accessToken error:nil]; |
| } |
| |
| - (void)executeCompletionBlockError:(NSError *)error { |
| [self executeCompletionBlockAccessToken:nil error:error]; |
| } |
| |
| - (void)executeCompletionBlockAccessToken:(NSString *)accessToken error:(NSError *)error { |
| if (self.operationCompletionBlock) { |
| void (^completionBlock)(NSString *accessToken, NSError *error); |
| completionBlock = self.operationCompletionBlock; |
| self.operationCompletionBlock = nil; // Prevent multiple execution if method on this request gets called inside completion block |
| completionBlock(accessToken, error); |
| } |
| [self completeOperation]; |
| } |
| |
| @end |