blob: 725ce860fce8f4d5c7b6e6d19a53d72fbd221c25 [file]
/*
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