diff --git a/CHANGES.rst b/CHANGES.rst index 453202661..d86cb086f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +Changes in 0.9.4 (2019-09-13) +=============================================== + +Improvements: + * Authentication: Improve the webview used for SSO (#2715). + Changes in 0.9.3 (2019-09-10) =============================================== diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 814e0b268..78a9a1001 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ 3232ABBC2257BE6500AD6A5C /* DeviceVerificationVerifyViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3232ABB42257BE6400AD6A5C /* DeviceVerificationVerifyViewAction.swift */; }; 3232ABC022594C0900AD6A5C /* VerifyEmojiCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3232ABBF22594C0900AD6A5C /* VerifyEmojiCollectionViewCell.swift */; }; 3232ABC2225B996200AD6A5C /* Themable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3232ABC1225B996100AD6A5C /* Themable.swift */; }; + 323AB947232BD74600C1451F /* AuthFallBackViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 323AB946232BD74600C1451F /* AuthFallBackViewController.m */; }; 324A204F225FC571004FE8B0 /* DeviceVerificationIncomingViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 324A2047225FC571004FE8B0 /* DeviceVerificationIncomingViewController.storyboard */; }; 324A2050225FC571004FE8B0 /* DeviceVerificationIncomingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324A2048225FC571004FE8B0 /* DeviceVerificationIncomingViewController.swift */; }; 324A2051225FC571004FE8B0 /* DeviceVerificationIncomingViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324A2049225FC571004FE8B0 /* DeviceVerificationIncomingViewState.swift */; }; @@ -651,6 +652,8 @@ 3232ABB42257BE6400AD6A5C /* DeviceVerificationVerifyViewAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationVerifyViewAction.swift; sourceTree = ""; }; 3232ABBF22594C0900AD6A5C /* VerifyEmojiCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyEmojiCollectionViewCell.swift; sourceTree = ""; }; 3232ABC1225B996100AD6A5C /* Themable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Themable.swift; sourceTree = ""; }; + 323AB945232BD74600C1451F /* AuthFallBackViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AuthFallBackViewController.h; sourceTree = ""; }; + 323AB946232BD74600C1451F /* AuthFallBackViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AuthFallBackViewController.m; sourceTree = ""; }; 324A2047225FC571004FE8B0 /* DeviceVerificationIncomingViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = DeviceVerificationIncomingViewController.storyboard; sourceTree = ""; }; 324A2048225FC571004FE8B0 /* DeviceVerificationIncomingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationIncomingViewController.swift; sourceTree = ""; }; 324A2049225FC571004FE8B0 /* DeviceVerificationIncomingViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationIncomingViewState.swift; sourceTree = ""; }; @@ -1584,6 +1587,15 @@ path = Views; sourceTree = ""; }; + 323AB944232BD71900C1451F /* Fallback */ = { + isa = PBXGroup; + children = ( + 323AB945232BD74600C1451F /* AuthFallBackViewController.h */, + 323AB946232BD74600C1451F /* AuthFallBackViewController.m */, + ); + path = Fallback; + sourceTree = ""; + }; 324A2046225FC571004FE8B0 /* Incoming */ = { isa = PBXGroup; children = ( @@ -2254,6 +2266,7 @@ B1B5568420EE6C4C00210D55 /* Home */ = { isa = PBXGroup; children = ( + 323AB944232BD71900C1451F /* Fallback */, B1B5568620EE6C4C00210D55 /* HomeViewController.h */, B1B5568520EE6C4C00210D55 /* HomeViewController.m */, B1B5593D20EF7BD000210D55 /* Views */, @@ -4428,6 +4441,7 @@ B1B558FA20EF768F00210D55 /* RoomMembershipBubbleCell.m in Sources */, 3232ABA1225730E100AD6A5C /* DeviceVerificationCoordinatorType.swift in Sources */, B1C562D9228C0B760037F12A /* RoomContextualMenuItem.swift in Sources */, + 323AB947232BD74600C1451F /* AuthFallBackViewController.m in Sources */, B1C562E1228C7C8C0037F12A /* RoomContextualMenuToolbarView.swift in Sources */, B1B557BF20EF5B4500210D55 /* DisabledRoomInputToolbarView.m in Sources */, B1B5578620EF564900210D55 /* GroupTableViewCellWithSwitch.m in Sources */, diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index 46def1f3a..5839ab726 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -23,8 +23,9 @@ #import "AuthInputsView.h" #import "ForgotPasswordInputsView.h" +#import "AuthFallBackViewController.h" -@interface AuthenticationViewController () +@interface AuthenticationViewController () { /** Store the potential login error received by using a default homeserver different from matrix.org @@ -46,6 +47,8 @@ Server discovery. */ MXAutoDiscovery *autoDiscovery; + + AuthFallBackViewController *authFallBackViewController; } @end @@ -408,6 +411,74 @@ } } + +#pragma mark - Fallback URL display + +- (void)showAuthenticationFallBackView:(NSString*)fallbackPage +{ + // Skip MatrixKit and use a VC instead + if (self.softLogoutCredentials) + { + // Add device_id as query param of the fallback + NSURLComponents *components = [[NSURLComponents alloc] initWithString:fallbackPage]; + + NSMutableArray *queryItems = [components.queryItems mutableCopy]; + if (!queryItems) + { + queryItems = [NSMutableArray array]; + } + + [queryItems addObject:[NSURLQueryItem queryItemWithName:@"device_id" + value:self.softLogoutCredentials.deviceId]]; + + components.queryItems = queryItems; + + fallbackPage = components.URL.absoluteString; + } + + [self showAuthenticationFallBackViewController:fallbackPage]; +} + +- (void)showAuthenticationFallBackViewController:(NSString*)fallbackPage +{ + authFallBackViewController = [[AuthFallBackViewController alloc] initWithURL:fallbackPage]; + authFallBackViewController.delegate = self; + + + authFallBackViewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(dismissFallBackViewController:)]; + + UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:authFallBackViewController]; + [self presentViewController:navigationController animated:YES completion:nil]; +} + +- (void)dismissFallBackViewController:(id)sender +{ + [authFallBackViewController dismissViewControllerAnimated:YES completion:nil]; + authFallBackViewController = nil; +} + + +#pragma mark AuthFallBackViewControllerDelegate + +- (void)authFallBackViewController:(AuthFallBackViewController *)authFallBackViewController + didLoginWithLoginResponse:(MXLoginResponse *)loginResponse +{ + [authFallBackViewController dismissViewControllerAnimated:YES completion:^{ + + MXCredentials *credentials = [[MXCredentials alloc] initWithLoginResponse:loginResponse andDefaultCredentials:nil]; + [self onSuccessfulLogin:credentials]; + }]; + + authFallBackViewController = nil; +} + + +- (void)authFallBackViewControllerDidClose:(AuthFallBackViewController *)authFallBackViewController +{ + [self dismissFallBackViewController:nil]; +} + + - (void)setSoftLogoutCredentials:(MXCredentials *)softLogoutCredentials { [super setSoftLogoutCredentials:softLogoutCredentials]; @@ -693,8 +764,6 @@ { // Do SSO using the fallback URL [self showAuthenticationFallBackView]; - - [ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar]; } else if (sender == self.softLogoutClearDataButton) { diff --git a/Riot/Modules/Home/Fallback/AuthFallBackViewController.h b/Riot/Modules/Home/Fallback/AuthFallBackViewController.h new file mode 100644 index 000000000..97f868f1f --- /dev/null +++ b/Riot/Modules/Home/Fallback/AuthFallBackViewController.h @@ -0,0 +1,40 @@ +/* + Copyright 2019 New Vector Ltd + + 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 "WebViewViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@class AuthFallBackViewController; +@protocol AuthFallBackViewControllerDelegate + +- (void)authFallBackViewController:(AuthFallBackViewController*)authFallBackViewController didLoginWithLoginResponse:(MXLoginResponse*)loginResponse; +- (void)authFallBackViewControllerDidClose:(AuthFallBackViewController*)authFallBackViewController; + +@end + + +/** + `AuthFallBackViewController` handles the display of a Matrix fallback URL for + login, registration and Single-Sign-On. + */ +@interface AuthFallBackViewController : WebViewViewController + +@property (nonatomic, weak, nullable) id delegate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/Home/Fallback/AuthFallBackViewController.m b/Riot/Modules/Home/Fallback/AuthFallBackViewController.m new file mode 100644 index 000000000..b61b6361b --- /dev/null +++ b/Riot/Modules/Home/Fallback/AuthFallBackViewController.m @@ -0,0 +1,217 @@ +/* + Copyright 2019 New Vector Ltd + + 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 "AuthFallBackViewController.h" +#import "AppDelegate.h" + + +// Generic method to make a bridge between JS and the UIWebView +NSString *FallBackViewControllerJavascriptSendObjectMessage = @"window.sendObjectMessage = function(parameters) { \ + var iframe = document.createElement('iframe'); \ + iframe.setAttribute('src', 'js:' + JSON.stringify(parameters)); \ + \ + document.documentElement.appendChild(iframe); \ + iframe.parentNode.removeChild(iframe); \ + iframe = null; \ +};"; + +// The function the fallback page calls when the registration is complete +NSString *FallBackViewControllerJavascriptOnRegistered = @"window.matrixRegistration.onRegistered = function(homeserverUrl, userId, accessToken) { \ + sendObjectMessage({ \ + 'action': 'onRegistered', \ + 'homeServer': homeserverUrl, \ + 'userId': userId, \ + 'accessToken': accessToken \ + }); \ +};"; + +// The function the fallback page calls when the login is complete +NSString *FallBackViewControllerJavascriptOnLogin = @"window.matrixLogin.onLogin = function(response) { \ + sendObjectMessage({ \ + 'action': 'onLogin', \ + 'response': response \ + }); \ +};"; + +@interface AuthFallBackViewController () + +@end + +@implementation AuthFallBackViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + // Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack + // the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK) + webView.customUserAgent = @"Mozilla/5.0"; + + [self clearCookies]; +} + +- (void)clearCookies +{ + // TODO: it would be better to do that at WKWebView init like below + // but this code is part of the kit + // WKWebViewConfiguration *config = [WKWebViewConfiguration new]; + // config.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; + // webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:config]; + + WKWebsiteDataStore *dateStore = [WKWebsiteDataStore defaultDataStore]; + [dateStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] + completionHandler:^(NSArray * __nonnull records) + { + for (WKWebsiteDataRecord *record in records) + { + [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes + forDataRecords:@[record] + completionHandler:^ + { + NSLog(@"[AuthFallBackViewController] clearCookies: Cookies for %@ deleted successfully", record.displayName); + }]; + } + }]; +} + +- (void)showErrorAsAlert:(NSError*)error +{ + NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey]; + NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey]; + if (!title) + { + if (msg) + { + title = msg; + msg = nil; + } + else + { + title = [NSBundle mxk_localizedStringForKey:@"error"]; + } + } + + MXWeakify(self); + + UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + if (self.delegate) + { + [self.delegate authFallBackViewControllerDidClose:self]; + } + + }]]; + + [self presentViewController:alert animated:YES completion:nil]; +} + + +#pragma mark - WKNavigationDelegate + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation +{ + [super webView:webView didFinishNavigation:navigation]; + + // Set up JS <-> iOS bridge + [webView evaluateJavaScript:FallBackViewControllerJavascriptSendObjectMessage completionHandler:nil]; + [webView evaluateJavaScript:FallBackViewControllerJavascriptOnRegistered completionHandler:nil]; + [webView evaluateJavaScript:FallBackViewControllerJavascriptOnLogin completionHandler:nil]; + + // Check connectivity + if ([AppDelegate theDelegate].isOffline) + { + NSError *error = [NSError errorWithDomain:NSURLErrorDomain + code:NSURLErrorNotConnectedToInternet + userInfo:@{ + NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"network_offline_prompt", @"Vector", nil) + }]; + [self showErrorAsAlert:error]; + } +} + +- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler +{ + NSString *urlString = navigationAction.request.URL.absoluteString; + + // TODO: We should use the WebKit PostMessage API and the + // `didReceiveScriptMessage` delegate to manage the JS<->Native bridge + if ([urlString hasPrefix:@"js:"]) + { + // Listen only to scheme of the JS-UIWebView bridge + NSString *jsonString = [[[urlString componentsSeparatedByString:@"js:"] lastObject] stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding]; + NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + + NSError *error; + NSDictionary *parameters = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers + error:&error]; + + if (!error) + { + if ([@"onRegistered" isEqualToString:parameters[@"action"]]) + { + // Translate the JS registration event to MXLoginResponse + // We cannot use [MXLoginResponse modelFromJSON:] because of https://github.com/matrix-org/synapse/issues/4756 + // Because of this issue, we cannot get the device_id allocated by the homeserver + // TODO: Fix it once the homeserver issue is fixed (filed at https://github.com/vector-im/riot-meta/issues/273). + MXLoginResponse *loginResponse = [MXLoginResponse new]; + loginResponse.homeserver = parameters[@"homeServer"]; + loginResponse.userId = parameters[@"userId"]; + loginResponse.accessToken = parameters[@"accessToken"]; + + // Sanity check + if (self.delegate + && loginResponse.homeserver.length && loginResponse.userId.length && loginResponse.accessToken.length) + { + // And inform the client + [self.delegate authFallBackViewController:self didLoginWithLoginResponse:loginResponse]; + } + } + else if ([@"onLogin" isEqualToString:parameters[@"action"]]) + { + // Translate the JS login event to MXLoginResponse + MXLoginResponse *loginResponse; + MXJSONModelSetMXJSONModel(loginResponse, MXLoginResponse, parameters[@"response"]); + + // Sanity check + if (self.delegate + && loginResponse.homeserver.length && loginResponse.userId.length && loginResponse.accessToken.length) + { + // And inform the client + [self.delegate authFallBackViewController:self didLoginWithLoginResponse:loginResponse]; + } + } + } + + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + + if (navigationAction.navigationType == WKNavigationTypeLinkActivated) + { + // Open links outside the app + [[UIApplication sharedApplication] openURL:navigationAction.request.URL options:@{} completionHandler:nil]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + + decisionHandler(WKNavigationActionPolicyAllow); +} + +@end