Skip to content

Commit

Permalink
Add custom browser support
Browse files Browse the repository at this point in the history
- An UICoordinator for iOS to support auth with a custom iOS browser like Chrome or Firefox.
- Implements #200.
  • Loading branch information
WilliamDenniss committed Feb 10, 2018
1 parent 8d6aabe commit 135f99d
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 4 deletions.
10 changes: 10 additions & 0 deletions AppAuth.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,9 @@
343AAB9B1E834A8800F9D36E /* AppAuth.h in Headers */ = {isa = PBXBuildFile; fileRef = 343AAA4D1E8345B600F9D36E /* AppAuth.h */; settings = {ATTRIBUTES = (Public, ); }; };
343AAB9C1E834A8900F9D36E /* AppAuth.h in Headers */ = {isa = PBXBuildFile; fileRef = 343AAA4D1E8345B600F9D36E /* AppAuth.h */; settings = {ATTRIBUTES = (Public, ); }; };
343AAB9D1E834A8A00F9D36E /* AppAuth.h in Headers */ = {isa = PBXBuildFile; fileRef = 343AAA4D1E8345B600F9D36E /* AppAuth.h */; settings = {ATTRIBUTES = (Public, ); }; };
345AE747202D526900738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 345AE745202D526800738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.m */; };
345AE748202D526900738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 345AE745202D526800738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.m */; };
345AE749202D526900738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.h in Headers */ = {isa = PBXBuildFile; fileRef = 345AE746202D526800738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.h */; settings = {ATTRIBUTES = (Public, ); }; };
347423E41E7F3C4000D3E6D6 /* OIDAuthorizationResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 341741B71C5D8243000EF209 /* OIDAuthorizationResponse.m */; };
347423FF1E7F4BA000D3E6D6 /* OIDAuthorizationRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 341741B51C5D8243000EF209 /* OIDAuthorizationRequest.m */; };
347424001E7F4BA000D3E6D6 /* OIDAuthorizationResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 341741B71C5D8243000EF209 /* OIDAuthorizationResponse.m */; };
Expand Down Expand Up @@ -542,6 +545,8 @@
343AAAAE1E83489A00F9D36E /* AppAuth_tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppAuth_tvOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
343AAAC21E8348A900F9D36E /* AppAuth.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppAuth.framework; sourceTree = BUILT_PRODUCTS_DIR; };
343AAACA1E8348AA00F9D36E /* AppAuth_macOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppAuth_macOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
345AE745202D526800738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OIDAuthorizationUICoordinatorCustomBrowser.m; path = iOS/OIDAuthorizationUICoordinatorCustomBrowser.m; sourceTree = "<group>"; };
345AE746202D526800738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OIDAuthorizationUICoordinatorCustomBrowser.h; path = iOS/OIDAuthorizationUICoordinatorCustomBrowser.h; sourceTree = "<group>"; };
347423F61E7F4B5600D3E6D6 /* libAppAuth-watchOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libAppAuth-watchOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
34D5EC431E6D1AD900814354 /* OIDAppAuthTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OIDAppAuthTests-Bridging-Header.h"; sourceTree = "<group>"; };
34D5EC441E6D1AD900814354 /* OIDSwiftTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OIDSwiftTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -847,6 +852,8 @@
children = (
F6F60FB31D2BFEFE00325CB3 /* OIDAuthorizationService+IOS.h */,
F6F60FB11D2BFEFE00325CB3 /* OIDAuthorizationService+IOS.m */,
345AE746202D526800738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.h */,
345AE745202D526800738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.m */,
F6F60FB51D2BFEFE00325CB3 /* OIDAuthState+IOS.h */,
F6F60FB01D2BFEFE00325CB3 /* OIDAuthState+IOS.m */,
F6F60FB41D2BFEFE00325CB3 /* OIDAuthorizationUICoordinatorIOS.h */,
Expand Down Expand Up @@ -874,6 +881,7 @@
343AAAE41E83499000F9D36E /* OIDAuthorizationResponse.h in Headers */,
343AAAF31E83499000F9D36E /* OIDScopes.h in Headers */,
343AAAE81E83499000F9D36E /* OIDAuthStateChangeDelegate.h in Headers */,
345AE749202D526900738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.h in Headers */,
343AAA6B1E83465500F9D36E /* AppAuth.h in Headers */,
343AAA6E1E83466B00F9D36E /* OIDAuthorizationUICoordinatorIOS.h in Headers */,
343AAAF21E83499000F9D36E /* OIDResponseTypes.h in Headers */,
Expand Down Expand Up @@ -1457,6 +1465,7 @@
60140F7A1DE4276800DA0DC3 /* OIDClientMetadataParameters.m in Sources */,
341741DE1C5D8243000EF209 /* OIDAuthState.m in Sources */,
341741DD1C5D8243000EF209 /* OIDAuthorizationService.m in Sources */,
345AE747202D526900738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.m in Sources */,
340DAECD1D582DE100EC285B /* OIDAuthorizationUICoordinatorIOS.m in Sources */,
341741EB1C5D8243000EF209 /* OIDURLQueryComponent.m in Sources */,
341741E11C5D8243000EF209 /* OIDFieldMapping.m in Sources */,
Expand Down Expand Up @@ -1579,6 +1588,7 @@
343AAA931E83478900F9D36E /* OIDTokenUtilities.m in Sources */,
343AAA901E83478900F9D36E /* OIDServiceDiscovery.m in Sources */,
343AAA911E83478900F9D36E /* OIDTokenRequest.m in Sources */,
345AE748202D526900738D22 /* OIDAuthorizationUICoordinatorCustomBrowser.m in Sources */,
343AAA6F1E83467D00F9D36E /* OIDAuthorizationService+IOS.m in Sources */,
343AAA8F1E83478900F9D36E /* OIDServiceConfiguration.m in Sources */,
343AAA891E83478900F9D36E /* OIDRegistrationResponse.m in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Source/AppAuth.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#elif TARGET_OS_IOS
#import "OIDAuthState+IOS.h"
#import "OIDAuthorizationService+IOS.h"
#import "OIDAuthorizationUICoordinatorCustomBrowser.h"
#import "OIDAuthorizationUICoordinatorIOS.h"
#elif TARGET_OS_MAC
#import "OIDAuthState+Mac.h"
Expand Down
1 change: 1 addition & 0 deletions Source/Framework/AppAuth.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ FOUNDATION_EXPORT const unsigned char AppAuthVersionString[];
#elif TARGET_OS_IOS
#import <AppAuth/OIDAuthState+IOS.h>
#import <AppAuth/OIDAuthorizationService+IOS.h>
#import <AppAuth/OIDAuthorizationUICoordinatorCustomBrowser.h>
#import <AppAuth/OIDAuthorizationUICoordinatorIOS.h>
#elif TARGET_OS_MAC
#import <AppAuth/OIDAuthState+Mac.h>
Expand Down
6 changes: 6 additions & 0 deletions Source/OIDURLQueryComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ extern BOOL gOIDURLQueryComponentForceIOS7Handling;
*/
- (NSString *)URLEncodedParameters;

/*! @brief A NSMutableCharacterSet containing allowed characters in URL parameter values (that is
the "value" part of "?key=value"). This has less allowed characters than
@c URLQueryAllowedCharacterSet, as the query component includes both the key & value.
*/
+ (NSMutableCharacterSet *)URLParamValueAllowedCharacters;

@end

NS_ASSUME_NONNULL_END
14 changes: 10 additions & 4 deletions Source/OIDURLQueryComponent.m
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ - (void)addParameters:(NSDictionary<NSString *, NSString *> *)parameters {
return queryParameters;
}

+ (NSMutableCharacterSet *)URLParamValueAllowedCharacters {
// Starts with the standard URL-allowed character set.
NSMutableCharacterSet *allowedParamCharacters =
[[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
// Removes additional characters we don't want to see in the query component.
[allowedParamCharacters removeCharactersInString:kQueryStringParamAdditionalDisallowedCharacters];
return allowedParamCharacters;
}

/*! @brief Builds a query string that can be set to @c NSURLComponents.percentEncodedQuery
@discussion This string is percent encoded, and shouldn't be used with
@c NSURLComponents.query.
Expand All @@ -133,10 +142,7 @@ - (NSString *)percentEncodedQueryString {
NSMutableArray<NSString *> *parameterizedValues = [NSMutableArray array];

// Starts with the standard URL-allowed character set.
NSMutableCharacterSet *allowedParamCharacters =
[[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
// Removes additional characters we don't want to see in the query component.
[allowedParamCharacters removeCharactersInString:kQueryStringParamAdditionalDisallowedCharacters];
NSMutableCharacterSet *allowedParamCharacters = [[self class] URLParamValueAllowedCharacters];

for (NSString *parameterName in _parameters.allKeys) {
NSString *encodedParameterName =
Expand Down
106 changes: 106 additions & 0 deletions Source/iOS/OIDAuthorizationUICoordinatorCustomBrowser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*! @file OIDAuthorizationUICoordinatorCustomBrowser.h
@brief AppAuth iOS SDK
@copyright
Copyright 2018 Google LLC
@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 <Foundation/Foundation.h>

#import "OIDAuthorizationUICoordinator.h"

NS_ASSUME_NONNULL_BEGIN

/*! @brief A block that transforms a regular http/https URL into one that will open in an
alternative browser.
@param requestURL the http/https request URL to be transformed.
@return transformed URL.
*/
typedef NSURL *_Nullable (^OIDCustomBrowserURLTransformation)(NSURL *_Nullable requestURL);

/*! @brief An implementation of the OIDAuthorizationUICoordinator protocol for iOS that uses
a custom browser (i.e. not Safari) for authorization. It is suitable for browsers that
offer a custom url scheme that simply replaces the "https" scheme. It is not designed
for browsers that require other modifications to the URL. If the browser is not installed
the user will be prompted to install it.
*/
@interface OIDAuthorizationUICoordinatorCustomBrowser : NSObject<OIDAuthorizationUICoordinator>

/*! @brief URL transformation block for the browser.
*/
@property(nonatomic, readonly) OIDCustomBrowserURLTransformation URLTransformation;

/*! @brief URL Scheme used to test for whether the browser is installed.
*/
@property(nonatomic, readonly, nullable) NSString *canOpenURLScheme;

/*! @brief URL of the browser's App Store listing.
*/
@property(nonatomic, readonly, nullable) NSURL *appStoreURL;

/*! @brief An instance of @c OIDAuthorizationUICoordinatorCustomBrowser for Chrome.
*/
+ (instancetype)CustomBrowserChrome;

/*! @brief An instance of @c OIDAuthorizationUICoordinatorCustomBrowser for Firefox.
*/
+ (instancetype)CustomBrowserFirefox;

/*! @brief An instance of @c OIDAuthorizationUICoordinatorCustomBrowser for Opera.
*/
+ (instancetype)CustomBrowserOpera;

/*! @brief An instance of @c OIDAuthorizationUICoordinatorCustomBrowser for Safari.
*/
+ (instancetype)CustomBrowserSafari;

/*! @brief Creates a @c OIDCustomBrowserURLTransformation using the scheme substitution method used
iOS browsers like Chrome and Firefox.
*/
+ (OIDCustomBrowserURLTransformation)
URLTransformationSchemeSubstitutionHTTPS:(NSString *)browserSchemeHTTPS
HTTP:(nullable NSString *)browserSchemeHTTP;

/*! @brief Creates a @c OIDCustomBrowserURLTransformation with the URL prefix method used by
iOS browsers like Firefox.
*/
+ (OIDCustomBrowserURLTransformation) URLTransformationSchemeConcatPrefix:(NSString*)URLprefix;

/*! @internal
@brief Unavailable. Please use @c initWithURLTransformation:canOpenURLScheme:appStoreURL:
*/
- (nonnull instancetype)init NS_UNAVAILABLE;

/*! @brief UICoordinator for a custom browser. @c presentAuthorizationRequest:session method
will return NO if the browser isn't installed.
*/
- (nullable instancetype)initWithURLTransformation:(OIDCustomBrowserURLTransformation)URLTransformation;

/*! @brief The designated initializer.
@param URLTransformation the transformation block to translate the URL into one that will open
in the desired custom browser.
@param canOpenURLScheme any scheme supported by the browser used to check if the browser is
installed.
@param appStoreURL URL of the browser in the app store. When this and @c canOpenURLScheme
are non-nil, @c presentAuthorizationRequest:session will redirect the user to the app store
if the browser is not installed.
*/
- (nullable instancetype)initWithURLTransformation:(OIDCustomBrowserURLTransformation)URLTransformation
canOpenURLScheme:(nullable NSString *)canOpenURLScheme
appStoreURL:(nullable NSURL *)appStoreURL
NS_DESIGNATED_INITIALIZER;

@end

NS_ASSUME_NONNULL_END
168 changes: 168 additions & 0 deletions Source/iOS/OIDAuthorizationUICoordinatorCustomBrowser.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*! @file OIDAuthorizationUICoordinatorCustomBrowser.m
@brief AppAuth iOS SDK
@copyright
Copyright 2018 Google LLC
@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 "OIDAuthorizationUICoordinatorCustomBrowser.h"

#import <UIKit/UIKit.h>

#import "OIDAuthorizationRequest.h"
#import "OIDAuthorizationService.h"
#import "OIDErrorUtilities.h"
#import "OIDURLQueryComponent.h"

NS_ASSUME_NONNULL_BEGIN

@implementation OIDAuthorizationUICoordinatorCustomBrowser {
OIDCustomBrowserURLTransformation _URLTransformation;
NSString *_canOpenURLScheme;
NSURL *_appStoreURL;
}

@synthesize URLTransformation = _URLTransformation;
@synthesize canOpenURLScheme = _canOpenURLScheme;
@synthesize appStoreURL = _appStoreURL;

+ (instancetype)CustomBrowserChrome {
// Chrome iOS documentation: https://developer.chrome.com/multidevice/ios/links
OIDCustomBrowserURLTransformation transform = [[self class] URLTransformationSchemeSubstitutionHTTPS:@"googlechromes" HTTP:@"googlechrome"];
NSURL *appStoreURL =
[NSURL URLWithString:@"itms-apps://itunes.apple.com/us/app/chrome/id535886823"];
return [[[self class] alloc] initWithURLTransformation:transform
canOpenURLScheme:@"googlechromes"
appStoreURL:appStoreURL];
}

+ (instancetype)CustomBrowserFirefox {
// Firefox iOS documentation: https://github.com/mozilla-mobile/firefox-ios-open-in-client
OIDCustomBrowserURLTransformation transform =
[[self class] URLTransformationSchemeConcatPrefix:@"firefox://open-url?url="];
NSURL *appStoreURL =
[NSURL URLWithString:@"itms-apps://itunes.apple.com/us/app/firefox-web-browser/id989804926"];
return [[[self class] alloc] initWithURLTransformation:transform
canOpenURLScheme:@"firefox"
appStoreURL:appStoreURL];
}

+ (instancetype)CustomBrowserOpera {
OIDCustomBrowserURLTransformation transform =
[[self class] URLTransformationSchemeSubstitutionHTTPS:@"opera-https" HTTP:@"opera-http"];
NSURL *appStoreURL =
[NSURL URLWithString:@"itms-apps://itunes.apple.com/us/app/opera-mini-web-browser/id363729560"];
return [[[self class] alloc] initWithURLTransformation:transform
canOpenURLScheme:@"opera-https"
appStoreURL:appStoreURL];
}

+ (instancetype)CustomBrowserSafari {
OIDCustomBrowserURLTransformation transformNOP = ^NSURL *(NSURL *requestURL) {
return requestURL;
};
OIDAuthorizationUICoordinatorCustomBrowser *coordinator =
[[[self class] alloc] initWithURLTransformation:transformNOP];
return coordinator;
}

+ (OIDCustomBrowserURLTransformation)
URLTransformationSchemeSubstitutionHTTPS:(NSString *)browserSchemeHTTPS
HTTP:(nullable NSString *)browserSchemeHTTP {
OIDCustomBrowserURLTransformation transform = ^NSURL *(NSURL *requestURL) {
// Replace the URL Scheme with the Chrome equivalent.
NSString *newScheme = nil;
if ([requestURL.scheme isEqualToString:@"https"]) {
newScheme = browserSchemeHTTPS;
} else if ([requestURL.scheme isEqualToString:@"http"]) {
if (!browserSchemeHTTP) {
NSAssert(false, @"No HTTP scheme registered for browser");
return nil;
}
newScheme = browserSchemeHTTP;
}

// Replaces the URI scheme with the custom scheme
NSURLComponents *components = [NSURLComponents componentsWithURL:requestURL
resolvingAgainstBaseURL:YES];
components.scheme = newScheme;
return components.URL;
};
return transform;
}

+ (OIDCustomBrowserURLTransformation)URLTransformationSchemeConcatPrefix:(NSString *)URLprefix {
OIDCustomBrowserURLTransformation transform = ^NSURL *(NSURL *requestURL) {
NSString *requestURLString = [requestURL absoluteString];
NSMutableCharacterSet *allowedParamCharacters =
[OIDURLQueryComponent URLParamValueAllowedCharacters];
NSString *encodedUrl = [requestURLString stringByAddingPercentEncodingWithAllowedCharacters:allowedParamCharacters];
NSString *newURL = [NSString stringWithFormat:@"%@%@", URLprefix, encodedUrl];
return [NSURL URLWithString:newURL];
};
return transform;
}

- (nullable instancetype)initWithURLTransformation:
(OIDCustomBrowserURLTransformation)URLTransformation {
return [self initWithURLTransformation:URLTransformation canOpenURLScheme:nil appStoreURL:nil];
}

- (nullable instancetype)
initWithURLTransformation:(OIDCustomBrowserURLTransformation)URLTransformation
canOpenURLScheme:(nullable NSString *)canOpenURLScheme
appStoreURL:(nullable NSURL *)appStoreURL {
self = [super init];
if (self) {
_URLTransformation = URLTransformation;
_canOpenURLScheme = canOpenURLScheme;
_appStoreURL = appStoreURL;
}
return self;
}

- (BOOL)presentAuthorizationRequest:(OIDAuthorizationRequest *)request
session:(id<OIDAuthorizationFlowSession>)session {
// If the app store URL is set, checks if the app is installed and if not opens the app store.
if (_appStoreURL && _canOpenURLScheme) {
// Verifies existence of LSApplicationQueriesSchemes Info.plist key.
NSArray __unused* canOpenURLs =
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"LSApplicationQueriesSchemes"];
NSAssert(canOpenURLs, @"plist missing LSApplicationQueriesSchemes key");
NSAssert1([canOpenURLs containsObject:_canOpenURLScheme],
@"plist missing LSApplicationQueriesSchemes entry for '%@'", _canOpenURLScheme);

// Opens AppStore if app isn't installed
NSString *testURLString = [NSString stringWithFormat:@"%@://example.com", _canOpenURLScheme];
NSURL *testURL = [NSURL URLWithString:testURLString];
if (![[UIApplication sharedApplication] canOpenURL:testURL]) {
[[UIApplication sharedApplication] openURL:_appStoreURL];
return NO;
}
}

// Transforms the request URL and opens it.
NSURL *requestURL = [request authorizationRequestURL];
requestURL = _URLTransformation(requestURL);
BOOL openedInBrowser = [[UIApplication sharedApplication] openURL:requestURL];
return openedInBrowser;
}

- (void)dismissAuthorizationAnimated:(BOOL)animated completion:(nonnull void (^)(void))completion {
completion();
}

@end

NS_ASSUME_NONNULL_END

0 comments on commit 135f99d

Please sign in to comment.