/* Copyright 2015 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2019 The Matrix.org Foundation C.I.C 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 "MXKAuthenticationViewController.h" #import "MXKAuthInputsEmailCodeBasedView.h" #import "MXKAuthInputsPasswordBasedView.h" #import "MXKAccountManager.h" #import "NSBundle+MatrixKit.h" #import #import "MXKAppSettings.h" #import "MXKSwiftHeader.h" @interface MXKAuthenticationViewController () { /** The matrix REST client used to make matrix API requests. */ MXRestClient *mxRestClient; /** Current request in progress. */ MXHTTPOperation *mxCurrentOperation; /** The MXKAuthInputsView class or a sub-class used when logging in. */ Class loginAuthInputsViewClass; /** The MXKAuthInputsView class or a sub-class used when registering. */ Class registerAuthInputsViewClass; /** The MXKAuthInputsView class or a sub-class used to handle forgot password case. */ Class forgotPasswordAuthInputsViewClass; /** Customized block used to handle unrecognized certificate (nil by default). */ MXHTTPClientOnUnrecognizedCertificate onUnrecognizedCertificateCustomBlock; /** The current authentication fallback URL (if any). */ NSString *authenticationFallback; /** The cancel button added in navigation bar when fallback page is opened. */ UIBarButtonItem *cancelFallbackBarButton; /** The timer used to postpone the registration when the authentication is pending (for example waiting for email validation) */ NSTimer* registrationTimer; /** Identity server discovery. */ MXAutoDiscovery *autoDiscovery; MXHTTPOperation *checkIdentityServerOperation; } /** The identity service used to make identity server API requests. */ @property (nonatomic) MXIdentityService *identityService; @end @implementation MXKAuthenticationViewController #pragma mark - Class methods + (UINib *)nib { return [UINib nibWithNibName:NSStringFromClass([MXKAuthenticationViewController class]) bundle:[NSBundle bundleForClass:[MXKAuthenticationViewController class]]]; } + (instancetype)authenticationViewController { return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKAuthenticationViewController class]) bundle:[NSBundle bundleForClass:[MXKAuthenticationViewController class]]]; } #pragma mark - - (void)finalizeInit { [super finalizeInit]; // Set initial auth type _authType = MXKAuthenticationTypeLogin; _deviceDisplayName = nil; // Initialize authInputs view classes loginAuthInputsViewClass = MXKAuthInputsPasswordBasedView.class; registerAuthInputsViewClass = nil; // No registration flow is supported yet forgotPasswordAuthInputsViewClass = nil; } #pragma mark - - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // Check whether the view controller has been pushed via storyboard if (!_authenticationScrollView) { // Instantiate view controller objects [[[self class] nib] instantiateWithOwner:self options:nil]; } self.authFallbackWebView = [[MXKAuthenticationFallbackWebView alloc] initWithFrame:self.authFallbackWebViewContainer.bounds]; [self.authFallbackWebViewContainer addSubview:self.authFallbackWebView]; [self.authFallbackWebView.leadingAnchor constraintEqualToAnchor:self.authFallbackWebViewContainer.leadingAnchor constant:0].active = YES; [self.authFallbackWebView.trailingAnchor constraintEqualToAnchor:self.authFallbackWebViewContainer.trailingAnchor constant:0].active = YES; [self.authFallbackWebView.topAnchor constraintEqualToAnchor:self.authFallbackWebViewContainer.topAnchor constant:0].active = YES; [self.authFallbackWebView.bottomAnchor constraintEqualToAnchor:self.authFallbackWebViewContainer.bottomAnchor constant:0].active = YES; // Load welcome image from MatrixKit asset bundle self.welcomeImageView.image = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"logoHighRes"]; _authenticationScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth; _subTitleLabel.numberOfLines = 0; _submitButton.enabled = NO; _authSwitchButton.enabled = YES; _homeServerTextField.text = _defaultHomeServerUrl; _identityServerTextField.text = _defaultIdentityServerUrl; // Hide the identity server by default [self setIdentityServerHidden:YES]; // Create here REST client (if homeserver is defined) [self updateRESTClient]; // Localize labels _homeServerLabel.text = [MatrixKitL10n loginHomeServerTitle]; _homeServerTextField.placeholder = [MatrixKitL10n loginServerUrlPlaceholder]; _homeServerInfoLabel.text = [MatrixKitL10n loginHomeServerInfo]; _identityServerLabel.text = [MatrixKitL10n loginIdentityServerTitle]; _identityServerTextField.placeholder = [MatrixKitL10n loginServerUrlPlaceholder]; _identityServerInfoLabel.text = [MatrixKitL10n loginIdentityServerInfo]; [_cancelAuthFallbackButton setTitle:[MatrixKitL10n cancel] forState:UIControlStateNormal]; [_cancelAuthFallbackButton setTitle:[MatrixKitL10n cancel] forState:UIControlStateHighlighted]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTextFieldChange:) name:UITextFieldTextDidChangeNotification object:nil]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self dismissKeyboard]; // close any opened alert if (alert) { [alert dismissViewControllerAnimated:NO completion:nil]; alert = nil; } [[NSNotificationCenter defaultCenter] removeObserver:self name:AFNetworkingReachabilityDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil]; } #pragma mark - Override MXKViewController - (void)onKeyboardShowAnimationComplete { // Report the keyboard view in order to track keyboard frame changes // TODO define inputAccessoryView for each text input // and report the inputAccessoryView.superview of the firstResponder in self.keyboardView. } - (void)setKeyboardHeight:(CGFloat)keyboardHeight { // Deduce the bottom inset for the scroll view (Don't forget the potential tabBar) CGFloat scrollViewInsetBottom = keyboardHeight - self.bottomLayoutGuide.length; // Check whether the keyboard is over the tabBar if (scrollViewInsetBottom < 0) { scrollViewInsetBottom = 0; } UIEdgeInsets insets = self.authenticationScrollView.contentInset; insets.bottom = scrollViewInsetBottom; self.authenticationScrollView.contentInset = insets; } - (void)destroy { self.authInputsView = nil; if (registrationTimer) { [registrationTimer invalidate]; registrationTimer = nil; } if (mxCurrentOperation) { [mxCurrentOperation cancel]; mxCurrentOperation = nil; } [self cancelIdentityServerCheck]; [mxRestClient close]; mxRestClient = nil; authenticationFallback = nil; cancelFallbackBarButton = nil; [super destroy]; } #pragma mark - Class methods - (void)registerAuthInputsViewClass:(Class)authInputsViewClass forAuthType:(MXKAuthenticationType)authType { // Sanity check: accept only MXKAuthInputsView classes or sub-classes NSParameterAssert([authInputsViewClass isSubclassOfClass:MXKAuthInputsView.class]); if (authType == MXKAuthenticationTypeLogin) { loginAuthInputsViewClass = authInputsViewClass; } else if (authType == MXKAuthenticationTypeRegister) { registerAuthInputsViewClass = authInputsViewClass; } else if (authType == MXKAuthenticationTypeForgotPassword) { forgotPasswordAuthInputsViewClass = authInputsViewClass; } } - (void)setAuthType:(MXKAuthenticationType)authType { if (_authType != authType) { _authType = authType; // Cancel external registration parameters if any _externalRegistrationParameters = nil; // Remove the current inputs view self.authInputsView = nil; isPasswordReseted = NO; [self.authInputsContainerView bringSubviewToFront: _authenticationActivityIndicator]; [_authenticationActivityIndicator startAnimating]; } // Restore user interaction self.userInteractionEnabled = YES; if (authType == MXKAuthenticationTypeLogin) { _subTitleLabel.hidden = YES; [_submitButton setTitle:[MatrixKitL10n login] forState:UIControlStateNormal]; [_submitButton setTitle:[MatrixKitL10n login] forState:UIControlStateHighlighted]; [_authSwitchButton setTitle:[MatrixKitL10n createAccount] forState:UIControlStateNormal]; [_authSwitchButton setTitle:[MatrixKitL10n createAccount] forState:UIControlStateHighlighted]; // Update supported authentication flow and associated information (defined in authentication session) [self refreshAuthenticationSession]; } else if (authType == MXKAuthenticationTypeRegister) { _subTitleLabel.hidden = NO; _subTitleLabel.text = [MatrixKitL10n loginCreateAccount]; [_submitButton setTitle:[MatrixKitL10n signUp] forState:UIControlStateNormal]; [_submitButton setTitle:[MatrixKitL10n signUp] forState:UIControlStateHighlighted]; [_authSwitchButton setTitle:[MatrixKitL10n back] forState:UIControlStateNormal]; [_authSwitchButton setTitle:[MatrixKitL10n back] forState:UIControlStateHighlighted]; // Update supported authentication flow and associated information (defined in authentication session) [self refreshAuthenticationSession]; } else if (authType == MXKAuthenticationTypeForgotPassword) { _subTitleLabel.hidden = YES; if (isPasswordReseted) { [_submitButton setTitle:[MatrixKitL10n back] forState:UIControlStateNormal]; [_submitButton setTitle:[MatrixKitL10n back] forState:UIControlStateHighlighted]; } else { [_submitButton setTitle:[MatrixKitL10n submit] forState:UIControlStateNormal]; [_submitButton setTitle:[MatrixKitL10n submit] forState:UIControlStateHighlighted]; [self refreshForgotPasswordSession]; } [_authSwitchButton setTitle:[MatrixKitL10n back] forState:UIControlStateNormal]; [_authSwitchButton setTitle:[MatrixKitL10n back] forState:UIControlStateHighlighted]; } [self checkIdentityServer]; } - (void)setAuthInputsView:(MXKAuthInputsView *)authInputsView { // Here a new view will be loaded, hide first subviews which depend on auth flow _submitButton.hidden = YES; _noFlowLabel.hidden = YES; _retryButton.hidden = YES; if (_authInputsView) { [_authInputsView removeObserver:self forKeyPath:@"viewHeightConstraint.constant"]; [NSLayoutConstraint deactivateConstraints:_authInputsView.constraints]; [_authInputsView removeFromSuperview]; _authInputsView.delegate = nil; [_authInputsView destroy]; _authInputsView = nil; } _authInputsView = authInputsView; CGFloat previousInputsContainerViewHeight = _authInputContainerViewHeightConstraint.constant; if (_authInputsView) { _authInputsView.translatesAutoresizingMaskIntoConstraints = NO; [_authInputsContainerView addSubview:_authInputsView]; _authInputsView.delegate = self; _submitButton.hidden = NO; _authInputsView.hidden = NO; _authInputContainerViewHeightConstraint.constant = _authInputsView.viewHeightConstraint.constant; NSLayoutConstraint* topConstraint = [NSLayoutConstraint constraintWithItem:_authInputsContainerView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:_authInputsView attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f]; NSLayoutConstraint* leadingConstraint = [NSLayoutConstraint constraintWithItem:_authInputsContainerView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:_authInputsView attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f]; NSLayoutConstraint* trailingConstraint = [NSLayoutConstraint constraintWithItem:_authInputsContainerView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:_authInputsView attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f]; [NSLayoutConstraint activateConstraints:@[topConstraint, leadingConstraint, trailingConstraint]]; [_authInputsView addObserver:self forKeyPath:@"viewHeightConstraint.constant" options:0 context:nil]; } else { // No input fields are displayed _authInputContainerViewHeightConstraint.constant = _authInputContainerViewMinHeightConstraint.constant; } [self.view layoutIfNeeded]; // Refresh content view height by considering the updated height of inputs container _contentViewHeightConstraint.constant += (_authInputContainerViewHeightConstraint.constant - previousInputsContainerViewHeight); } - (void)setDefaultHomeServerUrl:(NSString *)defaultHomeServerUrl { _defaultHomeServerUrl = defaultHomeServerUrl; if (!_homeServerTextField.text.length) { [self setHomeServerTextFieldText:defaultHomeServerUrl]; } } - (void)setDefaultIdentityServerUrl:(NSString *)defaultIdentityServerUrl { _defaultIdentityServerUrl = defaultIdentityServerUrl; if (!_identityServerTextField.text.length) { [self setIdentityServerTextFieldText:defaultIdentityServerUrl]; } } - (void)setHomeServerTextFieldText:(NSString *)homeServerUrl { if (!homeServerUrl.length) { // Force refresh with default value homeServerUrl = _defaultHomeServerUrl; } _homeServerTextField.text = homeServerUrl; if (!mxRestClient || ![mxRestClient.homeserver isEqualToString:homeServerUrl]) { [self updateRESTClient]; if (_authType == MXKAuthenticationTypeLogin || _authType == MXKAuthenticationTypeRegister) { // Restore default UI self.authType = _authType; } else { // Refresh the IS anyway [self checkIdentityServer]; } } } - (void)setIdentityServerTextFieldText:(NSString *)identityServerUrl { _identityServerTextField.text = identityServerUrl; [self updateIdentityServerURL:identityServerUrl]; } - (void)updateIdentityServerURL:(NSString*)url { if (![self.identityService.identityServer isEqualToString:url]) { if (url.length) { self.identityService = [[MXIdentityService alloc] initWithIdentityServer:url accessToken:nil andHomeserverRestClient:mxRestClient]; } else { self.identityService = nil; } } [mxRestClient setIdentityServer:url.length ? url : nil]; } - (void)setIdentityServerHidden:(BOOL)hidden { _identityServerContainer.hidden = hidden; } - (void)checkIdentityServer { [self cancelIdentityServerCheck]; // Hide the field while checking data [self setIdentityServerHidden:YES]; NSString *homeserver = mxRestClient.homeserver; // First, fetch the IS advertised by the HS if (homeserver) { MXLogDebug(@"[MXKAuthenticationVC] checkIdentityServer for homeserver %@", homeserver); autoDiscovery = [[MXAutoDiscovery alloc] initWithUrl:homeserver]; MXWeakify(self); checkIdentityServerOperation = [autoDiscovery findClientConfig:^(MXDiscoveredClientConfig * _Nonnull discoveredClientConfig) { MXStrongifyAndReturnIfNil(self); NSString *identityServer = discoveredClientConfig.wellKnown.identityServer.baseUrl; MXLogDebug(@"[MXKAuthenticationVC] checkIdentityServer: Identity server: %@", identityServer); if (identityServer) { // Apply the provided IS [self setIdentityServerTextFieldText:identityServer]; } // Then, check if the HS needs an IS for running MXWeakify(self); MXHTTPOperation *operation = [self checkIdentityServerRequirementWithCompletion:^(BOOL identityServerRequired) { MXStrongifyAndReturnIfNil(self); self->checkIdentityServerOperation = nil; // Show the field only if an IS is required so that the user can customise it [self setIdentityServerHidden:!identityServerRequired]; }]; if (operation) { [self->checkIdentityServerOperation mutateTo:operation]; } else { self->checkIdentityServerOperation = nil; } self->autoDiscovery = nil; } failure:^(NSError *error) { MXStrongifyAndReturnIfNil(self); // No need to report this error to the end user // There will be already an error about failing to get the auth flow from the HS MXLogDebug(@"[MXKAuthenticationVC] checkIdentityServer. Error: %@", error); self->autoDiscovery = nil; }]; } } - (void)cancelIdentityServerCheck { if (checkIdentityServerOperation) { [checkIdentityServerOperation cancel]; checkIdentityServerOperation = nil; } } - (MXHTTPOperation*)checkIdentityServerRequirementWithCompletion:(void (^)(BOOL identityServerRequired))completion { MXHTTPOperation *operation; if (_authType == MXKAuthenticationTypeLogin) { // The identity server is only required for registration and password reset // It is then stored in the user account data completion(NO); } else { operation = [mxRestClient supportedMatrixVersions:^(MXMatrixVersions *matrixVersions) { MXLogDebug(@"[MXKAuthenticationVC] checkIdentityServerRequirement: %@", matrixVersions.doesServerRequireIdentityServerParam ? @"YES": @"NO"); completion(matrixVersions.doesServerRequireIdentityServerParam); } failure:^(NSError *error) { // No need to report this error to the end user // There will be already an error about failing to get the auth flow from the HS MXLogDebug(@"[MXKAuthenticationVC] checkIdentityServerRequirement. Error: %@", error); }]; } return operation; } - (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled { _submitButton.enabled = (userInteractionEnabled && _authInputsView.areAllRequiredFieldsSet); _authSwitchButton.enabled = userInteractionEnabled; _homeServerTextField.enabled = userInteractionEnabled; _identityServerTextField.enabled = userInteractionEnabled; _userInteractionEnabled = userInteractionEnabled; } - (void)refreshAuthenticationSession { // Remove reachability observer [[NSNotificationCenter defaultCenter] removeObserver:self name:AFNetworkingReachabilityDidChangeNotification object:nil]; // Cancel potential request in progress [mxCurrentOperation cancel]; mxCurrentOperation = nil; // Reset potential authentication fallback url authenticationFallback = nil; if (mxRestClient) { if (_authType == MXKAuthenticationTypeLogin) { mxCurrentOperation = [mxRestClient getLoginSession:^(MXAuthenticationSession* authSession) { [self handleAuthenticationSession:authSession]; } failure:^(NSError *error) { [self onFailureDuringMXOperation:error]; }]; } else if (_authType == MXKAuthenticationTypeRegister) { mxCurrentOperation = [mxRestClient getRegisterSession:^(MXAuthenticationSession* authSession){ [self handleAuthenticationSession:authSession]; } failure:^(NSError *error){ [self onFailureDuringMXOperation:error]; }]; } else { // Not supported for other types MXLogDebug(@"[MXKAuthenticationVC] refreshAuthenticationSession is ignored"); } } } - (void)handleAuthenticationSession:(MXAuthenticationSession *)authSession { mxCurrentOperation = nil; [_authenticationActivityIndicator stopAnimating]; // Check whether fallback is defined, and retrieve the right input view class. Class authInputsViewClass; if (_authType == MXKAuthenticationTypeLogin) { authenticationFallback = [mxRestClient loginFallback]; authInputsViewClass = loginAuthInputsViewClass; } else if (_authType == MXKAuthenticationTypeRegister) { authenticationFallback = [mxRestClient registerFallback]; authInputsViewClass = registerAuthInputsViewClass; } else { // Not supported for other types MXLogDebug(@"[MXKAuthenticationVC] handleAuthenticationSession is ignored"); return; } MXKAuthInputsView *authInputsView = nil; if (authInputsViewClass) { // Instantiate a new auth inputs view, except if the current one is already an instance of this class. if (self.authInputsView && self.authInputsView.class == authInputsViewClass) { // Use the current view authInputsView = self.authInputsView; } else { authInputsView = [authInputsViewClass authInputsView]; } } if (authInputsView) { // Apply authentication session on inputs view if ([authInputsView setAuthSession:authSession withAuthType:_authType] == NO) { MXLogDebug(@"[MXKAuthenticationVC] Received authentication settings are not supported"); authInputsView = nil; } else if (!_softLogoutCredentials) { // If all listed flows in this authentication session are not supported we suggest using the fallback page. if (authenticationFallback.length && authInputsView.authSession.flows.count == 0) { MXLogDebug(@"[MXKAuthenticationVC] No supported flow, suggest using fallback page"); authInputsView = nil; } else if (authInputsView.authSession.flows.count != authSession.flows.count) { MXLogDebug(@"[MXKAuthenticationVC] The authentication session contains at least one unsupported flow"); } } } if (authInputsView) { // Check whether the current view must be replaced if (self.authInputsView != authInputsView) { // Refresh layout self.authInputsView = authInputsView; } // Refresh user interaction self.userInteractionEnabled = _userInteractionEnabled; // Check whether an external set of parameters have been defined to pursue a registration if (self.externalRegistrationParameters) { if ([authInputsView setExternalRegistrationParameters:self.externalRegistrationParameters]) { // Launch authentication now [self onButtonPressed:_submitButton]; } else { [self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[MatrixKitL10n notSupportedYet]}]]; _externalRegistrationParameters = nil; // Restore login screen on failure self.authType = MXKAuthenticationTypeLogin; } } if (_softLogoutCredentials) { [authInputsView setSoftLogoutCredentials:_softLogoutCredentials]; } } else { // Remove the potential auth inputs view self.authInputsView = nil; // Cancel external registration parameters if any _externalRegistrationParameters = nil; // Notify user that no flow is supported if (_authType == MXKAuthenticationTypeLogin) { _noFlowLabel.text = [MatrixKitL10n loginErrorDoNotSupportLoginFlows]; } else { _noFlowLabel.text = [MatrixKitL10n loginErrorRegistrationIsNotSupported]; } MXLogDebug(@"[MXKAuthenticationVC] Warning: %@", _noFlowLabel.text); if (authenticationFallback.length) { [_retryButton setTitle:[MatrixKitL10n loginUseFallback] forState:UIControlStateNormal]; [_retryButton setTitle:[MatrixKitL10n loginUseFallback] forState:UIControlStateNormal]; } else { [_retryButton setTitle:[MatrixKitL10n retry] forState:UIControlStateNormal]; [_retryButton setTitle:[MatrixKitL10n retry] forState:UIControlStateNormal]; } _noFlowLabel.hidden = NO; _retryButton.hidden = NO; } } - (void)setExternalRegistrationParameters:(NSDictionary*)parameters { if (parameters.count) { MXLogDebug(@"[MXKAuthenticationVC] setExternalRegistrationParameters"); // Cancel the current operation if any. [self cancel]; // Load the view controller’s view if it has not yet been loaded. // This is required before updating view's textfields (homeserver url...) [self loadViewIfNeeded]; // Force register mode self.authType = MXKAuthenticationTypeRegister; // Apply provided homeserver if any id hs_url = parameters[@"hs_url"]; NSString *homeserverURL = nil; if (hs_url && [hs_url isKindOfClass:NSString.class]) { homeserverURL = hs_url; } [self setHomeServerTextFieldText:homeserverURL]; // Apply provided identity server if any id is_url = parameters[@"is_url"]; NSString *identityURL = nil; if (is_url && [is_url isKindOfClass:NSString.class]) { identityURL = is_url; } [self setIdentityServerTextFieldText:identityURL]; // Disable user interaction self.userInteractionEnabled = NO; // Cancel potential request in progress [mxCurrentOperation cancel]; mxCurrentOperation = nil; // Remove the current auth inputs view self.authInputsView = nil; // Set external parameters and trigger a refresh (the parameters will be taken into account during [handleAuthenticationSession:]) _externalRegistrationParameters = parameters; [self refreshAuthenticationSession]; } else { MXLogDebug(@"[MXKAuthenticationVC] reset externalRegistrationParameters"); _externalRegistrationParameters = nil; // Restore default UI self.authType = _authType; } } - (void)setSoftLogoutCredentials:(MXCredentials *)softLogoutCredentials { MXLogDebug(@"[MXKAuthenticationVC] setSoftLogoutCredentials"); // Cancel the current operation if any. [self cancel]; // Load the view controller’s view if it has not yet been loaded. // This is required before updating view's textfields (homeserver url...) [self loadViewIfNeeded]; // Force register mode self.authType = MXKAuthenticationTypeLogin; [self setHomeServerTextFieldText:softLogoutCredentials.homeServer]; [self setIdentityServerTextFieldText:softLogoutCredentials.identityServer]; // Cancel potential request in progress [mxCurrentOperation cancel]; mxCurrentOperation = nil; // Remove the current auth inputs view self.authInputsView = nil; // Set parameters and trigger a refresh (the parameters will be taken into account during [handleAuthenticationSession:]) _softLogoutCredentials = softLogoutCredentials; [self refreshAuthenticationSession]; } - (void)setOnUnrecognizedCertificateBlock:(MXHTTPClientOnUnrecognizedCertificate)onUnrecognizedCertificateBlock { onUnrecognizedCertificateCustomBlock = onUnrecognizedCertificateBlock; } - (void)isUserNameInUse:(void (^)(BOOL isUserNameInUse))callback { mxCurrentOperation = [mxRestClient isUserNameInUse:self.authInputsView.userId callback:^(BOOL isUserNameInUse) { self->mxCurrentOperation = nil; if (callback) { callback (isUserNameInUse); } }]; } - (void)testUserRegistration:(void (^)(MXError *mxError))callback { mxCurrentOperation = [mxRestClient testUserRegistration:self.authInputsView.userId callback:callback]; } - (IBAction)onButtonPressed:(id)sender { [self dismissKeyboard]; if (sender == _submitButton) { // Disable user interaction to prevent multiple requests self.userInteractionEnabled = NO; // Check parameters validity NSString *errorMsg = [self.authInputsView validateParameters]; if (errorMsg) { [self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:errorMsg}]]; } else { [self.authInputsContainerView bringSubviewToFront: _authenticationActivityIndicator]; // Launch the authentication according to its type if (_authType == MXKAuthenticationTypeLogin) { // Prepare the parameters dict [self.authInputsView prepareParameters:^(NSDictionary *parameters, NSError *error) { if (parameters && self->mxRestClient) { [self->_authenticationActivityIndicator startAnimating]; [self loginWithParameters:parameters]; } else { MXLogDebug(@"[MXKAuthenticationVC] Failed to prepare parameters"); [self onFailureDuringAuthRequest:error]; } }]; } else if (_authType == MXKAuthenticationTypeRegister) { // Check here the availability of the userId if (self.authInputsView.userId.length) { [_authenticationActivityIndicator startAnimating]; if (self.authInputsView.password.length) { // Trigger here a register request in order to associate the filled userId and password to the current session id // This will check the availability of the userId at the same time NSDictionary *parameters = @{@"auth": @{}, @"username": self.authInputsView.userId, @"password": self.authInputsView.password, @"bind_email": @(NO), @"initial_device_display_name":self.deviceDisplayName }; mxCurrentOperation = [mxRestClient registerWithParameters:parameters success:^(NSDictionary *JSONResponse) { // Unexpected case where the registration succeeds without any other stages MXLoginResponse *loginResponse; MXJSONModelSetMXJSONModel(loginResponse, MXLoginResponse, JSONResponse); MXCredentials *credentials = [[MXCredentials alloc] initWithLoginResponse:loginResponse andDefaultCredentials:self->mxRestClient.credentials]; // Sanity check if (!credentials.userId || !credentials.accessToken) { [self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[MatrixKitL10n notSupportedYet]}]]; } else { MXLogDebug(@"[MXKAuthenticationVC] Registration succeeded"); // Report the certificate trusted by user (if any) credentials.allowedCertificate = self->mxRestClient.allowedCertificate; [self onSuccessfulLogin:credentials]; } } failure:^(NSError *error) { self->mxCurrentOperation = nil; // An updated authentication session should be available in response data in case of unauthorized request. NSDictionary *JSONResponse = nil; if (error.userInfo[MXHTTPClientErrorResponseDataKey]) { JSONResponse = error.userInfo[MXHTTPClientErrorResponseDataKey]; } if (JSONResponse) { MXAuthenticationSession *authSession = [MXAuthenticationSession modelFromJSON:JSONResponse]; [self->_authenticationActivityIndicator stopAnimating]; // Update session identifier self.authInputsView.authSession.session = authSession.session; // Launch registration by preparing parameters dict [self.authInputsView prepareParameters:^(NSDictionary *parameters, NSError *error) { if (parameters && self->mxRestClient) { [self->_authenticationActivityIndicator startAnimating]; [self registerWithParameters:parameters]; } else { MXLogDebug(@"[MXKAuthenticationVC] Failed to prepare parameters"); [self onFailureDuringAuthRequest:error]; } }]; } else { [self onFailureDuringAuthRequest:error]; } }]; } else { [self isUserNameInUse:^(BOOL isUserNameInUse) { if (isUserNameInUse) { MXLogDebug(@"[MXKAuthenticationVC] User name is already use"); [self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[MatrixKitL10n authUsernameInUse]}]]; } else { [self->_authenticationActivityIndicator stopAnimating]; // Launch registration by preparing parameters dict [self.authInputsView prepareParameters:^(NSDictionary *parameters, NSError *error) { if (parameters && self->mxRestClient) { [self->_authenticationActivityIndicator startAnimating]; [self registerWithParameters:parameters]; } else { MXLogDebug(@"[MXKAuthenticationVC] Failed to prepare parameters"); [self onFailureDuringAuthRequest:error]; } }]; } }]; } } else if (self.externalRegistrationParameters) { // Launch registration by preparing parameters dict [self.authInputsView prepareParameters:^(NSDictionary *parameters, NSError *error) { if (parameters && self->mxRestClient) { [self->_authenticationActivityIndicator startAnimating]; [self registerWithParameters:parameters]; } else { MXLogDebug(@"[MXKAuthenticationVC] Failed to prepare parameters"); [self onFailureDuringAuthRequest:error]; } }]; } else { MXLogDebug(@"[MXKAuthenticationVC] User name is missing"); [self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[MatrixKitL10n authInvalidUserName]}]]; } } else if (_authType == MXKAuthenticationTypeForgotPassword) { // Check whether the password has been reseted if (isPasswordReseted) { // Return to login screen self.authType = MXKAuthenticationTypeLogin; } else { // Prepare the parameters dict [self.authInputsView prepareParameters:^(NSDictionary *parameters, NSError *error) { if (parameters && self->mxRestClient) { [self->_authenticationActivityIndicator startAnimating]; [self resetPasswordWithParameters:parameters]; } else { MXLogDebug(@"[MXKAuthenticationVC] Failed to prepare parameters"); [self onFailureDuringAuthRequest:error]; } }]; } } } } else if (sender == _authSwitchButton) { if (_authType == MXKAuthenticationTypeLogin) { self.authType = MXKAuthenticationTypeRegister; } else { self.authType = MXKAuthenticationTypeLogin; } } else if (sender == _retryButton) { if (authenticationFallback) { [self showAuthenticationFallBackView:authenticationFallback]; } else { [self refreshAuthenticationSession]; } } else if (sender == _cancelAuthFallbackButton) { // Hide fallback webview [self hideRegistrationFallbackView]; } } - (void)cancel { MXLogDebug(@"[MXKAuthenticationVC] cancel"); // Cancel external registration parameters if any _externalRegistrationParameters = nil; if (registrationTimer) { [registrationTimer invalidate]; registrationTimer = nil; } // Cancel request in progress if (mxCurrentOperation) { [mxCurrentOperation cancel]; mxCurrentOperation = nil; } [_authenticationActivityIndicator stopAnimating]; self.userInteractionEnabled = YES; // Reset potential completed stages self.authInputsView.authSession.completed = nil; // Update authentication inputs view to return in initial step [self.authInputsView setAuthSession:self.authInputsView.authSession withAuthType:_authType]; } - (void)onFailureDuringAuthRequest:(NSError *)error { mxCurrentOperation = nil; [_authenticationActivityIndicator stopAnimating]; self.userInteractionEnabled = YES; // Ignore connection cancellation error if (([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled)) { MXLogDebug(@"[MXKAuthenticationVC] Auth request cancelled"); return; } MXLogDebug(@"[MXKAuthenticationVC] Auth request failed: %@", error); // Cancel external registration parameters if any _externalRegistrationParameters = nil; // Translate the error code to a human message NSString *title = error.localizedFailureReason; if (!title) { if (self.authType == MXKAuthenticationTypeLogin) { title = [MatrixKitL10n loginErrorTitle]; } else if (self.authType == MXKAuthenticationTypeRegister) { title = [MatrixKitL10n registerErrorTitle]; } else { title = [MatrixKitL10n error]; } } NSString* message = error.localizedDescription; NSDictionary* dict = error.userInfo; // detect if it is a Matrix SDK issue if (dict) { NSString* localizedError = [dict valueForKey:@"error"]; NSString* errCode = [dict valueForKey:@"errcode"]; if (localizedError.length > 0) { message = localizedError; } if (errCode) { if ([errCode isEqualToString:kMXErrCodeStringForbidden]) { message = [MatrixKitL10n loginErrorForbidden]; } else if ([errCode isEqualToString:kMXErrCodeStringUnknownToken]) { message = [MatrixKitL10n loginErrorUnknownToken]; } else if ([errCode isEqualToString:kMXErrCodeStringBadJSON]) { message = [MatrixKitL10n loginErrorBadJson]; } else if ([errCode isEqualToString:kMXErrCodeStringNotJSON]) { message = [MatrixKitL10n loginErrorNotJson]; } else if ([errCode isEqualToString:kMXErrCodeStringLimitExceeded]) { message = [MatrixKitL10n loginErrorLimitExceeded]; } else if ([errCode isEqualToString:kMXErrCodeStringUserInUse]) { message = [MatrixKitL10n loginErrorUserInUse]; } else if ([errCode isEqualToString:kMXErrCodeStringLoginEmailURLNotYet]) { message = [MatrixKitL10n loginErrorLoginEmailNotYet]; } else if ([errCode isEqualToString:kMXErrCodeStringResourceLimitExceeded]) { [self showResourceLimitExceededError:dict onAdminContactTapped:nil]; return; } else if (!message.length) { message = errCode; } } } // Alert user if (alert) { [alert dismissViewControllerAnimated:NO completion:nil]; } alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { self->alert = nil; }]]; [self presentViewController:alert animated:YES completion:nil]; // Update authentication inputs view to return in initial step [self.authInputsView setAuthSession:self.authInputsView.authSession withAuthType:_authType]; if (self.softLogoutCredentials) { self.authInputsView.softLogoutCredentials = self.softLogoutCredentials; } } - (void)showResourceLimitExceededError:(NSDictionary *)errorDict onAdminContactTapped:(void (^)(NSURL *adminContact))onAdminContactTapped { mxCurrentOperation = nil; [_authenticationActivityIndicator stopAnimating]; self.userInteractionEnabled = YES; // Alert user if (alert) { [alert dismissViewControllerAnimated:NO completion:nil]; } // Parse error data NSString *limitType, *adminContactString; NSURL *adminContact; MXJSONModelSetString(limitType, errorDict[kMXErrorResourceLimitExceededLimitTypeKey]); MXJSONModelSetString(adminContactString, errorDict[kMXErrorResourceLimitExceededAdminContactKey]); if (adminContactString) { adminContact = [NSURL URLWithString:adminContactString]; } NSString *title = [MatrixKitL10n loginErrorResourceLimitExceededTitle]; // Build the message content NSMutableString *message = [NSMutableString new]; if ([limitType isEqualToString:kMXErrorResourceLimitExceededLimitTypeMonthlyActiveUserValue]) { [message appendString:[MatrixKitL10n loginErrorResourceLimitExceededMessageMonthlyActiveUser]]; } else { [message appendString:[MatrixKitL10n loginErrorResourceLimitExceededMessageDefault]]; } [message appendString:[MatrixKitL10n loginErrorResourceLimitExceededMessageContact]]; // Build the alert alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; MXWeakify(self); if (adminContact && onAdminContactTapped) { [alert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n loginErrorResourceLimitExceededContactButton] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); self->alert = nil; // Let the system handle the URI // It could be something like "mailto: server.admin@example.com" onAdminContactTapped(adminContact); }]]; } [alert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n cancel] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); self->alert = nil; }]]; [self presentViewController:alert animated:YES completion:nil]; // Update authentication inputs view to return in initial step [self.authInputsView setAuthSession:self.authInputsView.authSession withAuthType:_authType]; } - (void)onSuccessfulLogin:(MXCredentials*)credentials { mxCurrentOperation = nil; [_authenticationActivityIndicator stopAnimating]; self.userInteractionEnabled = YES; if (self.softLogoutCredentials) { // Hydrate the account with the new access token MXKAccount *account = [[MXKAccountManager sharedManager] accountForUserId:self.softLogoutCredentials.userId]; [[MXKAccountManager sharedManager] hydrateAccount:account withCredentials:credentials]; if (_delegate) { [_delegate authenticationViewController:self didLogWithUserId:credentials.userId]; } } // Sanity check: check whether the user is not already logged in with this id else if ([[MXKAccountManager sharedManager] accountForUserId:credentials.userId]) { //Alert user __weak typeof(self) weakSelf = self; if (alert) { [alert dismissViewControllerAnimated:NO completion:nil]; } alert = [UIAlertController alertControllerWithTitle:[MatrixKitL10n loginErrorAlreadyLoggedIn] message:nil preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { // We remove the authentication view controller. typeof(self) self = weakSelf; self->alert = nil; [self withdrawViewControllerAnimated:YES completion:nil]; }]]; [self presentViewController:alert animated:YES completion:nil]; } else { // Report the new account in account manager if (!credentials.identityServer) { credentials.identityServer = _identityServerTextField.text; } [self createAccountWithCredentials:credentials]; } } - (MXHTTPOperation *)currentHttpOperation { return mxCurrentOperation; } #pragma mark - Privates // Hook point for triggering device rehydration in subclasses // Avoid cycles by using a separate private method do to the actual work - (void)createAccountWithCredentials:(MXCredentials *)credentials { [self _createAccountWithCredentials:credentials]; } - (void)attemptDeviceRehydrationWithKeyData:(NSData *)keyData credentials:(MXCredentials *)credentials { [self attemptDeviceRehydrationWithKeyData:keyData credentials:credentials retry:YES]; } - (void)attemptDeviceRehydrationWithKeyData:(NSData *)keyData credentials:(MXCredentials *)credentials retry:(BOOL)retry { MXLogDebug(@"[MXKAuthenticationViewController] attemptDeviceRehydration: starting device rehydration"); if (keyData == nil) { MXLogError(@"[MXKAuthenticationViewController] attemptDeviceRehydration: no key provided for device rehydration"); [self _createAccountWithCredentials:credentials]; return; } MXRestClient *mxRestClient = [[MXRestClient alloc] initWithCredentials:credentials andOnUnrecognizedCertificateBlock:^BOOL(NSData *certificate) { return NO; } andPersistentTokenDataHandler:^(void (^handler)(NSArray *credentials, void (^completion)(BOOL didUpdateCredentials))) { [[MXKAccountManager sharedManager] readAndWriteCredentials:handler]; } andUnauthenticatedHandler: nil]; MXWeakify(self); [[MXKAccountManager sharedManager].dehydrationService rehydrateDeviceWithMatrixRestClient:mxRestClient dehydrationKey:keyData success:^(NSString * deviceId) { MXStrongifyAndReturnIfNil(self); if (deviceId) { MXLogDebug(@"[MXKAuthenticationViewController] attemptDeviceRehydration: device %@ rehydrated successfully.", deviceId); credentials.deviceId = deviceId; } else { MXLogDebug(@"[MXKAuthenticationViewController] attemptDeviceRehydration: device rehydration has been canceled."); } [self _createAccountWithCredentials:credentials]; } failure:^(NSError *error) { MXStrongifyAndReturnIfNil(self); if (retry) { MXLogError(@"[MXKAuthenticationViewController] attemptDeviceRehydration: device rehydration failed due to error: %@. Retrying", error); [self attemptDeviceRehydrationWithKeyData:keyData credentials:credentials retry:NO]; return; } MXLogError(@"[MXKAuthenticationViewController] attemptDeviceRehydration: device rehydration failed due to error: %@", error); [self _createAccountWithCredentials:credentials]; }]; } - (void)_createAccountWithCredentials:(MXCredentials *)credentials { MXKAccount *account = [[MXKAccount alloc] initWithCredentials:credentials]; account.identityServerURL = credentials.identityServer; [[MXKAccountManager sharedManager] addAccount:account andOpenSession:YES]; if (_delegate) { [_delegate authenticationViewController:self didLogWithUserId:credentials.userId]; } } - (NSString *)deviceDisplayName { if (_deviceDisplayName) { return _deviceDisplayName; } #if TARGET_OS_IPHONE NSString *deviceName = [[UIDevice currentDevice].model isEqualToString:@"iPad"] ? [MatrixKitL10n loginTabletDevice] : [MatrixKitL10n loginMobileDevice]; #elif TARGET_OS_OSX NSString *deviceName = [MatrixKitL10n loginDesktopDevice]; #endif return deviceName; } - (void)refreshForgotPasswordSession { [_authenticationActivityIndicator stopAnimating]; MXKAuthInputsView *authInputsView = nil; if (forgotPasswordAuthInputsViewClass) { // Instantiate a new auth inputs view, except if the current one is already an instance of this class. if (self.authInputsView && self.authInputsView.class == forgotPasswordAuthInputsViewClass) { // Use the current view authInputsView = self.authInputsView; } else { authInputsView = [forgotPasswordAuthInputsViewClass authInputsView]; } } if (authInputsView) { // Update authentication inputs view to return in initial step [authInputsView setAuthSession:nil withAuthType:MXKAuthenticationTypeForgotPassword]; // Check whether the current view must be replaced if (self.authInputsView != authInputsView) { // Refresh layout self.authInputsView = authInputsView; } // Refresh user interaction self.userInteractionEnabled = _userInteractionEnabled; } else { // Remove the potential auth inputs view self.authInputsView = nil; _noFlowLabel.text = [MatrixKitL10n loginErrorForgotPasswordIsNotSupported]; MXLogDebug(@"[MXKAuthenticationVC] Warning: %@", _noFlowLabel.text); _noFlowLabel.hidden = NO; } } - (void)updateRESTClient { NSString *homeserverURL = _homeServerTextField.text; if (homeserverURL.length) { // Check change if ([homeserverURL isEqualToString:mxRestClient.homeserver] == NO) { mxRestClient = [[MXRestClient alloc] initWithHomeServer:homeserverURL andOnUnrecognizedCertificateBlock:^BOOL(NSData *certificate) { // Check first if the app developer provided its own certificate handler. if (self->onUnrecognizedCertificateCustomBlock) { return self->onUnrecognizedCertificateCustomBlock (certificate); } // Else prompt the user by displaying a fingerprint (SHA256) of the certificate. __block BOOL isTrusted; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); NSString *title = [MatrixKitL10n sslCouldNotVerify]; NSString *homeserverURLStr = [MatrixKitL10n sslHomeserverUrl:homeserverURL]; NSString *fingerprint = [MatrixKitL10n sslFingerprintHash:@"SHA256"]; NSString *certFingerprint = [certificate mx_SHA256AsHexString]; NSString *msg = [NSString stringWithFormat:@"%@\n\n%@\n\n%@\n\n%@\n\n%@\n\n%@", [MatrixKitL10n sslCertNotTrust], [MatrixKitL10n sslCertNewAccountExpl], homeserverURLStr, fingerprint, certFingerprint, [MatrixKitL10n sslOnlyAccept]]; if (self->alert) { [self->alert dismissViewControllerAnimated:NO completion:nil]; } self->alert = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert]; [self->alert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n cancel] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { self->alert = nil; isTrusted = NO; dispatch_semaphore_signal(semaphore); }]]; [self->alert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n sslTrust] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { self->alert = nil; isTrusted = YES; dispatch_semaphore_signal(semaphore); }]]; dispatch_async(dispatch_get_main_queue(), ^{ [self presentViewController:self->alert animated:YES completion:nil]; }); dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); if (!isTrusted) { // Cancel request in progress [self->mxCurrentOperation cancel]; self->mxCurrentOperation = nil; [[NSNotificationCenter defaultCenter] removeObserver:self name:AFNetworkingReachabilityDidChangeNotification object:nil]; [self->_authenticationActivityIndicator stopAnimating]; } return isTrusted; }]; if (_identityServerTextField.text.length) { [self updateIdentityServerURL:self.identityServerTextField.text]; } } } else { [mxRestClient close]; mxRestClient = nil; } } - (void)loginWithParameters:(NSDictionary*)parameters { // Add the device name NSMutableDictionary *theParameters = [NSMutableDictionary dictionaryWithDictionary:parameters]; theParameters[@"initial_device_display_name"] = self.deviceDisplayName; mxCurrentOperation = [mxRestClient login:theParameters success:^(NSDictionary *JSONResponse) { MXLoginResponse *loginResponse; MXJSONModelSetMXJSONModel(loginResponse, MXLoginResponse, JSONResponse); MXCredentials *credentials = [[MXCredentials alloc] initWithLoginResponse:loginResponse andDefaultCredentials:self->mxRestClient.credentials]; // Sanity check if (!credentials.userId || !credentials.accessToken) { [self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[MatrixKitL10n notSupportedYet]}]]; } else { MXLogDebug(@"[MXKAuthenticationVC] Login process succeeded"); // Report the certificate trusted by user (if any) credentials.allowedCertificate = self->mxRestClient.allowedCertificate; [self onSuccessfulLogin:credentials]; } } failure:^(NSError *error) { [self onFailureDuringAuthRequest:error]; }]; } - (void)registerWithParameters:(NSDictionary*)parameters { if (registrationTimer) { [registrationTimer invalidate]; registrationTimer = nil; } // Add the device name NSMutableDictionary *theParameters = [NSMutableDictionary dictionaryWithDictionary:parameters]; theParameters[@"initial_device_display_name"] = self.deviceDisplayName; mxCurrentOperation = [mxRestClient registerWithParameters:theParameters success:^(NSDictionary *JSONResponse) { MXLoginResponse *loginResponse; MXJSONModelSetMXJSONModel(loginResponse, MXLoginResponse, JSONResponse); MXCredentials *credentials = [[MXCredentials alloc] initWithLoginResponse:loginResponse andDefaultCredentials:self->mxRestClient.credentials]; // Sanity check if (!credentials.userId || !credentials.accessToken) { [self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[MatrixKitL10n notSupportedYet]}]]; } else { MXLogDebug(@"[MXKAuthenticationVC] Registration succeeded"); // Report the certificate trusted by user (if any) credentials.allowedCertificate = self->mxRestClient.allowedCertificate; [self onSuccessfulLogin:credentials]; } } failure:^(NSError *error) { self->mxCurrentOperation = nil; // Check whether the authentication is pending (for example waiting for email validation) MXError *mxError = [[MXError alloc] initWithNSError:error]; if (mxError && [mxError.errcode isEqualToString:kMXErrCodeStringUnauthorized]) { MXLogDebug(@"[MXKAuthenticationVC] Wait for email validation"); // Postpone a new attempt in 10 sec self->registrationTimer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(registrationTimerFireMethod:) userInfo:parameters repeats:NO]; } else { // The completed stages should be available in response data in case of unauthorized request. NSDictionary *JSONResponse = nil; if (error.userInfo[MXHTTPClientErrorResponseDataKey]) { JSONResponse = error.userInfo[MXHTTPClientErrorResponseDataKey]; } if (JSONResponse) { MXAuthenticationSession *authSession = [MXAuthenticationSession modelFromJSON:JSONResponse]; if (authSession.completed) { [self->_authenticationActivityIndicator stopAnimating]; // Update session identifier in case of change self.authInputsView.authSession.session = authSession.session; [self.authInputsView updateAuthSessionWithCompletedStages:authSession.completed didUpdateParameters:^(NSDictionary *parameters, NSError *error) { if (parameters) { MXLogDebug(@"[MXKAuthenticationVC] Pursue registration"); [self->_authenticationActivityIndicator startAnimating]; [self registerWithParameters:parameters]; } else { MXLogDebug(@"[MXKAuthenticationVC] Failed to update parameters"); [self onFailureDuringAuthRequest:error]; } }]; return; } [self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[MatrixKitL10n notSupportedYet]}]]; } else { [self onFailureDuringAuthRequest:error]; } } }]; } - (void)registrationTimerFireMethod:(NSTimer *)timer { if (timer == registrationTimer && timer.isValid) { MXLogDebug(@"[MXKAuthenticationVC] Retry registration"); [self registerWithParameters:registrationTimer.userInfo]; } } - (void)resetPasswordWithParameters:(NSDictionary*)parameters { mxCurrentOperation = [mxRestClient resetPasswordWithParameters:parameters success:^() { MXLogDebug(@"[MXKAuthenticationVC] Reset password succeeded"); self->mxCurrentOperation = nil; [self->_authenticationActivityIndicator stopAnimating]; self->isPasswordReseted = YES; // Force UI update to refresh submit button title. self.authType = self->_authType; // Refresh the authentication inputs view on success. [self.authInputsView nextStep]; } failure:^(NSError *error) { MXError *mxError = [[MXError alloc] initWithNSError:error]; if (mxError && [mxError.errcode isEqualToString:kMXErrCodeStringUnauthorized]) { MXLogDebug(@"[MXKAuthenticationVC] Forgot Password: wait for email validation"); self->mxCurrentOperation = nil; [self->_authenticationActivityIndicator stopAnimating]; if (self->alert) { [self->alert dismissViewControllerAnimated:NO completion:nil]; } self->alert = [UIAlertController alertControllerWithTitle:[MatrixKitL10n error] message:[MatrixKitL10n authResetPasswordErrorUnauthorized] preferredStyle:UIAlertControllerStyleAlert]; [self->alert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { self->alert = nil; }]]; [self presentViewController:self->alert animated:YES completion:nil]; } else if (mxError && [mxError.errcode isEqualToString:kMXErrCodeStringNotFound]) { MXLogDebug(@"[MXKAuthenticationVC] Forgot Password: not found"); NSMutableDictionary *userInfo; if (error.userInfo) { userInfo = [NSMutableDictionary dictionaryWithDictionary:error.userInfo]; } else { userInfo = [NSMutableDictionary dictionary]; } userInfo[NSLocalizedDescriptionKey] = [MatrixKitL10n authResetPasswordErrorNotFound]; [self onFailureDuringAuthRequest:[NSError errorWithDomain:kMXNSErrorDomain code:0 userInfo:userInfo]]; } else { [self onFailureDuringAuthRequest:error]; } }]; } - (void)onFailureDuringMXOperation:(NSError*)error { mxCurrentOperation = nil; [_authenticationActivityIndicator stopAnimating]; if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { // Ignore this error MXLogDebug(@"[MXKAuthenticationVC] flows request cancelled"); return; } MXLogDebug(@"[MXKAuthenticationVC] Failed to get %@ flows: %@", (_authType == MXKAuthenticationTypeLogin ? @"Login" : @"Register"), error); // Cancel external registration parameters if any _externalRegistrationParameters = nil; // Alert user NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey]; if (!title) { title = [MatrixKitL10n error]; } NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey]; if (alert) { [alert dismissViewControllerAnimated:NO completion:nil]; } alert = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n dismiss] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { self->alert = nil; }]]; [self presentViewController:alert animated:YES completion:nil]; // Handle specific error code here if ([error.domain isEqualToString:NSURLErrorDomain]) { // Check network reachability if (error.code == NSURLErrorNotConnectedToInternet) { // Add reachability observer in order to launch a new request when network will be available [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onReachabilityStatusChange:) name:AFNetworkingReachabilityDidChangeNotification object:nil]; } else if (error.code == kCFURLErrorTimedOut) { // Send a new request in 2 sec dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self refreshAuthenticationSession]; }); } else { // Remove the potential auth inputs view self.authInputsView = nil; } } else { // Remove the potential auth inputs view self.authInputsView = nil; } if (!_authInputsView) { // Display failure reason _noFlowLabel.hidden = NO; _noFlowLabel.text = [error.userInfo valueForKey:NSLocalizedDescriptionKey]; if (!_noFlowLabel.text.length) { _noFlowLabel.text = [MatrixKitL10n loginErrorNoLoginFlow]; } [_retryButton setTitle:[MatrixKitL10n retry] forState:UIControlStateNormal]; [_retryButton setTitle:[MatrixKitL10n retry] forState:UIControlStateNormal]; _retryButton.hidden = NO; } } - (void)onReachabilityStatusChange:(NSNotification *)notif { AFNetworkReachabilityManager *reachabilityManager = [AFNetworkReachabilityManager sharedManager]; AFNetworkReachabilityStatus status = reachabilityManager.networkReachabilityStatus; if (status == AFNetworkReachabilityStatusReachableViaWiFi || status == AFNetworkReachabilityStatusReachableViaWWAN) { dispatch_async(dispatch_get_main_queue(), ^{ [self refreshAuthenticationSession]; }); } else if (status == AFNetworkReachabilityStatusNotReachable) { _noFlowLabel.text = [MatrixKitL10n networkErrorNotReachable]; } } #pragma mark - Keyboard handling - (void)dismissKeyboard { // Hide the keyboard [_authInputsView dismissKeyboard]; [_homeServerTextField resignFirstResponder]; [_identityServerTextField resignFirstResponder]; } #pragma mark - UITextField delegate - (void)onTextFieldChange:(NSNotification *)notif { _submitButton.enabled = _authInputsView.areAllRequiredFieldsSet; if (notif.object == _homeServerTextField) { // If any, the current request is obsolete [self cancelIdentityServerCheck]; [self setIdentityServerHidden:YES]; } } - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { if (textField == _homeServerTextField) { // Cancel supported AuthFlow refresh if a request is in progress [[NSNotificationCenter defaultCenter] removeObserver:self name:AFNetworkingReachabilityDidChangeNotification object:nil]; if (mxCurrentOperation) { // Cancel potential request in progress [mxCurrentOperation cancel]; mxCurrentOperation = nil; } } return YES; } - (void)textFieldDidEndEditing:(UITextField *)textField { if (textField == _homeServerTextField) { [self setHomeServerTextFieldText:textField.text]; } else if (textField == _identityServerTextField) { [self setIdentityServerTextFieldText:textField.text]; } } - (BOOL)textFieldShouldReturn:(UITextField*)textField { if (textField.returnKeyType == UIReturnKeyDone) { // "Done" key has been pressed [textField resignFirstResponder]; } return YES; } #pragma mark - AuthInputsViewDelegate delegate - (void)authInputsView:(MXKAuthInputsView*)authInputsView presentAlertController:(UIAlertController*)inputsAlert { [self dismissKeyboard]; [self presentViewController:inputsAlert animated:YES completion:nil]; } - (void)authInputsViewDidPressDoneKey:(MXKAuthInputsView *)authInputsView { if (_submitButton.isEnabled) { // Launch authentication now [self onButtonPressed:_submitButton]; } } - (MXRestClient *)authInputsViewThirdPartyIdValidationRestClient:(MXKAuthInputsView *)authInputsView { return mxRestClient; } - (MXIdentityService *)authInputsViewThirdPartyIdValidationIdentityService:(MXIdentityService *)authInputsView { return self.identityService; } #pragma mark - Authentication Fallback - (void)showAuthenticationFallBackView { [self showAuthenticationFallBackView:authenticationFallback]; } - (void)showAuthenticationFallBackView:(NSString*)fallbackPage { _authenticationScrollView.hidden = YES; _authFallbackContentView.hidden = NO; // Add a cancel button in case of navigation controller use. if (self.navigationController) { if (!cancelFallbackBarButton) { cancelFallbackBarButton = [[UIBarButtonItem alloc] initWithTitle:[MatrixKitL10n loginLeaveFallback] style:UIBarButtonItemStylePlain target:self action:@selector(hideRegistrationFallbackView)]; } // Add cancel button in right bar items NSArray *rightBarButtonItems = self.navigationItem.rightBarButtonItems; self.navigationItem.rightBarButtonItems = rightBarButtonItems ? [rightBarButtonItems arrayByAddingObject:cancelFallbackBarButton] : @[cancelFallbackBarButton]; } 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; } [_authFallbackWebView openFallbackPage:fallbackPage success:^(MXLoginResponse *loginResponse) { MXCredentials *credentials = [[MXCredentials alloc] initWithLoginResponse:loginResponse andDefaultCredentials:self->mxRestClient.credentials]; // TODO handle unrecognized certificate (if any) during registration through fallback webview. [self onSuccessfulLogin:credentials]; }]; } - (void)hideRegistrationFallbackView { if (cancelFallbackBarButton) { NSMutableArray *rightBarButtonItems = [NSMutableArray arrayWithArray: self.navigationItem.rightBarButtonItems]; [rightBarButtonItems removeObject:cancelFallbackBarButton]; self.navigationItem.rightBarButtonItems = rightBarButtonItems; } [_authFallbackWebView stopLoading]; _authenticationScrollView.hidden = NO; _authFallbackContentView.hidden = YES; } #pragma mark - KVO - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([@"viewHeightConstraint.constant" isEqualToString:keyPath]) { // Refresh the height of the auth inputs view container. CGFloat previousInputsContainerViewHeight = _authInputContainerViewHeightConstraint.constant; _authInputContainerViewHeightConstraint.constant = _authInputsView.viewHeightConstraint.constant; // Force to render the view [self.view layoutIfNeeded]; // Refresh content view height by considering the updated height of inputs container _contentViewHeightConstraint.constant += (_authInputContainerViewHeightConstraint.constant - previousInputsContainerViewHeight); } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } @end