Classes/BITAuthenticator.m
/*
 * Author: Stephan Diederich
 *
 * Copyright (c) 2013-2014 HockeyApp, Bit Stadium GmbH.
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */


#import "HockeySDK.h"

#if HOCKEYSDK_FEATURE_AUTHENTICATOR

#import "HockeySDKPrivate.h"
#import "BITAuthenticator_Private.h"
#import "BITAuthenticationViewController.h"
#import "BITHockeyAppClient.h"
#import "BITHockeyHelper.h"
#import "BITHockeyHelper+Application.h"
#import "BITHockeyBaseManagerPrivate.h"

#include <sys/stat.h>

static NSString *const kBITAuthenticatorUUIDKey = @"BITAuthenticatorUUIDKey";
static NSString *const kBITAuthenticatorIdentifierKey = @"BITAuthenticatorIdentifierKey";
static NSString *const kBITAuthenticatorIdentifierTypeKey = @"BITAuthenticatorIdentifierTypeKey";
static NSString *const kBITAuthenticatorLastAuthenticatedVersionKey = @"BITAuthenticatorLastAuthenticatedVersionKey";
static NSString *const kBITAuthenticatorUserEmailKey = @"BITAuthenticatorUserEmailKey";

//deprecated
static NSString *const kBITAuthenticatorAuthTokenKey = @"BITAuthenticatorAuthTokenKey";
static NSString *const kBITAuthenticatorAuthTokenTypeKey = @"BITAuthenticatorAuthTokenTypeKey";

typedef unsigned int bit_uint32;

@interface BITAuthenticator()

@property (nonatomic, assign) BOOL isSetup;

@property (nonatomic, strong) id appDidBecomeActiveObserver;
@property (nonatomic, strong) id appDidEnterBackgroundObserver;
@property (nonatomic, strong) UIViewController *authenticationController;

@end


@implementation BITAuthenticator

- (instancetype)initWithAppIdentifier:(NSString *)appIdentifier appEnvironment:(BITEnvironment)environment {
  self = [super initWithAppIdentifier:appIdentifier appEnvironment:environment];
  if (self) {
    _webpageURL = [NSURL URLWithString:@"https://rink.hockeyapp.net/"];
    
    _identificationType = BITAuthenticatorIdentificationTypeAnonymous;
    _isSetup = NO;
    _restrictApplicationUsage = NO;
    _restrictionEnforcementFrequency = BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch;
  }
  return self;
}

- (void)dealloc {
  [self unregisterObservers];
}

#pragma mark - BITHockeyBaseManager overrides

- (void)startManager {
  //disabled in TestFlight and the AppStore
  if (self.appEnvironment != BITEnvironmentOther) { return; }
  
  self.isSetup = YES;
}

#pragma mark -

- (void)authenticateInstallation {
  //disabled in TestFlight and the AppStore
  if (self.appEnvironment != BITEnvironmentOther) { return; }
  
  // make sure this is called after startManager so all modules are fully setup
  if (!self.isSetup) {
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(authenticateInstallation) object:nil];
    [self performSelector:@selector(authenticateInstallation) withObject:nil afterDelay:0.1];
  } else {
    switch ([BITHockeyHelper applicationState]) {
      case BITApplicationStateActive:
        [self authenticate];
        break;
      case BITApplicationStateBackground:
      case BITApplicationStateInactive:
      case BITApplicationStateUnknown:
        // do nothing, wait for active state
        break;
    }
  }
  [self registerObservers];
}

- (void)authenticate {
  [self identifyWithCompletion:^(BOOL identified, NSError *error) {
    if (identified) {
      if ([self needsValidation]) {
        [self validate];
      } else {
        [self dismissAuthenticationControllerAnimated:YES completion:nil];
      }
    } else {
      BITHockeyLogError(@"Failed to identify. Error: %@", error);
    }
  }];
}

#pragma mark - Identification

- (void)identifyWithCompletion:(void (^)(BOOL identified, NSError *))completion {
  if (self.authenticationController) {
    BITHockeyLogDebug(@"Authentication controller already visible. Ignoring identify request");
    if (completion) { completion(NO, nil); }
    return;
  }
  //first check if the stored identification type matches the one currently configured
  NSString *storedTypeString = [self stringValueFromKeychainForKey:kBITAuthenticatorIdentifierTypeKey];
  NSString *configuredTypeString = [self.class stringForIdentificationType:self.identificationType];
  if (storedTypeString && ![storedTypeString isEqualToString:configuredTypeString]) {
    BITHockeyLogDebug(@"Identification type mismatch for stored auth-token. Resetting.");
    [self storeInstallationIdentifier:nil withType:BITAuthenticatorIdentificationTypeAnonymous];
  }
  
  NSString *identification = [self installationIdentifier];
  
  if (identification) {
    self.identified = YES;
    if (completion) { completion(YES, nil); }
    return;
  }
  
  if (self.identified) {
    if (completion) { completion(YES, nil); }
    return;
  }
  
  //it's not identified yet, do it now
  BITAuthenticationViewController *viewController = nil;
  switch (self.identificationType) {
    case BITAuthenticatorIdentificationTypeAnonymous:
      [self storeInstallationIdentifier:bit_UUID() withType:BITAuthenticatorIdentificationTypeAnonymous];
      self.identified = YES;
      if (completion) { completion(YES, nil); }
      return;
    case BITAuthenticatorIdentificationTypeHockeyAppUser:
      viewController = [[BITAuthenticationViewController alloc] initWithDelegate:self];
      viewController.requirePassword = YES;
      viewController.tableViewTitle = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerDataEmailAndPasswordDescription");
      break;
    case BITAuthenticatorIdentificationTypeDevice:
      viewController = [[BITAuthenticationViewController alloc] initWithDelegate:self];
      viewController.requirePassword = NO;
      viewController.showsLoginViaWebButton = YES;
      viewController.tableViewTitle = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerWebUDIDLoginDescription");
      break;
    case BITAuthenticatorIdentificationTypeWebAuth:
      viewController = [[BITAuthenticationViewController alloc] initWithDelegate:self];
      viewController.requirePassword = NO;
      viewController.showsLoginViaWebButton = YES;
      viewController.tableViewTitle = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerWebAuthLoginDescription");
      break;
    case BITAuthenticatorIdentificationTypeHockeyAppEmail:
      if (nil == self.authenticationSecret) {
        NSError *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                             code:BITAuthenticatorAuthorizationSecretMissing
                                         userInfo:@{NSLocalizedDescriptionKey:@"For email identification, the authentication secret must be set"}];
        if (completion) { completion(NO, error); }
        return;
      }
      viewController = [[BITAuthenticationViewController alloc] initWithDelegate:self];
      viewController.requirePassword = NO;
      viewController.tableViewTitle = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerDataEmailDescription");
      break;
  }
  id<BITAuthenticatorDelegate> strongDelegate = self.delegate;
  if ([strongDelegate respondsToSelector:@selector(authenticator:willShowAuthenticationController:)]) {
    [strongDelegate authenticator:self willShowAuthenticationController:viewController];
  }
  
  NSAssert(viewController, @"ViewController should've been created");
  
  viewController.email = [self stringValueFromKeychainForKey:kBITAuthenticatorUserEmailKey];
  self.authenticationController = viewController;
  self.identificationCompletion = completion;
  dispatch_async(dispatch_get_main_queue(), ^{
    [self showView:viewController];
  });
}

#pragma mark - Validation

- (BOOL)needsValidation {
  if (BITAuthenticatorIdentificationTypeAnonymous == self.identificationType) {
    return NO;
  }
  if (NO == self.restrictApplicationUsage) {
    return NO;
  }
  if (self.restrictionEnforcementFrequency == BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch &&
      ![self.executableUUID isEqualToString:self.lastAuthenticatedVersion]) {
    return YES;
  }
  if (NO == self.isValidated && self.restrictionEnforcementFrequency == BITAuthenticatorAppRestrictionEnforcementOnAppActive) {
    return YES;
  }
  return NO;
}

- (void)validate {
  [self validateWithCompletion:^(BOOL validated, NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
      if (validated) {
        [self dismissAuthenticationControllerAnimated:YES completion:nil];
      } else {
        BITHockeyLogError(@"Validation failed with error: %@", error);
        __weak typeof(self) weakSelf = self;

        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil
                                                                                 message:error.localizedDescription
                                                                          preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *okAction = [UIAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyOK")
                                                           style:UIAlertActionStyleDefault
                                                         handler:^(UIAlertAction __unused *action) {
                                                           typeof(self) strongSelf = weakSelf;
                                                           [strongSelf validate];
                                                         }];

        [alertController addAction:okAction];
        [self showAlertController:alertController];
      }
    });
  }];
}

- (void)validateWithCompletion:(void (^)(BOOL validated, NSError *))completion {
  BOOL requirementsFulfilled = YES;
  NSError *error = nil;
  switch (self.identificationType) {
    case BITAuthenticatorIdentificationTypeAnonymous: {
      error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                  code:BITAuthenticatorErrorUnknown
                              userInfo:@{NSLocalizedDescriptionKey:@"Anonymous users can't be validated"}];
      requirementsFulfilled = NO;
      break;
    }
    case BITAuthenticatorIdentificationTypeHockeyAppEmail:
      if (nil == self.authenticationSecret) {
        error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                    code:BITAuthenticatorAuthorizationSecretMissing
                                userInfo:@{NSLocalizedDescriptionKey:@"For email validation, the authentication secret must be set"}];
        requirementsFulfilled = NO;
        break;
      }
      //no break
    case BITAuthenticatorIdentificationTypeDevice:
    case BITAuthenticatorIdentificationTypeHockeyAppUser:
    case BITAuthenticatorIdentificationTypeWebAuth:
      if (nil == self.installationIdentifier) {
        error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                    code:BITAuthenticatorNotIdentified
                                userInfo:@{NSLocalizedDescriptionKey:@"Make sure to identify the installation first"}];
        requirementsFulfilled = NO;
      }
      break;
  }
  if (NO == requirementsFulfilled) {
    if (completion) {
      completion(NO, error);
    }
    return;
  }
  
  NSString *validationPath = [NSString stringWithFormat:@"api/3/apps/%@/identity/validate", self.encodedAppIdentifier];
  
  __weak typeof(self) weakSelf = self;
  NSURLRequest *request = [self.hockeyAppClient requestWithMethod:@"GET" path:validationPath parameters:[self validationParameters]];

  NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
  __block NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];

  NSURLSessionDataTask *task = [session dataTaskWithRequest:request
                                          completionHandler:^(NSData *data, NSURLResponse __unused *response, NSError *innerError) {
                                            typeof(self) strongSelf = weakSelf;

                                            [session finishTasksAndInvalidate];

                                            [strongSelf handleValidationResponseWithData:data error:innerError completion:completion];
                                          }];
  [task resume];
}

- (void)handleValidationResponseWithData:(NSData *)responseData error:(NSError *)error completion:(void (^)(BOOL validated, NSError *))completion {
  if (nil == responseData) {
    NSDictionary *userInfo = @{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")};
    if (error) {
      NSMutableDictionary *dict = [userInfo mutableCopy];
      dict[NSUnderlyingErrorKey] = error;
      userInfo = dict;
    }
    NSError *localError = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                         code:BITAuthenticatorNetworkError
                                     userInfo:userInfo];
    self.validated = NO;
    if (completion) { completion(NO, localError); }
  } else {
    NSError *validationParseError = nil;
    BOOL valid = [self.class isValidationResponseValid:responseData error:&validationParseError];
    self.validated = valid;
    if (valid) {
      [self setLastAuthenticatedVersion:self.executableUUID];
    }
    if (completion) { completion(valid, validationParseError); }
  }
}

- (NSDictionary *)validationParameters {
  NSParameterAssert(self.installationIdentifier);
  NSParameterAssert(self.installationIdentifierParameterString);
  
  NSString *installString = bit_appAnonID(NO);
  if (installString) {
    return @{self.installationIdentifierParameterString:self.installationIdentifier, @"install_string":installString};
  }
  
  return @{self.installationIdentifierParameterString:self.installationIdentifier};
}

+ (BOOL)isValidationResponseValid:(id)response error:(NSError *__autoreleasing *)error {
  NSParameterAssert(response);
  
  NSError *jsonParseError = nil;
  id jsonObject = [NSJSONSerialization JSONObjectWithData:response
                                                  options:0
                                                    error:&jsonParseError];
  if (nil == jsonObject) {
    if (error) {
      *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                   code:BITAuthenticatorAPIServerReturnedInvalidResponse
                               userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}];
    }
    return NO;
  }
  if (![(NSObject *)jsonObject isKindOfClass:[NSDictionary class]]) {
    if (error) {
      *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                   code:BITAuthenticatorAPIServerReturnedInvalidResponse
                               userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}];
    }
    return NO;
  }
  
  NSString *status = jsonObject[@"status"];
  if ([status isEqualToString:@"not authorized"]) {
    if (error) {
      *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                   code:BITAuthenticatorNotAuthorized
                               userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationNotMember")}];
    }
    return NO;
  } else if ([status isEqualToString:@"not found"]) {
    if (error) {
      *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                   code:BITAuthenticatorUnknownApplicationID
                               userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationContactDeveloper")}];
    }
    return NO;
  } else if ([status isEqualToString:@"validated"]) {
    return YES;
  } else {
    if (error) {
      *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                   code:BITAuthenticatorAPIServerReturnedInvalidResponse
                               userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}];
    }
    return NO;
  }
}

#pragma mark - AuthenticationViewController Helper

/**
 * This method has to be called on the main queue
 */
- (void)dismissAuthenticationControllerAnimated:(BOOL)animated completion:(void (^)(void))completion {
  if (!self.authenticationController) { return; }
  UIViewController *presentingViewController = [self.authenticationController presentingViewController];
  
  // If there is no presenting view controller just remove view
  if (presentingViewController) {
    [self.authenticationController dismissViewControllerAnimated:animated completion:completion];
  } else {
    [self.authenticationController.navigationController.view removeFromSuperview];
    if (completion) {
      completion();
    }
  }
  self.authenticationController = nil;
}

#pragma mark - AuthenticationViewControllerDelegate

- (void)authenticationViewController:(UIViewController *)viewController
       handleAuthenticationWithEmail:(NSString *)email
                            password:(NSString *)password
                          completion:(void (^)(BOOL, NSError *))completion {
  
  NSParameterAssert(email && email.length);
  NSParameterAssert(self.identificationType == BITAuthenticatorIdentificationTypeHockeyAppEmail || (password && password.length));
  
  // Trim whitespace from email in case the user has added a whitespace at the end of the email address. This shouldn't
  // happen if devs use our UI but we've had 1-2 support tickets where the email contained whitespace at the end and
  // verification failed because of that.
  email = [email stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  
  NSURLRequest *request = [self requestForAuthenticationEmail:email password:password];
  
  [self authenticationViewController:viewController handleAuthenticationWithEmail:email request:request completion:completion];
}

- (void)authenticationViewController:(UIViewController *) __unused viewController
       handleAuthenticationWithEmail:(NSString *)email
                             request:(NSURLRequest *)request
                          completion:(void (^)(BOOL, NSError *))completion {
  
  __weak typeof(self) weakSelf = self;
  NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
  __block NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];

  NSURLSessionDataTask *task = [session dataTaskWithRequest:request
                                          completionHandler:^(NSData *data, NSURLResponse *response, NSError __unused *error) {
                                            typeof(self) strongSelf = weakSelf;
                                            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

                                            [session finishTasksAndInvalidate];

                                            [strongSelf handleAuthenticationWithResponse:httpResponse email:email data:data completion:completion];
                                          }];
  [task resume];
}

- (void)authenticationViewControllerDidTapWebButton:(UIViewController *) __unused viewController {
  NSURL *url = [self deviceAuthenticationURL];
  if (url) {
    [[UIApplication sharedApplication] openURL:url];
  }
}

#pragma mark - Networking

- (void)handleAuthenticationWithResponse:(NSHTTPURLResponse *)response email:(NSString *)email data:(NSData *)data completion:(void (^)(BOOL, NSError *))completion {
  NSError *authParseError = nil;
  NSString *authToken = [self.class authenticationTokenFromURLResponse:response
                                                                  data:data
                                                                 error:&authParseError];
  BOOL identified;
  if (authToken) {
    identified = YES;
    [self storeInstallationIdentifier:authToken withType:self.identificationType];
    dispatch_async(dispatch_get_main_queue(), ^{
      [self dismissAuthenticationControllerAnimated:YES completion:nil];
    });
    BOOL success = [self addStringValueToKeychain:email forKey:kBITAuthenticatorUserEmailKey];
    if (!success) {
      [self alertOnFailureStoringTokenInKeychain];
    }
  } else {
    identified = NO;
  }
  self.identified = identified;
  if (completion) { completion(identified, authParseError); }
  if (self.identificationCompletion) {
    self.identificationCompletion(identified, authParseError);
    self.identificationCompletion = nil;
  }
}

- (NSURLRequest *)requestForAuthenticationEmail:(NSString *)email password:(NSString *)password {
  NSString *authenticationPath = [self authenticationPath];
  NSMutableDictionary *params = [NSMutableDictionary dictionary];
  
  NSString *installString = bit_appAnonID(NO);
  if (installString) {
    params[@"install_string"] = installString;
  }
  
  if (BITAuthenticatorIdentificationTypeHockeyAppEmail == self.identificationType) {
    NSString *authCode = BITHockeyMD5([NSString stringWithFormat:@"%@%@",
                                       self.authenticationSecret ?: @"",
                                       email ?: @""]);
    
    params[@"email"] = email ?: @"";
    params[@"authcode"] = authCode.lowercaseString;
  }
  
  NSMutableURLRequest *request = [self.hockeyAppClient requestWithMethod:@"POST"
                                                                    path:authenticationPath
                                                              parameters:params];
  if (BITAuthenticatorIdentificationTypeHockeyAppUser == self.identificationType) {
    NSString *authStr = [NSString stringWithFormat:@"%@:%@", email, password];
    NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding];
    NSString *authValue = [NSString stringWithFormat:@"Basic %@", [authData base64EncodedStringWithOptions:0]];
    [request setValue:authValue forHTTPHeaderField:@"Authorization"];
  }
  
  return request;
}

- (NSString *)authenticationPath {
  if (BITAuthenticatorIdentificationTypeHockeyAppUser == self.identificationType) {
    return [NSString stringWithFormat:@"api/3/apps/%@/identity/authorize", self.encodedAppIdentifier];
  } else {
    return [NSString stringWithFormat:@"api/3/apps/%@/identity/check", self.encodedAppIdentifier];
  }
}

+ (NSString *)authenticationTokenFromURLResponse:(NSHTTPURLResponse *)urlResponse data:(NSData *)data error:(NSError *__autoreleasing *)error {
  if (nil == urlResponse) {
    if (error) {
      *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                   code:BITAuthenticatorAPIServerReturnedInvalidResponse
                               userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}];
    }
    return nil;
  }
  
  switch (urlResponse.statusCode) {
    case 401:
      if (error) {
        *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                     code:BITAuthenticatorNotAuthorized
                                 userInfo:@{
                                            NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationWrongEmailPassword")
                                            }];
      }
      break;
    case 200:
    case 404:
      //Do nothing, handled below
      break;
    default:
      if (error) {
        *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                     code:BITAuthenticatorAPIServerReturnedInvalidResponse
                                 userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}];
        
      }
      break;
  }
  if (200 != urlResponse.statusCode && 404 != urlResponse.statusCode) {
    //make sure we have an error created if user wanted to have one
    NSParameterAssert(nil == error || *error);
    return nil;
  }
  
  NSError *jsonParseError = nil;
  id jsonObject = [NSJSONSerialization JSONObjectWithData:data
                                                  options:0
                                                    error:&jsonParseError];
  //no json or unexpected json
  if (nil == jsonObject || ![(NSObject *)jsonObject isKindOfClass:[NSDictionary class]]) {
    if (error) {
      NSDictionary *userInfo = @{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")};
      if (jsonParseError) {
        NSMutableDictionary *userInfoMutable = [userInfo mutableCopy];
        userInfoMutable[NSUnderlyingErrorKey] = jsonParseError;
        userInfo = userInfoMutable;
      }
      *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                   code:BITAuthenticatorAPIServerReturnedInvalidResponse
                               userInfo:userInfo];
    }
    return nil;
  }
  
  NSString *status = jsonObject[@"status"];
  NSString *authToken = nil;
  if ([status isEqualToString:@"identified"]) {
    authToken = jsonObject[@"iuid"];
  } else if ([status isEqualToString:@"authorized"]) {
    authToken = jsonObject[@"auid"];
  } else if ([status isEqualToString:@"not authorized"]) {
    if (error) {
      *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                   code:BITAuthenticatorNotAuthorized
                               userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationNotMember")}];
      
    }
  }
  //if no error is set yet, but error parameter is given, return a generic error
  if (nil == authToken && error && nil == *error) {
    *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                 code:BITAuthenticatorAPIServerReturnedInvalidResponse
                             userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}];
  }
  return authToken;
}

- (NSURL *)deviceAuthenticationURL {
  NSString *whatParameter = nil;
  switch (self.identificationType) {
    case BITAuthenticatorIdentificationTypeWebAuth:
      whatParameter = @"email";
      break;
    case BITAuthenticatorIdentificationTypeDevice:
      whatParameter = @"udid";
      break;
    case BITAuthenticatorIdentificationTypeAnonymous:
    case BITAuthenticatorIdentificationTypeHockeyAppEmail:
    case BITAuthenticatorIdentificationTypeHockeyAppUser:
      return nil;
  }
  NSURL *url = [self.webpageURL URLByAppendingPathComponent:[NSString stringWithFormat:@"apps/%@/authorize", self.encodedAppIdentifier]];
  NSParameterAssert(whatParameter && url.absoluteString);
  url = [NSURL URLWithString:[NSString stringWithFormat:@"%@?what=%@", url.absoluteString, whatParameter]];
  return url;
}

- (BOOL)handleOpenURL:(NSURL *)url
    sourceApplication:(NSString *) __unused sourceApplication
           annotation:(id)annotation {
  //check if this URL was meant for us, if not return NO so the user can
  //handle it
  NSString *const kAuthorizationHost = @"authorize";
  NSString *urlScheme = self.urlScheme ?: [NSString stringWithFormat:@"ha%@", self.appIdentifier];
  if (!([[url scheme] isEqualToString:urlScheme] && [[url host] isEqualToString:kAuthorizationHost])) {
    BITHockeyLogWarning(@"WARNING: URL scheme for authentication doesn't match!");
    return NO;
  }
  
  NSString *installationIdentifier = nil;
  NSString *localizedErrorDescription = nil;
  switch (self.identificationType) {
    case BITAuthenticatorIdentificationTypeWebAuth: {
      NSString *email = nil;
      [self.class email:&email andIUID:&installationIdentifier fromOpenURL:url];
      if (email) {
        BOOL success = [self addStringValueToKeychain:email forKey:kBITAuthenticatorUserEmailKey];
        if (!success) {
          [self alertOnFailureStoringTokenInKeychain];
        }
      } else {
        BITHockeyLogDebug(@"No email found in URL: %@", url);
      }
      localizedErrorDescription = @"Failed to retrieve parameters from URL.";
      break;
    }
    case BITAuthenticatorIdentificationTypeDevice: {
      installationIdentifier = [self.class UDIDFromOpenURL:url annotation:annotation];
      localizedErrorDescription = @"Failed to retrieve UDID from URL.";
      break;
    }
    case BITAuthenticatorIdentificationTypeHockeyAppEmail:
    case BITAuthenticatorIdentificationTypeAnonymous:
    case BITAuthenticatorIdentificationTypeHockeyAppUser:
      return NO;
  }
  
  if (installationIdentifier) {
    BITHockeyLogDebug(@"Authentication succeeded.");
    if (NO == self.restrictApplicationUsage) {
      [self dismissAuthenticationControllerAnimated:YES completion:nil];
    }
    [self storeInstallationIdentifier:installationIdentifier withType:self.identificationType];
    self.identified = YES;
    if (self.identificationCompletion) {
      self.identificationCompletion(YES, nil);
      self.identificationCompletion = nil;
    }
  } else {
    //reset token
    BITHockeyLogDebug(@"Resetting authentication token");
    [self storeInstallationIdentifier:nil withType:self.identificationType];
    self.identified = NO;
    if (self.identificationCompletion) {
      NSError *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
                                           code:BITAuthenticatorErrorUnknown
                                       userInfo:@{NSLocalizedDescriptionKey:localizedErrorDescription}];
      self.identificationCompletion(NO, error);
      self.identificationCompletion = nil;
    }
  }
  return YES;
}

+ (NSString *)UDIDFromOpenURL:(NSURL *)url annotation:(id) __unused annotation {
  NSString *query = [url query];
  NSString *udid = nil;
  //there should actually only one
  static NSString *const UDIDQuerySpecifier = @"udid";
  for (NSString *queryComponents in [query componentsSeparatedByString:@"&"]) {
    NSArray<NSString *> *parameterComponents = [queryComponents componentsSeparatedByString:@"="];
    if (2 == parameterComponents.count && [parameterComponents[0] isEqualToString:UDIDQuerySpecifier]) {
      udid = parameterComponents[1];
      break;
    }
  }
  return udid;
}

+ (void)email:(NSString *__autoreleasing *)email andIUID:(NSString *__autoreleasing *)iuid fromOpenURL:(NSURL *)url {
  NSString *query = [url query];
  //there should actually only one
  static NSString *const EmailQuerySpecifier = @"email";
  static NSString *const IUIDQuerySpecifier = @"iuid";
  for (NSString *queryComponents in [query componentsSeparatedByString:@"&"]) {
    NSArray<NSString *> *parameterComponents = [queryComponents componentsSeparatedByString:@"="];
    if (email && 2 == parameterComponents.count && [parameterComponents[0] isEqualToString:EmailQuerySpecifier]) {
      *email = parameterComponents[1];
    } else if (iuid && 2 == parameterComponents.count && [parameterComponents[0] isEqualToString:IUIDQuerySpecifier]) {
      *iuid = parameterComponents[1];
    }
  }
}

#pragma mark - Private helpers

- (void)alertOnFailureStoringTokenInKeychain {
  if ([BITHockeyHelper applicationState] != BITApplicationStateActive) {
    return;
  }
  
  BITHockeyLogError(@"[HockeySDK] ERROR: The authentication token could not be stored due to a keychain error. This is most likely a signing or keychain entitlement issue!");
}

- (void)cleanupInternalStorage {
  [self removeKeyFromKeychain:kBITAuthenticatorIdentifierTypeKey];
  [self removeKeyFromKeychain:kBITAuthenticatorIdentifierKey];
  [self removeKeyFromKeychain:kBITAuthenticatorUUIDKey];
  [self removeKeyFromKeychain:kBITAuthenticatorUserEmailKey];
  [self setLastAuthenticatedVersion:nil];
  self.identified = NO;
  self.validated = NO;
  
  //cleanup values stored from 3.5 Beta1..Beta3
  [self removeKeyFromKeychain:kBITAuthenticatorAuthTokenKey];
  [self removeKeyFromKeychain:kBITAuthenticatorAuthTokenTypeKey];
}

#pragma mark - NSNotification

- (void)registerObservers {
  __weak typeof(self) weakSelf = self;
  if (nil == self.appDidBecomeActiveObserver) {
    self.appDidBecomeActiveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification
                                                                                        object:nil
                                                                                         queue:NSOperationQueue.mainQueue
                                                                                    usingBlock:^(NSNotification *note) {
                                                                                      typeof(self) strongSelf = weakSelf;
                                                                                      [strongSelf applicationDidBecomeActive:note];
                                                                                    }];
  }
  if (nil == self.appDidEnterBackgroundObserver) {
    self.appDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification
                                                                                           object:nil
                                                                                            queue:NSOperationQueue.mainQueue
                                                                                       usingBlock:^(NSNotification *note) {
                                                                                         typeof(self) strongSelf = weakSelf;
                                                                                         [strongSelf applicationDidEnterBackground:note];
                                                                                       }];
  }
}

- (void)unregisterObservers {
  if (self.appDidBecomeActiveObserver) {
    [[NSNotificationCenter defaultCenter] removeObserver:self.appDidBecomeActiveObserver];
    self.appDidBecomeActiveObserver = nil;
  }
  if (self.appDidEnterBackgroundObserver) {
    [[NSNotificationCenter defaultCenter] removeObserver:self.appDidEnterBackgroundObserver];
    self.appDidEnterBackgroundObserver = nil;
  }
}

#pragma mark - Property overrides

- (void)storeInstallationIdentifier:(NSString *)installationIdentifier withType:(BITAuthenticatorIdentificationType)type {
  if (nil == installationIdentifier) {
    [self removeKeyFromKeychain:kBITAuthenticatorIdentifierKey];
    [self removeKeyFromKeychain:kBITAuthenticatorIdentifierTypeKey];
  } else {
    BOOL success1 = [self addStringValueToKeychainForThisDeviceOnly:installationIdentifier
                                                             forKey:kBITAuthenticatorIdentifierKey];
    BOOL success2 = [self addStringValueToKeychainForThisDeviceOnly:[self.class stringForIdentificationType:type]
                                                             forKey:kBITAuthenticatorIdentifierTypeKey];
    if (!success1 || !success2) {
      [self alertOnFailureStoringTokenInKeychain];
    }
  }
}

- (NSString *)installationIdentifier {
  NSString *identifier = [self stringValueFromKeychainForKey:kBITAuthenticatorIdentifierKey];
  return identifier;
}

- (void)setLastAuthenticatedVersion:(NSString *)lastAuthenticatedVersion {
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  if (nil == lastAuthenticatedVersion) {
    [defaults removeObjectForKey:kBITAuthenticatorLastAuthenticatedVersionKey];
  } else {
    [defaults setObject:lastAuthenticatedVersion
                 forKey:kBITAuthenticatorLastAuthenticatedVersionKey];
  }
}

- (NSString *)lastAuthenticatedVersion {
  return [[NSUserDefaults standardUserDefaults] objectForKey:kBITAuthenticatorLastAuthenticatedVersionKey];
}

- (NSString *)installationIdentifierParameterString {
  switch (self.identificationType) {
    case BITAuthenticatorIdentificationTypeHockeyAppEmail:
    case BITAuthenticatorIdentificationTypeWebAuth:
      return @"iuid";
    case BITAuthenticatorIdentificationTypeHockeyAppUser:
      return @"auid";
    case BITAuthenticatorIdentificationTypeDevice:
      return @"udid";
    case BITAuthenticatorIdentificationTypeAnonymous:
      return @"uuid";
  }
}

+ (NSString *)stringForIdentificationType:(BITAuthenticatorIdentificationType)identificationType {
  switch (identificationType) {
    case BITAuthenticatorIdentificationTypeHockeyAppEmail:
      return @"iuid";
    case BITAuthenticatorIdentificationTypeWebAuth:
      return @"webAuth";
    case BITAuthenticatorIdentificationTypeHockeyAppUser:
      return @"auid";
    case BITAuthenticatorIdentificationTypeDevice:
      return @"udid";
    case BITAuthenticatorIdentificationTypeAnonymous:
      return @"uuid";
  }
}

- (void)setIdentificationType:(BITAuthenticatorIdentificationType)identificationType {
  if (_identificationType != identificationType) {
    _identificationType = identificationType;
    self.identified = NO;
    self.validated = NO;
  }
}

- (NSString *)publicInstallationIdentifier {
  switch (self.identificationType) {
    case BITAuthenticatorIdentificationTypeHockeyAppEmail:
    case BITAuthenticatorIdentificationTypeHockeyAppUser:
    case BITAuthenticatorIdentificationTypeWebAuth:
      return [self stringValueFromKeychainForKey:kBITAuthenticatorUserEmailKey];
    case BITAuthenticatorIdentificationTypeAnonymous:
    case BITAuthenticatorIdentificationTypeDevice:
      return [self stringValueFromKeychainForKey:kBITAuthenticatorIdentifierKey];
  }
}

#pragma mark - Application Lifecycle

- (void)applicationDidBecomeActive:(NSNotification *) __unused note {
  [self authenticate];
}

- (void)applicationDidEnterBackground:(NSNotification *) __unused note {
  if (BITAuthenticatorAppRestrictionEnforcementOnAppActive == self.restrictionEnforcementFrequency) {
    self.validated = NO;
  }
}

@end

#endif  /* HOCKEYSDK_FEATURE_AUTHENTICATOR */
