redo BITAuthenticator interface

given the good feedback we got, this is take 2
of the BITAuthenticator interface.
It's simplified, cleaned up and now allows full
control over the authentication process.
Major changes:
* manual mode
  Authenticator provides the bits to show the
  viewController to identify the user as well as to
  trigger validation on behalf of the developer.
* process separation
  identification and app-usage-restriction are now
  2 completely separated things.
* public identifier
  Authenticator now allows the developer to query
  parts of the information, e.g. the UDID or the user's
  emailaddy once identified
This commit is contained in:
Stephan Diederich 2013-09-23 23:15:56 +02:00
parent 5ac00ff174
commit c2c6558687
10 changed files with 618 additions and 833 deletions

View File

@ -54,14 +54,12 @@
*/ */
@property (nonatomic, assign) BOOL requirePassword; @property (nonatomic, assign) BOOL requirePassword;
/** configure if user can skip authentication or not
*
* defaults to YES
*/
@property (nonatomic, assign) BOOL showsSkipButton;
@property (nonatomic, weak) id<BITAuthenticationViewControllerDelegate> delegate; @property (nonatomic, weak) id<BITAuthenticationViewControllerDelegate> delegate;
/**
* allows to pre-fill the email-addy
*/
@property (nonatomic, copy) NSString* email;
@end @end
/** /**
@ -69,13 +67,6 @@
*/ */
@protocol BITAuthenticationViewControllerDelegate<NSObject> @protocol BITAuthenticationViewControllerDelegate<NSObject>
/**
* called then the user skipped the auth-dialgo
*
* @param viewController the delegating viewcontroller
*/
- (void) authenticationViewControllerDidSkip:(UIViewController*) viewController;
- (void) authenticationViewControllerDidTapWebButton:(UIViewController*) viewController; - (void) authenticationViewControllerDidTapWebButton:(UIViewController*) viewController;
/** /**

View File

@ -34,9 +34,9 @@
@interface BITAuthenticationViewController ()<UITextFieldDelegate> { @interface BITAuthenticationViewController ()<UITextFieldDelegate> {
UIStatusBarStyle _statusBarStyle; UIStatusBarStyle _statusBarStyle;
__weak UITextField *_emailField;
} }
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password; @property (nonatomic, copy) NSString *password;
@end @end
@ -48,7 +48,6 @@
if (self) { if (self) {
self.title = BITHockeyLocalizedString(@"HockeyAuthenticatorViewControllerTitle"); self.title = BITHockeyLocalizedString(@"HockeyAuthenticatorViewControllerTitle");
_delegate = delegate; _delegate = delegate;
_showsSkipButton = YES;
} }
return self; return self;
} }
@ -85,22 +84,8 @@
} }
#pragma mark - Property overrides #pragma mark - Property overrides
- (void)setShowsSkipButton:(BOOL)showsSkipButton {
if(_showsSkipButton != showsSkipButton) {
_showsSkipButton = showsSkipButton;
[self updateBarButtons];
}
}
- (void) updateBarButtons { - (void) updateBarButtons {
if(self.showsSkipButton) {
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:BITHockeyLocalizedString(@"Skip")
style:UIBarButtonItemStyleBordered
target:self
action:@selector(dismissAction:)];
} else {
self.navigationItem.leftBarButtonItem = nil;
}
if(self.showsLoginViaWebButton) { if(self.showsLoginViaWebButton) {
self.navigationItem.rightBarButtonItem = nil; self.navigationItem.rightBarButtonItem = nil;
} else { } else {
@ -149,6 +134,13 @@
- (IBAction) handleWebLoginButton:(id)sender { - (IBAction) handleWebLoginButton:(id)sender {
[self.delegate authenticationViewControllerDidTapWebButton:self]; [self.delegate authenticationViewControllerDidTapWebButton:self];
} }
- (void)setEmail:(NSString *)email {
_email = email;
if(self.isViewLoaded) {
_emailField.text = email;
}
}
#pragma mark - UIViewController Rotation #pragma mark - UIViewController Rotation
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orientation { - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orientation {
@ -222,6 +214,7 @@
if (0 == [indexPath row]) { if (0 == [indexPath row]) {
textField.placeholder = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerEmailPlaceholder"); textField.placeholder = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerEmailPlaceholder");
textField.text = self.email; textField.text = self.email;
_emailField = textField;
textField.keyboardType = UIKeyboardTypeEmailAddress; textField.keyboardType = UIKeyboardTypeEmailAddress;
if ([self requirePassword]) if ([self requirePassword])
@ -296,10 +289,6 @@
} }
#pragma mark - Actions #pragma mark - Actions
- (void)dismissAction:(id)sender {
[self.delegate authenticationViewControllerDidSkip:self];
}
- (void)saveAction:(id)sender { - (void)saveAction:(id)sender {
[self setLoginUIEnabled:NO]; [self setLoginUIEnabled:NO];

View File

@ -31,122 +31,83 @@
#import "BITHockeyBaseManager.h" #import "BITHockeyBaseManager.h"
/** /**
* Auth types * Identification Types
*/ */
typedef NS_ENUM(NSUInteger, BITAuthenticatorAuthType) { typedef NS_ENUM(NSUInteger, BITAuthenticatorIdentificationType) {
/**
* Assigns this app an anonymous user id
*/
BITAuthenticatorIdentificationTypeAnonymous,
/** /**
* Ask for the HockeyApp account email * Ask for the HockeyApp account email
*/ */
BITAuthenticatorAuthTypeEmail, BITAuthenticatorIdentificationTypeHockeyAppEmail,
/** /**
* Ask for the HockeyApp account email and password * Ask for the HockeyApp account by email and password
*/ */
BITAuthenticatorAuthTypeEmailAndPassword, BITAuthenticatorIdentificationTypeHockeyAppUser,
/** /**
* Request the device UDID * Identifies the current device
*/ */
BITAuthenticatorAuthTypeUDIDProvider BITAuthenticatorIdentificationTypeDevice,
}; };
/** /**
* Validation types * BITAuthenticatorAppRestrictionEnforcementFrequency
* Specifies how often the Authenticator checks if the user is allowed to use
* use this app.
*/ */
typedef NS_ENUM(NSUInteger, BITAuthenticatorValidationType) { typedef NS_ENUM(NSUInteger, BITAuthenticatorAppRestrictionEnforcementFrequency) {
/** /**
* Never validate if the user is allowed to run the app * Check if the user is allowed to use the app the first time a version is started
*/ */
BITAuthenticatorValidationTypeNever = 0, BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch,
/** /**
* Optionally validate if the user is authorized; user can skip the process * Check if the user is allowed to use the app everytime the app becomes active
*/ */
BITAuthenticatorValidationTypeOptional, BITAuthenticatorAppRestrictionEnforcementOnAppActive,
/**
* Check if the user is authenticated at the first time a new version is started
*/
BITAuthenticatorValidationTypeOnFirstLaunch,
/**
* Check if the user is authenticated everytime the app becomes active
*/
BITAuthenticatorValidationTypeOnAppActive,
}; };
typedef void(^tAuthenticationCompletion)(NSString* authenticationToken, NSError *error);
typedef void(^tValidationCompletion)(BOOL validated, NSError *error);
@protocol BITAuthenticatorDelegate; @protocol BITAuthenticatorDelegate;
/**
* Authenticator module used to identify and optionally authenticate the current app user
*
* This is the HockeySDK module for handling authentication when using Ad-Hoc or Enterprise provisioning profiles.
* This module allows you to make sure the current app installation is done on an authorized device by choosing from
* various authentication and validation mechanisms which provide different levels of authentication.
*
* This does not provide DRM or copy protection in any form. Each authentication type and validation type provide
* a different level of user authorization. Validation is the process of checking against the HockeyApp server if
* the provided/existing authorization is still valid.
*
* This module automatically disables itself when running in an App Store build by default!
*
* Authentication is a 2 step process:
*
* 1. authenticate:
* a token is acquired depending on the `authenticationType`
* 2. validation:
* the acquired token from step 1 is validated depending the `validationType`
*
* There are currently 3 ways of authentication (`BITAuthenticatorAuthType`):
*
* 1. authenticate the user via email only (`BITAuthenticatorAuthTypeEmail`)
* 2. authenticate the user via email & password (`BITAuthenticatorAuthTypeEmailAndPassword`)
* 3. authenticate the device via its UDID (_Default_) (`BITAuthenticatorAuthTypeUDIDProvider`)
*
* There are currently 4 ways of validation (`BITAuthenticatorValidationType`):
*
* 1. never (_Default_) (`BITAuthenticatorValidationTypeNever`)
* 2. optional (`BITAuthenticatorValidationTypeOptional`)
* 3. on first launch of a new app version (`BITAuthenticatorValidationTypeOnFirstLaunch`)
* 4. every time the app becomes active (needs internet connection) (`BITAuthenticatorValidationTypeOnAppActive`)
*
* We have created a detailed guide on how to use this class: [Authenticating Users on iOS](HowTo-Authenticating-Users-on-iOS)
*
*/
@interface BITAuthenticator : BITHockeyBaseManager @interface BITAuthenticator : BITHockeyBaseManager
#pragma mark - Configuration #pragma mark - Configuration
/** /**
* Defines the authentication mechanism to be used * Defines the identification mechanism to be used
* *
* The values are listed here: `BITAuthenticatorAuthType`: * _Default_: `BITAuthenticatorIdentificationTypeAnonymous`
* *
* 1. `BITAuthenticatorAuthTypeEmail`: authenticate the user via email only * @see BITAuthenticatorIdentificationType
* 2. `BITAuthenticatorAuthTypeEmailAndPassword`: authenticate the user via email & password
* 3. `BITAuthenticatorAuthTypeUDIDProvider`: authenticate the device via its UDID (_Default_)
*
* _Default_: `BITAuthenticatorAuthTypeUDIDProvider`
*
* @see BITAuthenticatorAuthType
*/ */
@property (nonatomic, assign) BITAuthenticatorAuthType authenticationType; @property (nonatomic, assign) BITAuthenticatorIdentificationType identificationType;
/** /**
* Defines the validation mechanism to be used * Defines if the BITAuthenticator automatically identifies the user and also
* checks if he's still allowed to use the app (depending on `restrictApplicationUsage`)
* *
* The values are listed here: `BITAuthenticatorValidationType`: * _Default_: `YES`
* *
* 1. `BITAuthenticatorValidationTypeNever`: never (_Default_)
* 2. `BITAuthenticatorValidationTypeOptional`: optional
* 3. `BITAuthenticatorValidationTypeOnFirstLaunch`: on first launch of a new app version
* 4. `BITAuthenticatorValidationTypeOnAppActive`: every time the app becomes active (needs internet connection)
*
* _Default_: `BITAuthenticatorValidationTypeNever`
*
* @see BITAuthenticatorValidationType
*/ */
@property (nonatomic, assign) BITAuthenticatorValidationType validationType; @property (nonatomic, assign) BOOL automaticMode;
@property (nonatomic, weak) id<BITAuthenticatorDelegate> delegate; /**
* Enables or disables checking if the user is allowed to run this app
*
* _Default_: `YES`
*/
@property (nonatomic, assign) BOOL restrictApplicationUsage;
/**
* Defines how often the BITAuthenticator checks if the user is allowed
* to run this application
*
* _Default_: `BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch`
*
* @see BITAuthenticatorAppRestrictionEnforcementFrequency
*/
@property (nonatomic, assign) BITAuthenticatorAppRestrictionEnforcementFrequency restrictionEnforcementFrequency;
/** /**
* The authentication secret from HockeyApp. To find the right secret, click on your app on the HockeyApp dashboard, * The authentication secret from HockeyApp. To find the right secret, click on your app on the HockeyApp dashboard,
@ -156,8 +117,13 @@ typedef void(^tValidationCompletion)(BOOL validated, NSError *error);
*/ */
@property (nonatomic, copy) NSString *authenticationSecret; @property (nonatomic, copy) NSString *authenticationSecret;
#pragma mark - UDID auth /**
* Delegate that can be used to do any last minute configurations on the presented viewController.
*/
@property (nonatomic, weak) id<BITAuthenticatorDelegate> delegate;
#pragma mark - UDID auth
/** /**
* baseURL of the webpage the user is redirected to if authenticationType is BITAuthenticatorAuthTypeUDIDProvider * baseURL of the webpage the user is redirected to if authenticationType is BITAuthenticatorAuthTypeUDIDProvider
* defaults to https://rink.hockeyapp.net * defaults to https://rink.hockeyapp.net
@ -193,6 +159,46 @@ typedef void(^tValidationCompletion)(BOOL validated, NSError *error);
sourceApplication:(NSString *) sourceApplication sourceApplication:(NSString *) sourceApplication
annotation:(id) annotation; annotation:(id) annotation;
#pragma mark - Authentication
/**
* Identifies the user according to the type specified in `identificationType`
* If the BITAuthenticator is in manual mode, it's your responsibility to call
* this method. Depending on the `identificationType`, this method
* might present a viewController to let the user enter his/her credentials.
* If the Authenticator is in auto-mode, this is called by the authenticator itself
* once needed.
*/
- (void) identifyWithCompletion:(void(^)(BOOL identified, NSError *error)) completion;
/**
* returns YES if this app is identified according to the setting in `identificationType`
*/
@property (nonatomic, assign, readonly, getter = isIdentified) BOOL identified;
/**
* Validates if the identified user is allowed to run this application. This checks
* with the HockeyApp backend and calls the completion-block once completed.
* If the BITAuthenticator is in manual mode, it's your responsibility to call
* this method. If the application is not yet identified, validation is not possible
* and the completion-block is called with an error set.
* If the Authenticator is in auto-mode, this is called by the authenticator itself
* once needed.
*/
- (void) validateWithCompletion:(void(^)(BOOL validated, NSError *error)) completion;
@property (nonatomic, assign, readonly, getter = isValidated) BOOL validated;
/**
* removes all previously stored authentication tokens, UDIDs, etc
*/
- (void) cleanupInternalStorage;
/**
* can be used by the application to identify the user.
* returns different values depending on `identificationType`.
*/
- (NSString*) publicInstallationIdentifier;
@end @end
#pragma mark - Protocol #pragma mark - Protocol
@ -204,13 +210,12 @@ typedef void(^tValidationCompletion)(BOOL validated, NSError *error);
@optional @optional
/** /**
* If the authentication (or validation) needs to authenticate the user, * If the authentication (or validation) needs to identify the user,
* this delegate method is called with the viewController that we'll present. * this delegate method is called with the viewController that we'll present.
* *
* @param authenticator authenticator object * @param authenticator authenticator object
* @param viewController viewcontroller used to authenticate the user * @param viewController viewcontroller used to identify the user
* *
*/ */
- (void) authenticator:(BITAuthenticator *)authenticator willShowAuthenticationController:(UIViewController*) viewController; - (void) authenticator:(BITAuthenticator *)authenticator willShowAuthenticationController:(UIViewController*) viewController;
@end @end

View File

@ -35,10 +35,16 @@
#import "BITHockeyAppClient.h" #import "BITHockeyAppClient.h"
#import "BITHockeyHelper.h" #import "BITHockeyHelper.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 kBITAuthenticatorAuthTokenKey = @"BITAuthenticatorAuthTokenKey";
static NSString* const kBITAuthenticatorAuthTokenTypeKey = @"BITAuthenticatorAuthTokenTypeKey"; static NSString* const kBITAuthenticatorAuthTokenTypeKey = @"BITAuthenticatorAuthTokenTypeKey";
static NSString* const kBITAuthenticatorLastAuthenticatedVersionKey = @"BITAuthenticatorLastAuthenticatedVersionKey";
static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticatorDidSkipOptionalLogin";
@implementation BITAuthenticator { @implementation BITAuthenticator {
id _appDidBecomeActiveObserver; id _appDidBecomeActiveObserver;
@ -55,8 +61,10 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
if( self ) { if( self ) {
_webpageURL = [NSURL URLWithString:@"https://rink.hockeyapp.net/"]; _webpageURL = [NSURL URLWithString:@"https://rink.hockeyapp.net/"];
_authenticationType = BITAuthenticatorAuthTypeUDIDProvider; _identificationType = BITAuthenticatorIdentificationTypeAnonymous;
_validationType = BITAuthenticatorValidationTypeNever; _automaticMode = YES;
_restrictApplicationUsage = NO;
_restrictionEnforcementFrequency = BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch;
} }
return self; return self;
} }
@ -68,7 +76,7 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
switch ([[UIApplication sharedApplication] applicationState]) { switch ([[UIApplication sharedApplication] applicationState]) {
case UIApplicationStateActive: case UIApplicationStateActive:
[self triggerAuthentication]; [self authenticate];
break; break;
case UIApplicationStateBackground: case UIApplicationStateBackground:
case UIApplicationStateInactive: case UIApplicationStateInactive:
@ -80,65 +88,162 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
} }
#pragma mark - #pragma mark -
- (void) triggerAuthentication { - (void) authenticate {
switch (self.validationType) { //when running in manual mode, we don't actually do anything ourselves
case BITAuthenticatorValidationTypeOnAppActive: if(!self.automaticMode) return;
[self validateInstallationWithCompletion:[self defaultValidationCompletionBlock]];
break;
case BITAuthenticatorValidationTypeOnFirstLaunch:
if(![self.lastAuthenticatedVersion isEqualToString:self.executableUUID]) {
self.installationIdentificationValidated = NO;
[self validateInstallationWithCompletion:[self defaultValidationCompletionBlock]];
} else {
self.installationIdentificationValidated = YES;
}
break;
case BITAuthenticatorValidationTypeOptional:
if(NO == self.didSkipOptionalLogin) {
[self validateInstallationWithCompletion:[self defaultValidationCompletionBlock]];
} else {
self.installationIdentificationValidated = YES;
}
break;
case BITAuthenticatorValidationTypeNever:
self.installationIdentificationValidated = YES;
break;
}
}
#pragma mark - [self identifyWithCompletion:^(BOOL identified, NSError *error) {
- (NSString *)installationIdentification { if(identified) {
NSString *authToken = self.authenticationToken; if([self needsValidation]) {
if(authToken) { [self validateWithCompletion:^(BOOL validated, NSError *error) {
return authToken; if(validated) {
} [_authenticationController dismissViewControllerAnimated:YES completion:nil];
return bit_appAnonID(); _authenticationController = nil;
}
- (NSString*) installationIdentificationType {
NSString *authToken = self.authenticationToken;
if(nil == authToken) {
return @"udid";
} else { } else {
return [self authenticationTokenType]; [self storeInstallationIdentifier:nil withType:self.identificationType];
} UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil
} message:error.localizedDescription
delegate:nil
#pragma mark - Validation cancelButtonTitle:BITHockeyLocalizedString(@"HockeyOK")
- (void) validateInstallationWithCompletion:(tValidationCompletion) completion { otherButtonTitles:nil];
if(nil == self.authenticationToken) { [alertView show];
[self authenticateWithCompletion:^(NSString *authenticationToken, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{
if(nil == authenticationToken) { [self authenticate];
//if authentication fails, there's nothing to validate });
NSError *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
code:BITAuthenticatorNotAuthorized
userInfo:nil];
if(completion) completion(NO, error);
} else {
if(completion) completion(YES, nil);
} }
}]; }];
} else { } else {
[_authenticationController dismissViewControllerAnimated:YES completion:nil];
_authenticationController = nil;
}
} else {
BITHockeyLog(@"Failed to identify. Error: %@", error);
}
}];
}
- (BOOL) needsValidation {
if(BITAuthenticatorIdentificationTypeAnonymous == self.identificationType) {
return NO;
}
if(NO == self.restrictApplicationUsage) {
return NO;
}
if(YES == self.isValidated) {
return NO;
}
if(self.restrictionEnforcementFrequency == BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch &&
![self.executableUUID isEqualToString:self.lastAuthenticatedVersion]) {
return YES;
}
if(self.restrictionEnforcementFrequency == BITAuthenticatorAppRestrictionEnforcementOnAppActive) {
return YES;
}
return NO;
}
- (void) identifyWithCompletion:(void (^)(BOOL identified, NSError *))completion {
if(_authenticationController) {
BITHockeyLog(@"Authentication controller already visible. Ingoring 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]) {
BITHockeyLog(@"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;
}
//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;
break;
case BITAuthenticatorIdentificationTypeHockeyAppUser:
viewController = [[BITAuthenticationViewController alloc] initWithDelegate:self];
viewController.requirePassword = YES;
break;
case BITAuthenticatorIdentificationTypeDevice:
viewController = [[BITAuthenticationViewController alloc] initWithDelegate:self];
viewController.requirePassword = NO;
viewController.showsLoginViaWebButton = YES;
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;
break;
}
if([self.delegate respondsToSelector:@selector(authenticator:willShowAuthenticationController:)]) {
[self.delegate authenticator:self willShowAuthenticationController:viewController];
}
NSAssert(viewController, @"ViewController should've been created");
viewController.email = [self stringValueFromKeychainForKey:kBITAuthenticatorUserEmailKey];
_authenticationController = viewController;
_identificationCompletion = completion;
[self showView:viewController];
}
#pragma mark - Validation
- (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:
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]; NSString *validationPath = [NSString stringWithFormat:@"api/3/apps/%@/identity/validate", self.encodedAppIdentifier];
__weak typeof (self) weakSelf = self; __weak typeof (self) weakSelf = self;
[self.hockeyAppClient getPath:validationPath [self.hockeyAppClient getPath:validationPath
@ -153,24 +258,24 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
NSError *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain NSError *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
code:BITAuthenticatorNetworkError code:BITAuthenticatorNetworkError
userInfo:userInfo]; userInfo:userInfo];
[strongSelf validationFailedWithError:error completion:completion]; self.validated = NO;
if(completion) completion(NO, error);
} else { } else {
NSError *validationParseError = nil; NSError *validationParseError = nil;
BOOL isValidated = [strongSelf.class isValidationResponseValid:responseData error:&validationParseError]; BOOL valid = [strongSelf.class isValidationResponseValid:responseData error:&validationParseError];
if(isValidated) { strongSelf.validated = valid;
[strongSelf validationSucceededWithCompletion:completion]; if(valid) {
} else { [self setLastAuthenticatedVersion:self.executableUUID];
[strongSelf validationFailedWithError:validationParseError completion:completion];
} }
if(completion) completion(valid, validationParseError);
} }
}]; }];
} }
}
- (NSDictionary*) validationParameters { - (NSDictionary*) validationParameters {
NSParameterAssert(self.authenticationToken); NSParameterAssert(self.installationIdentifier);
NSParameterAssert(self.installationIdentificationType); NSParameterAssert(self.installationIdentifierTypeString);
return @{self.installationIdentificationType : self.authenticationToken}; return @{self.installationIdentifierTypeString : self.installationIdentifier};
} }
+ (BOOL) isValidationResponseValid:(id) response error:(NSError **) error { + (BOOL) isValidationResponseValid:(id) response error:(NSError **) error {
@ -224,105 +329,13 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
} }
} }
#pragma mark - Authentication
- (void)authenticateWithCompletion:(tAuthenticationCompletion)completion {
if(_authenticationController) {
BITHockeyLog(@"Already authenticating. Ignoring request");
return;
}
if(_authenticationType == BITAuthenticatorAuthTypeEmail && (nil == _authenticationSecret || !_authenticationSecret.length)) {
if(completion) {
NSError *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
code:BITAuthenticatorAuthorizationSecretMissing
userInfo:@{NSLocalizedDescriptionKey: @"HockeyAuthenticationAuthSecretMissing"}];
completion(nil, error);
}
return;
}
BITAuthenticationViewController *viewController = [[BITAuthenticationViewController alloc] initWithDelegate:self];
switch (self.authenticationType) {
case BITAuthenticatorAuthTypeEmailAndPassword:
viewController.requirePassword = YES;
break;
case BITAuthenticatorAuthTypeEmail:
viewController.requirePassword = NO;
break;
case BITAuthenticatorAuthTypeUDIDProvider:
viewController.requirePassword = NO;
viewController.showsLoginViaWebButton = YES;
break;
}
switch (self.validationType) {
case BITAuthenticatorValidationTypeNever:
case BITAuthenticatorValidationTypeOptional:
viewController.showsSkipButton = YES;
break;
case BITAuthenticatorValidationTypeOnAppActive:
case BITAuthenticatorValidationTypeOnFirstLaunch:
viewController.showsSkipButton = NO;
break;
}
if([self.delegate respondsToSelector:@selector(authenticator:willShowAuthenticationController:)]) {
[self.delegate authenticator:self willShowAuthenticationController:viewController];
}
_authenticationController = viewController;
_authenticationCompletionBlock = completion;
[self showView:viewController];
}
- (void) didAuthenticateWithToken:(NSString*) token {
[_authenticationController dismissViewControllerAnimated:YES completion:nil];
_authenticationController = nil;
[self setAuthenticationToken:token withType:[self.class stringForAuthenticationType:self.authenticationType]];
self.installationIdentificationValidated = YES;
self.lastAuthenticatedVersion = [self executableUUID];
if(self.authenticationCompletionBlock) {
self.authenticationCompletionBlock(self.authenticationToken, nil);
self.authenticationCompletionBlock = nil;
}
}
+ (NSString*) stringForAuthenticationType:(BITAuthenticatorAuthType) authType {
switch (authType) {
case BITAuthenticatorAuthTypeEmail: return @"iuid";
case BITAuthenticatorAuthTypeEmailAndPassword: return @"auid";
case BITAuthenticatorAuthTypeUDIDProvider:
//fallthrough
default:
return @"udid";
break;
}
}
#pragma mark - AuthenticationViewControllerDelegate #pragma mark - AuthenticationViewControllerDelegate
- (void) authenticationViewControllerDidSkip:(UIViewController *)viewController {
[viewController dismissViewControllerAnimated:YES completion:nil];
_authenticationController = nil;
[self setAuthenticationToken:nil withType:nil];
if(self.validationType == BITAuthenticatorValidationTypeOptional) {
self.didSkipOptionalLogin = YES;
self.installationIdentificationValidated = YES;
}
NSError *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
code:BITAuthenticatorAuthenticationCancelled
userInfo:nil];
if(self.authenticationCompletionBlock) {
self.authenticationCompletionBlock(self.authenticationToken, error);
self.authenticationCompletionBlock = nil;
}
}
- (void)authenticationViewController:(UIViewController *)viewController - (void)authenticationViewController:(UIViewController *)viewController
handleAuthenticationWithEmail:(NSString *)email handleAuthenticationWithEmail:(NSString *)email
password:(NSString *)password password:(NSString *)password
completion:(void (^)(BOOL, NSError *))completion { completion:(void (^)(BOOL, NSError *))completion {
NSParameterAssert(email && email.length); NSParameterAssert(email && email.length);
NSParameterAssert(self.authenticationType == BITAuthenticatorAuthTypeEmail || (password && password.length)); NSParameterAssert(self.identificationType == BITAuthenticatorIdentificationTypeHockeyAppEmail || (password && password.length));
NSURLRequest* request = [self requestForAuthenticationEmail:email password:password]; NSURLRequest* request = [self requestForAuthenticationEmail:email password:password];
__weak typeof (self) weakSelf = self; __weak typeof (self) weakSelf = self;
BITHTTPOperation *operation = [self.hockeyAppClient operationWithURLRequest:request BITHTTPOperation *operation = [self.hockeyAppClient operationWithURLRequest:request
@ -332,12 +345,23 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
NSString *authToken = [strongSelf.class authenticationTokenFromURLResponse:operation.response NSString *authToken = [strongSelf.class authenticationTokenFromURLResponse:operation.response
data:responseData data:responseData
error:&authParseError]; error:&authParseError];
if(nil == authToken) { BOOL identified;
completion(NO, authParseError); if(authToken) {
identified = YES;
[strongSelf storeInstallationIdentifier:authToken withType:strongSelf.identificationType];
[strongSelf->_authenticationController dismissViewControllerAnimated:YES
completion:nil];
strongSelf->_authenticationController = nil;
[self addStringValueToKeychain:email forKey:kBITAuthenticatorUserEmailKey];
} else { } else {
//no need to call completion, we're dismissing it anyways identified = NO;
[self didAuthenticateWithToken:authToken]; }
}}]; self.identified = identified;
completion(identified, authParseError);
if(strongSelf.identificationCompletion) strongSelf.identificationCompletion(identified, authParseError);
strongSelf.identificationCompletion = nil;
}];
[self.hockeyAppClient enqeueHTTPOperation:operation]; [self.hockeyAppClient enqeueHTTPOperation:operation];
} }
@ -345,7 +369,7 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
NSString *authenticationPath = [self authenticationPath]; NSString *authenticationPath = [self authenticationPath];
NSDictionary *params = nil; NSDictionary *params = nil;
if(BITAuthenticatorAuthTypeEmail == self.authenticationType) { if(BITAuthenticatorIdentificationTypeHockeyAppEmail == self.identificationType) {
NSString *authCode = BITHockeyMD5([NSString stringWithFormat:@"%@%@", NSString *authCode = BITHockeyMD5([NSString stringWithFormat:@"%@%@",
self.authenticationSecret ? : @"", self.authenticationSecret ? : @"",
email ? : @""]); email ? : @""]);
@ -358,7 +382,7 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
NSMutableURLRequest *request = [self.hockeyAppClient requestWithMethod:@"POST" NSMutableURLRequest *request = [self.hockeyAppClient requestWithMethod:@"POST"
path:authenticationPath path:authenticationPath
parameters:params]; parameters:params];
if(BITAuthenticatorAuthTypeEmailAndPassword == self.authenticationType) { if(BITAuthenticatorIdentificationTypeHockeyAppUser == self.identificationType) {
NSString *authStr = [NSString stringWithFormat:@"%@:%@", email, password]; NSString *authStr = [NSString stringWithFormat:@"%@:%@", email, password];
NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding]; NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
NSString *authValue = [NSString stringWithFormat:@"Basic %@", bit_base64String(authData, authData.length)]; NSString *authValue = [NSString stringWithFormat:@"Basic %@", bit_base64String(authData, authData.length)];
@ -368,14 +392,13 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
} }
- (NSString *) authenticationPath { - (NSString *) authenticationPath {
if(BITAuthenticatorAuthTypeEmailAndPassword == self.authenticationType) { if(BITAuthenticatorIdentificationTypeHockeyAppUser == self.identificationType) {
return [NSString stringWithFormat:@"api/3/apps/%@/identity/authorize", self.encodedAppIdentifier]; return [NSString stringWithFormat:@"api/3/apps/%@/identity/authorize", self.encodedAppIdentifier];
} else { } else {
return [NSString stringWithFormat:@"api/3/apps/%@/identity/check", self.encodedAppIdentifier]; return [NSString stringWithFormat:@"api/3/apps/%@/identity/check", self.encodedAppIdentifier];
} }
} }
+ (NSString *) authenticationTokenFromURLResponse:(NSHTTPURLResponse*) urlResponse data:(NSData*) data error:(NSError **) error { + (NSString *) authenticationTokenFromURLResponse:(NSHTTPURLResponse*) urlResponse data:(NSData*) data error:(NSError **) error {
NSParameterAssert(urlResponse); NSParameterAssert(urlResponse);
if(nil == urlResponse) { if(nil == urlResponse) {
@ -412,7 +435,7 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
} }
if(200 != urlResponse.statusCode && 404 != urlResponse.statusCode) { if(200 != urlResponse.statusCode && 404 != urlResponse.statusCode) {
//make sure we have an error created if user wanted to have one //make sure we have an error created if user wanted to have one
NSParameterAssert(0 == error || *error); NSParameterAssert(nil == error || *error);
return nil; return nil;
} }
@ -475,18 +498,27 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
} }
if(udid){ if(udid){
[self setAuthenticationToken:udid withType:[self.class stringForAuthenticationType:BITAuthenticatorAuthTypeUDIDProvider]]; if(NO == self.restrictApplicationUsage) {
[self validateInstallationWithCompletion:^(BOOL validated, NSError *error) {
if(validated) {
[_authenticationController dismissViewControllerAnimated:YES completion:nil]; [_authenticationController dismissViewControllerAnimated:YES completion:nil];
_authenticationController = nil; _authenticationController = nil;
} else {
//TODO: show why validation failed
} }
}]; [self storeInstallationIdentifier:udid withType:BITAuthenticatorIdentificationTypeDevice];
self.identified = YES;
if(self.identificationCompletion) {
self.identificationCompletion(YES, nil);
self.identificationCompletion = nil;
}
} else { } else {
//reset auth-token //reset token
[self setAuthenticationToken:nil withType:nil]; [self storeInstallationIdentifier:nil withType:BITAuthenticatorIdentificationTypeDevice];
self.identified = NO;
if(self.identificationCompletion) {
NSError *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain
code:BITAuthenticatorErrorUnknown
userInfo:@{NSLocalizedDescriptionKey : @"Failed to retrieve UDID from URL"}];
self.identificationCompletion(NO, error);
self.identificationCompletion = nil;
}
} }
return YES; return YES;
} }
@ -517,32 +549,20 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
} }
} }
#pragma mark - Validation Pseudo-Delegate
- (void)validationFailedWithError:(NSError *)validationError completion:(tValidationCompletion) completion{
if(completion) {
completion(NO, validationError);
}
}
- (void)validationSucceededWithCompletion:(tValidationCompletion) completion {
self.installationIdentificationValidated = YES;
if(completion) {
completion(YES, nil);
}
}
#pragma mark - Private helpers #pragma mark - Private helpers
- (UIDevice *)currentDevice {
return _currentDevice ? : [UIDevice currentDevice];;
}
- (void) cleanupInternalStorage { - (void) cleanupInternalStorage {
[self removeKeyFromKeychain:kBITAuthenticatorIdentifierTypeKey];
[self removeKeyFromKeychain:kBITAuthenticatorIdentifierKey];
[self removeKeyFromKeychain:kBITAuthenticatorUUIDKey];
[self removeKeyFromKeychain:kBITAuthenticatorUserEmailKey];
[self setLastAuthenticatedVersion:nil];
//cleanup values stored from 3.5 Beta1..Beta3
[self removeKeyFromKeychain:kBITAuthenticatorAuthTokenKey]; [self removeKeyFromKeychain:kBITAuthenticatorAuthTokenKey];
[self removeKeyFromKeychain:kBITAuthenticatorAuthTokenTypeKey]; [self removeKeyFromKeychain:kBITAuthenticatorAuthTokenTypeKey];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kBITAuthenticatorDidSkipOptionalLogin];
[self setLastAuthenticatedVersion:nil];
} }
#pragma mark - KVO
- (void) registerObservers { - (void) registerObservers {
__weak typeof(self) weakSelf = self; __weak typeof(self) weakSelf = self;
if(nil == _appDidBecomeActiveObserver) { if(nil == _appDidBecomeActiveObserver) {
@ -577,38 +597,23 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
} }
#pragma mark - Property overrides #pragma mark - Property overrides
- (void)setAuthenticationToken:(NSString *)authenticationToken withType:(NSString*) authenticationTokenType { - (void)storeInstallationIdentifier:(NSString *)installationIdentifier withType:(BITAuthenticatorIdentificationType) type {
NSParameterAssert(nil == authenticationToken || nil != authenticationTokenType); if(nil == installationIdentifier) {
if(![self.authenticationToken isEqualToString:authenticationToken] || ![self.authenticationTokenType isEqualToString:authenticationTokenType]) { [self removeKeyFromKeychain:kBITAuthenticatorIdentifierKey];
[self willChangeValueForKey:@"installationIdentification"]; [self removeKeyFromKeychain:kBITAuthenticatorIdentifierTypeKey];
if(nil == authenticationToken) {
[self removeKeyFromKeychain:kBITAuthenticatorAuthTokenKey];
[self removeKeyFromKeychain:kBITAuthenticatorAuthTokenTypeKey];
} else { } else {
[self addStringValueToKeychainForThisDeviceOnly:authenticationToken forKey:kBITAuthenticatorAuthTokenKey]; BOOL success = [self addStringValueToKeychainForThisDeviceOnly:installationIdentifier
[self addStringValueToKeychainForThisDeviceOnly:authenticationTokenType forKey:kBITAuthenticatorAuthTokenTypeKey]; forKey:kBITAuthenticatorIdentifierKey];
} NSParameterAssert(success);
[self didChangeValueForKey:@"installationIdentification"]; success = [self addStringValueToKeychainForThisDeviceOnly:[self.class stringForIdentificationType:type]
forKey:kBITAuthenticatorIdentifierTypeKey];
NSParameterAssert(success);
} }
} }
- (NSString *)authenticationToken { - (NSString*) installationIdentifier {
NSString *authToken = [self stringValueFromKeychainForKey:kBITAuthenticatorAuthTokenKey]; NSString *identifier = [self stringValueFromKeychainForKey:kBITAuthenticatorIdentifierKey];
if(nil == authToken) return nil; return identifier;
//check if the auth token matches the current setting
if(![self.authenticationTokenType isEqualToString:[self.class stringForAuthenticationType:self.authenticationType]]) {
BITHockeyLog(@"Auth type mismatch for stored auth-token. Resetting.");
[self removeKeyFromKeychain:kBITAuthenticatorAuthTokenKey];
return nil;
}
return authToken;
}
- (NSString *)authenticationTokenType {
NSString *authTokenType = [self stringValueFromKeychainForKey:kBITAuthenticatorAuthTokenTypeKey];
return authTokenType;
} }
- (void)setLastAuthenticatedVersion:(NSString *)lastAuthenticatedVersion { - (void)setLastAuthenticatedVersion:(NSString *)lastAuthenticatedVersion {
@ -626,46 +631,46 @@ static NSString* const kBITAuthenticatorDidSkipOptionalLogin = @"BITAuthenticato
return [[NSUserDefaults standardUserDefaults] objectForKey:kBITAuthenticatorLastAuthenticatedVersionKey]; return [[NSUserDefaults standardUserDefaults] objectForKey:kBITAuthenticatorLastAuthenticatedVersionKey];
} }
- (void)setDidSkipOptionalLogin:(BOOL)didSkipOptionalLogin { - (NSString *)installationIdentifierTypeString {
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; return [self.class stringForIdentificationType:self.identificationType];
if(NO == didSkipOptionalLogin){ }
[defaults removeObjectForKey:kBITAuthenticatorDidSkipOptionalLogin];
} else { + (NSString *)stringForIdentificationType:(BITAuthenticatorIdentificationType) identificationType {
[defaults setObject:@(YES) switch(identificationType) {
forKey:kBITAuthenticatorDidSkipOptionalLogin]; case BITAuthenticatorIdentificationTypeHockeyAppEmail: return @"iuid";
[defaults synchronize]; case BITAuthenticatorIdentificationTypeHockeyAppUser: return @"auid";
case BITAuthenticatorIdentificationTypeDevice: return @"udid";
case BITAuthenticatorIdentificationTypeAnonymous: return @"uuid";
} }
} }
- (BOOL)didSkipOptionalLogin { - (void)setIdentificationType:(BITAuthenticatorIdentificationType)identificationType {
return [[NSUserDefaults standardUserDefaults] objectForKey:kBITAuthenticatorDidSkipOptionalLogin]; if(_identificationType != identificationType) {
_identificationType = identificationType;
self.identified = NO;
self.validated = NO;
}
}
- (NSString *)publicInstallationIdentifier {
switch (self.identificationType) {
case BITAuthenticatorIdentificationTypeHockeyAppEmail:
case BITAuthenticatorIdentificationTypeHockeyAppUser:
return [self stringValueFromKeychainForKey:kBITAuthenticatorUserEmailKey];
case BITAuthenticatorIdentificationTypeAnonymous:
case BITAuthenticatorIdentificationTypeDevice:
return [self stringValueFromKeychainForKey:kBITAuthenticatorIdentifierKey];
}
} }
#pragma mark - Application Lifecycle #pragma mark - Application Lifecycle
- (void)applicationDidBecomeActive:(NSNotification *)note { - (void)applicationDidBecomeActive:(NSNotification *)note {
[self triggerAuthentication]; [self authenticate];
} }
- (void)applicationWillResignActive:(NSNotification *)note { - (void)applicationWillResignActive:(NSNotification *)note {
if(BITAuthenticatorValidationTypeOnAppActive == self.validationType) { if(BITAuthenticatorAppRestrictionEnforcementOnAppActive == self.restrictionEnforcementFrequency) {
self.installationIdentificationValidated = NO; self.validated = NO;
} }
} }
#pragma mark -
- (tValidationCompletion) defaultValidationCompletionBlock {
return ^(BOOL validated, NSError *error) {
switch (self.validationType) {
case BITAuthenticatorValidationTypeNever:
case BITAuthenticatorValidationTypeOptional:
break;
case BITAuthenticatorValidationTypeOnAppActive:
case BITAuthenticatorValidationTypeOnFirstLaunch:
if(!validated) {
[self authenticateWithCompletion:nil];
}
break;
}
};
};
@end @end

View File

@ -42,71 +42,22 @@
*/ */
@property (nonatomic, strong) BITHockeyAppClient *hockeyAppClient; @property (nonatomic, strong) BITHockeyAppClient *hockeyAppClient;
#pragma mark - Identification
/**
* Provides an identification for the current app installation
*
* During Alpha and Beta-phase HockeyApp tries to uniquely identify each app installation
* to provide better error reporting & analytics. If authenticator is configured to login
* (@see BITAuthenticatorValidationType), this identifier is retrieved from HockeyApp. In case
* it is disabled, it returns the vendorIdentifier provided by UIKit.
* KVO'able
*
* @return a string identifying this app installation
*/
@property (nonatomic, readonly) NSString *installationIdentification;
/**
* Specifies if the installationIdentification has been validated
*
* Depending on authenticationType and validationType this flag switches between
* YES/NO during application runtime. If it's YES, the installationIdentification has been
* validated (either against the backend on app launch / initial run of this version) or validation
* is not required. The flag is first updated (and stays NO) until the manager has been started.
* KVO'able
*/
@property (nonatomic, readwrite) BOOL installationIdentificationValidated;
/**
* Specifies the type of installation identification for the current app installation
*/
@property (nonatomic, readonly) NSString *installationIdentificationType;
#pragma mark - #pragma mark -
//can be set for testing
@property (nonatomic) UIDevice *currentDevice;
#pragma mark -
/**
* if set, this serves as the installationIdentifier.
* This is retrieved from the hockeyApp backend
* @see installationIdentification
*/
@property (nonatomic, readonly) NSString *authenticationToken;
/**
* store the authentication token with the given type
* if authToken is not nil, authentikationTokenType must also be non nil
*
* @param authenticationToken The authentication token
* @param authenticationTokenType The authentication token type
*/
- (void)setAuthenticationToken:(NSString *)authenticationToken withType:(NSString*) authenticationTokenType;
/** /**
* holds the identifier of the last version that was authenticated * holds the identifier of the last version that was authenticated
* only used if validation is set BITAuthenticatorValidationTypeOnFirstLaunch * only used if validation is set BITAuthenticatorValidationTypeOnFirstLaunch
*/ */
@property (nonatomic, copy) NSString *lastAuthenticatedVersion; @property (nonatomic, copy) NSString *lastAuthenticatedVersion;
@property (nonatomic, copy) tAuthenticationCompletion authenticationCompletionBlock; /**
* return the string used to identify this app against the HockeyApp backend.
*/
@property (nonatomic, copy, readonly) NSString *installationIdentifierTypeString;
/** /**
* removes all previously stored authentication tokens, UDIDs, etc * returns the string used to identify this app against the HockeyApp backend.
*/ */
- (void) cleanupInternalStorage; @property (nonatomic, copy, readonly) NSString *installationIdentifier;
/** /**
* method registered as observer for applicationWillBecomeInactive events * method registered as observer for applicationWillBecomeInactive events
@ -122,49 +73,14 @@
*/ */
- (void) applicationDidBecomeActive:(NSNotification*) note; - (void) applicationDidBecomeActive:(NSNotification*) note;
/** @property (nonatomic, copy) void(^identificationCompletion)(BOOL identified, NSError* error);
* once the user skipped the optional login, this is set to YES
* (and thus the optional login should never be shown again)
* persisted to disk. Defaults to NO
*/
@property (nonatomic, assign) BOOL didSkipOptionalLogin;
#pragma mark - Authentication #pragma mark - Overrides
/** @property (nonatomic, assign, readwrite, getter = isIdentified) BOOL identified;
* Authenticate this app installation @property (nonatomic, assign, readwrite, getter = isValidated) BOOL validated;
*
* Depending on 'authenticationType', this tries to authenticate the app installation
* against the HockeyApp server.
* You should not need to call this, as it's done automatically once the manager has
* been started, depending on validationType.
*
* @param completion if nil, success/failure is reported via the delegate, if not nil, the
* delegate methods are not called.
*/
- (void) authenticateWithCompletion:(tAuthenticationCompletion) completion;
#pragma mark - Internal Auth callbacks #pragma mark - Testing
- (void) didAuthenticateWithToken:(NSString*) token; - (void) storeInstallationIdentifier:(NSString*) identifier withType:(BITAuthenticatorIdentificationType) type;
- (BOOL) needsValidation;
#pragma mark - Validation - (void) authenticate;
/**
* Validate the app installation
*
* Depending on @see validationType, this is called by the manager after the app becomes active
* and tries to revalidate the installation.
* You should not need to call this, as it's done automatically once the manager has
* been started, depending on validationType.
*
* @param completion if nil, success/failure is reported via the delegate, if not nil, the
* delegate methods are not called
*/
- (void) validateInstallationWithCompletion:(tValidationCompletion) completion;
#pragma mark - Validation callbacks
- (void) validationSucceededWithCompletion:(tValidationCompletion) completion;
- (void) validationFailedWithError:(NSError *) validationError completion:(tValidationCompletion) completion;
#pragma mark - Helpers for testing
- (tValidationCompletion) defaultValidationCompletionBlock;
@end @end

View File

@ -210,7 +210,7 @@
// start Authenticator // start Authenticator
if (![self isAppStoreEnvironment]) { if (![self isAppStoreEnvironment]) {
// hook into manager with kvo! // hook into manager with kvo!
[_authenticator addObserver:self forKeyPath:@"installationIdentificationValidated" options:0 context:nil]; [_authenticator addObserver:self forKeyPath:@"identified" options:0 context:nil];
BITHockeyLog(@"INFO: Start Authenticator"); BITHockeyLog(@"INFO: Start Authenticator");
if (_serverURL) { if (_serverURL) {
@ -226,7 +226,7 @@
|| [[self class] isJMCPresent] || [[self class] isJMCPresent]
#endif /* HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT */ #endif /* HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT */
) { ) {
if ([self.authenticator installationIdentificationValidated]) { if ([self.authenticator isIdentified]) {
[self invokeStartUpdateManager]; [self invokeStartUpdateManager];
} }
} }
@ -283,11 +283,11 @@
#if HOCKEYSDK_FEATURE_UPDATES #if HOCKEYSDK_FEATURE_UPDATES
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"installationIdentificationValidated"] && if ([keyPath isEqualToString:@"identified"] &&
[object valueForKey:@"installationIdentificationValidated"] ) { [object valueForKey:@"isIdentified"] ) {
if ((![self isAppStoreEnvironment] && ![self isUpdateManagerDisabled])) { if ((![self isAppStoreEnvironment] && ![self isUpdateManagerDisabled])) {
BOOL isValidated = [(NSNumber *)[object valueForKey:@"installationIdentificationValidated"] boolValue]; BOOL identified = [(NSNumber *)[object valueForKey:@"isIdentified"] boolValue];
if (isValidated) { if (identified) {
[self invokeStartUpdateManager]; [self invokeStartUpdateManager];
} }
} }
@ -330,9 +330,9 @@
[_updateManager setServerURL:_serverURL]; [_updateManager setServerURL:_serverURL];
} }
if (_authenticator) { if (_authenticator) {
[_updateManager setInstallationIdentification:[self.authenticator installationIdentification]]; [_updateManager setInstallationIdentification:[self.authenticator installationIdentifier]];
[_updateManager setInstallationIdentificationType:[self.authenticator installationIdentificationType]]; [_updateManager setInstallationIdentificationType:[self.authenticator installationIdentifierTypeString]];
[_updateManager setInstallationIdentificationValidated:[self.authenticator installationIdentificationValidated]]; [_updateManager setInstallationIdentified:[self.authenticator isIdentified]];
} }
[_updateManager performSelector:@selector(startManager) withObject:nil afterDelay:0.5f]; [_updateManager performSelector:@selector(startManager) withObject:nil afterDelay:0.5f];
} }

View File

@ -535,7 +535,7 @@ typedef NS_ENUM(NSInteger, BITUpdateAlertViewTag) {
- (void)checkForUpdate { - (void)checkForUpdate {
if (![self isAppStoreEnvironment] && ![self isUpdateManagerDisabled]) { if (![self isAppStoreEnvironment] && ![self isUpdateManagerDisabled]) {
if ([self expiryDateReached]) return; if ([self expiryDateReached]) return;
if (![self installationIdentificationValidated]) return; if (![self installationIdentified]) return;
if (self.isUpdateAvailable && [self hasNewerMandatoryVersion]) { if (self.isUpdateAvailable && [self hasNewerMandatoryVersion]) {
[self showCheckForUpdateAlert]; [self showCheckForUpdateAlert];
@ -881,12 +881,6 @@ typedef NS_ENUM(NSInteger, BITUpdateAlertViewTag) {
} }
} }
- (void)setInstallationIdentificationValidated:(BOOL)installationIdentificationValidated {
if (installationIdentificationValidated != _installationIdentificationValidated) {
_installationIdentificationValidated = installationIdentificationValidated;
}
}
- (void)setInstallationIdentification:(NSString *)installationIdentification { - (void)setInstallationIdentification:(NSString *)installationIdentification {
if (![_installationIdentification isEqualToString:installationIdentification]) { if (![_installationIdentification isEqualToString:installationIdentification]) {
if (installationIdentification) { if (installationIdentification) {

View File

@ -64,7 +64,7 @@
@property (nonatomic, strong) NSString *installationIdentificationType; @property (nonatomic, strong) NSString *installationIdentificationType;
@property (nonatomic) BOOL installationIdentificationValidated; @property (nonatomic) BOOL installationIdentified;
// if YES, the API will return an existing JMC config // if YES, the API will return an existing JMC config
// if NO, the API will return only version information // if NO, the API will return only version information

View File

@ -174,10 +174,6 @@ typedef NS_ENUM(NSInteger, BITAuthenticatorReason) {
* Not Authorized * Not Authorized
*/ */
BITAuthenticatorNotAuthorized, BITAuthenticatorNotAuthorized,
/**
* Authorization cancelled
*/
BITAuthenticatorAuthenticationCancelled,
/** /**
* Unknown Application ID (configuration error) * Unknown Application ID (configuration error)
*/ */
@ -186,6 +182,10 @@ typedef NS_ENUM(NSInteger, BITAuthenticatorReason) {
* Authorization secret missing * Authorization secret missing
*/ */
BITAuthenticatorAuthorizationSecretMissing, BITAuthenticatorAuthorizationSecretMissing,
/**
* Not yet identified
*/
BITAuthenticatorNotIdentified,
}; };
extern NSString *const __attribute__((unused)) kBITAuthenticatorErrorDomain; extern NSString *const __attribute__((unused)) kBITAuthenticatorErrorDomain;

View File

@ -19,6 +19,7 @@
#import "BITAuthenticator_Private.h" #import "BITAuthenticator_Private.h"
#import "BITHTTPOperation.h" #import "BITHTTPOperation.h"
#import "BITTestHelper.h" #import "BITTestHelper.h"
#import "BITHockeyAppClient.h"
@interface MyDevice : NSObject @interface MyDevice : NSObject
- (NSString*) uniqueIdentifier; - (NSString*) uniqueIdentifier;
@ -56,7 +57,6 @@ static void *kInstallationIdentification = &kInstallationIdentification;
[super setUp]; [super setUp];
_sut = [[BITAuthenticator alloc] initWithAppIdentifier:nil isAppStoreEnvironment:NO]; _sut = [[BITAuthenticator alloc] initWithAppIdentifier:nil isAppStoreEnvironment:NO];
_sut.authenticationType = BITAuthenticatorAuthTypeEmailAndPassword;
} }
- (void)tearDown { - (void)tearDown {
@ -88,12 +88,6 @@ static void *kInstallationIdentification = &kInstallationIdentification;
} }
#pragma mark - Persistence Tests #pragma mark - Persistence Tests
- (void) testThatAuthenticationTokenIsPersisted {
[_sut setAuthenticationToken:@"SuperToken" withType:@"udid"];
_sut = [[BITAuthenticator alloc] initWithAppIdentifier:nil isAppStoreEnvironment:YES];
assertThat(_sut.authenticationToken, equalTo(@"SuperToken"));
}
- (void) testThatLastAuthenticatedVersionIsPersisted { - (void) testThatLastAuthenticatedVersionIsPersisted {
_sut.lastAuthenticatedVersion = @"1.2.1"; _sut.lastAuthenticatedVersion = @"1.2.1";
_sut = [[BITAuthenticator alloc] initWithAppIdentifier:nil isAppStoreEnvironment:YES]; _sut = [[BITAuthenticator alloc] initWithAppIdentifier:nil isAppStoreEnvironment:YES];
@ -101,224 +95,206 @@ static void *kInstallationIdentification = &kInstallationIdentification;
} }
- (void) testThatCleanupWorks { - (void) testThatCleanupWorks {
[_sut setAuthenticationToken:@"MyToken" withType:@"udid"];
_sut.lastAuthenticatedVersion = @"1.2"; _sut.lastAuthenticatedVersion = @"1.2";
[_sut setDidSkipOptionalLogin:YES];
[_sut cleanupInternalStorage]; [_sut cleanupInternalStorage];
assertThat(_sut.authenticationToken, equalTo(nil));
assertThat(_sut.lastAuthenticatedVersion, equalTo(nil)); assertThat(_sut.lastAuthenticatedVersion, equalTo(nil));
assertThat(_sut.installationIdentificationType, equalTo(@"udid")); assertThat(_sut.installationIdentifier, equalTo(nil));
assertThatBool(_sut.didSkipOptionalLogin, equalToBool(NO));
} }
- (void) testThatSkipLoginIsPersisted { #pragma mark - Initial defaults
[_sut setDidSkipOptionalLogin:YES]; - (void) testDefaultValues {
_sut = [[BITAuthenticator alloc] initWithAppIdentifier:nil isAppStoreEnvironment:YES]; assertThatBool(_sut.automaticMode, equalToBool(YES));
assertThatBool(_sut.didSkipOptionalLogin, equalToBool(YES)); assertThatBool(_sut.restrictApplicationUsage, equalToBool(NO));
[_sut setDidSkipOptionalLogin:NO]; assertThatBool(_sut.isIdentified, equalToBool(NO));
_sut = [[BITAuthenticator alloc] initWithAppIdentifier:nil isAppStoreEnvironment:YES]; assertThatBool(_sut.isValidated, equalToBool(NO));
assertThatBool(_sut.didSkipOptionalLogin, equalToBool(NO)); assertThat(_sut.authenticationSecret, equalTo(nil));
} assertThat(_sut.installationIdentifier, equalTo(nil));
assertThat(_sut.installationIdentifierTypeString, equalTo(@"uuid"));
#pragma mark - Identification Tests
- (void) testIdentificationReturnsTheVendorIdentifierIfAvailable {
STAssertEqualObjects([_sut installationIdentification], [[UIDevice currentDevice] identifierForVendor].UUIDString,
@"Freshly initialized, it should return the vendor identifier");
}
- (void) testIdentificationReturnsTheAuthTokenIfSet {
[_sut setAuthenticationType:BITAuthenticatorAuthTypeUDIDProvider];
[_sut setAuthenticationToken:@"PeterPan" withType:@"udid"];
assertThat(_sut.installationIdentification, equalTo(@"PeterPan"));
}
#pragma mark - authentication tests
- (void) testThatAuthenticateWithTypeEmailShowsAViewController {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
_sut.authenticationSecret = @"myscret";
_sut.authenticationType = BITAuthenticatorAuthTypeEmail;
[_sut authenticateWithCompletion:nil];
[verifyCount(delegateMock, times(1)) authenticator:_sut willShowAuthenticationController:(id)anything()];
}
- (void) testThatAuthenticateWithTypeEmailShowsAViewControllerOnlyIfAuthenticationSecretIsSet {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
_sut.authenticationSecret = nil;
_sut.authenticationType = BITAuthenticatorAuthTypeEmail;
[_sut authenticateWithCompletion:nil];
[verifyCount(delegateMock, times(0)) authenticator:_sut willShowAuthenticationController:(id)anything()];
}
- (void) testThatAuthenticateWithTypeEmailAndPasswordShowsAViewController {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
_sut.authenticationType = BITAuthenticatorAuthTypeEmailAndPassword;
[_sut authenticateWithCompletion:nil];
[verifyCount(delegateMock, times(1)) authenticator:_sut willShowAuthenticationController:(id)anything()];
} }
#pragma mark - General identification tests
- (void) testThatIsDoesntShowMoreThanOneAuthenticationController { - (void) testThatIsDoesntShowMoreThanOneAuthenticationController {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate)); id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock; _sut.delegate = delegateMock;
_sut.authenticationType = BITAuthenticatorAuthTypeEmailAndPassword; _sut.identificationType = BITAuthenticatorIdentificationTypeDevice;
[_sut authenticateWithCompletion:nil]; [_sut identifyWithCompletion:nil];
[_sut authenticateWithCompletion:nil]; [_sut identifyWithCompletion:nil];
[_sut authenticateWithCompletion:nil]; [_sut identifyWithCompletion:nil];
[verifyCount(delegateMock, times(1)) authenticator:_sut willShowAuthenticationController:(id)anything()]; [verifyCount(delegateMock, times(1)) authenticator:_sut willShowAuthenticationController:(id)anything()];
} }
- (void) testThatSuccessfulAuthenticationStoresTheToken { - (void) testThatChangingIdentificationTypeResetsIdentifiedFlag {
_sut.identified = YES;
_sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppUser;
assertThatBool(_sut.identified, equalToBool(NO));
}
- (void) testThatAfterChangingIdentificationTypeIdentificationIsRedone {
[_sut storeInstallationIdentifier:@"meh" withType:BITAuthenticatorIdentificationTypeHockeyAppEmail];
_sut.identified = YES;
_sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppUser;
[_sut identifyWithCompletion:nil];
assertThatBool(_sut.identified, equalToBool(NO));
assertThat(_sut.installationIdentifier, nilValue());
}
- (void) testThatIdentifyingAnAlreadyIdentifiedInstanceDoesNothing {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate)); id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock; _sut.delegate = delegateMock;
//this will prepare everything and show the viewcontroller _sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppEmail;
[_sut authenticateWithCompletion:nil]; [_sut storeInstallationIdentifier:@"meh" withType:BITAuthenticatorIdentificationTypeHockeyAppEmail];
//fake delegate call from the viewcontroller _sut.identified = YES;
[_sut didAuthenticateWithToken:@"SuperToken"];
assertThat(_sut.authenticationToken, equalTo(@"SuperToken")); [_sut identifyWithCompletion:nil];
}
- (void) testThatSuccessfulAuthenticationCallsTheBlock {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
[_sut setAuthenticationToken:@"Test" withType:@"adid"];
__block BOOL didAuthenticate = NO;
[_sut authenticateWithCompletion:^(NSString *authenticationToken, NSError *error) {
if(authenticationToken) didAuthenticate = YES;
}];
[_sut didAuthenticateWithToken:@"SuperToken"];
assertThatBool(didAuthenticate, equalToBool(YES));
}
- (void) testThatCancelledAuthenticationSetsProperError {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
//this will prepare everything and show the viewcontroller
__block BOOL didAuthenticateCalled = NO;
__block NSError *authenticationError = nil;
[_sut authenticateWithCompletion:^(NSString *authenticationToken, NSError *error) {
didAuthenticateCalled = YES;
authenticationError = error;
}];
//fake delegate call from the viewcontroller
[_sut authenticationViewControllerDidSkip:nil];
assertThatBool(didAuthenticateCalled, equalToBool(YES));
assertThat(authenticationError, equalTo([NSError errorWithDomain:kBITAuthenticatorErrorDomain
code:BITAuthenticatorAuthenticationCancelled
userInfo:nil]));
}
- (void) testThatCancelledAuthenticationResetsTheToken {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
[_sut setAuthenticationToken:@"Meh" withType:@"bdid"];
//this will prepare everything and show the viewcontroller
[_sut authenticateWithCompletion:nil];
//fake delegate call from the viewcontroller
[_sut authenticationViewControllerDidSkip:nil];
assertThat(_sut.authenticationToken, equalTo(nil));
}
- (void) testThatKVOWorksOnApplicationIdentification {
//this will prepare everything and show the viewcontroller
[_sut authenticateWithCompletion:nil];
[_sut addObserver:self forKeyPath:@"installationIdentification"
options:0
context:kInstallationIdentification];
//fake delegate call from the viewcontroller
[_sut authenticationViewControllerDidSkip:nil];
assertThatBool(_KVOCalled, equalToBool(YES));
[_sut removeObserver:self forKeyPath:@"installationIdentification"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if(kInstallationIdentification == context) {
_KVOCalled = YES;
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - InstallationIdentificationType
- (void) testThatEmailAuthSetsTheProperInstallationIdentificationType {
_sut.authenticationType = BITAuthenticatorAuthTypeEmail;
//fake delegate call from the viewcontroller
[_sut didAuthenticateWithToken:@"SuperToken"];
assertThat(_sut.installationIdentificationType, equalTo(@"iuid"));
}
- (void) testThatPasswordAuthSetsTheProperInstallationIdentificationType {
_sut.authenticationType = BITAuthenticatorAuthTypeEmailAndPassword;
//fake delegate call from the viewcontroller
[_sut didAuthenticateWithToken:@"SuperToken"];
assertThat(_sut.installationIdentificationType, equalTo(@"auid"));
}
- (void) testThatUDIDAuthSetsTheProperInstallationIdentificationType {
_sut.authenticationType = BITAuthenticatorAuthTypeUDIDProvider;
//fake delegate call from the viewcontroller
[_sut didAuthenticateWithToken:@"SuperToken"];
assertThat(_sut.installationIdentificationType, equalTo(@"udid"));
}
- (void) testThatDefaultAuthReturnsProperInstallationIdentificationType {
_sut.authenticationType = BITAuthenticatorAuthTypeEmailAndPassword;
//fake delegate call from the viewcontroller
[_sut authenticationViewControllerDidSkip:nil];
assertThat(_sut.installationIdentificationType, equalTo(@"udid"));
}
#pragma mark - validation tests
- (void) testThatValidationWithoutTokenWantsToShowTheAuthenticationViewController {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
_sut.validationType = BITAuthenticatorValidationTypeOptional;
[_sut validateInstallationWithCompletion:nil];
[verify(delegateMock) authenticator:_sut willShowAuthenticationController:(id)anything()];
}
#pragma mark - Lifetime checks
- (void) testThatValidationDoesntTriggerIfDisabled {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
_sut.validationType = BITAuthenticatorValidationTypeNever;
[_sut startManager];
[verifyCount(delegateMock, never()) authenticator:_sut willShowAuthenticationController:(id)anything()]; [verifyCount(delegateMock, never()) authenticator:_sut willShowAuthenticationController:(id)anything()];
} }
- (void) testThatValidationTriggersOnStart {
#pragma mark - Anonymous identification type
- (void) testAnonymousIdentification {
_sut.identificationType = BITAuthenticatorIdentificationTypeAnonymous;
assertThatBool(_sut.isIdentified, equalToBool(NO));
[_sut identifyWithCompletion:^(BOOL identified, NSError *error) {
assertThatBool(identified, equalToBool(YES));
assertThat(error, equalTo(nil));
}];
assertThatBool(_sut.isIdentified, equalToBool(YES));
assertThat(_sut.installationIdentifier, notNilValue());
}
//anoynmous users can't be validated
- (void) testAnonymousValidation {
_sut.identificationType = BITAuthenticatorIdentificationTypeAnonymous;
assertThatBool(_sut.isValidated, equalToBool(NO));
[_sut validateWithCompletion:^(BOOL validated, NSError *error) {
assertThatBool(_sut.validated, equalToBool(NO));
assertThat(error, notNilValue());
}];
assertThatBool(_sut.isValidated, equalToBool(NO));
}
#pragma mark - Device identification type
- (void) testDeviceIdentificationShowsViewController {
_sut.identificationType = BITAuthenticatorIdentificationTypeDevice;
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate)); id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock; _sut.delegate = delegateMock;
_sut.validationType = BITAuthenticatorValidationTypeOnFirstLaunch;
[_sut identifyWithCompletion:nil];
[verifyCount(delegateMock, times(1)) authenticator:_sut willShowAuthenticationController:(id)anything()];
}
#pragma mark - Email identification type
- (void) testEmailIdentificationFailsWithMissingSecret {
_sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppEmail;
[_sut identifyWithCompletion:^(BOOL identified, NSError *error) {
assertThatBool(identified, equalToBool(NO));
assertThat(error, notNilValue());
}];
}
- (void) testEmailIdentificationShowsViewController {
_sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppEmail;
_sut.authenticationSecret = @"mySecret";
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
[_sut identifyWithCompletion:nil];
[verifyCount(delegateMock, times(1)) authenticator:_sut willShowAuthenticationController:(id)anything()];
}
- (void) testEmailValidationFailsWithMissingSecret {
_sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppEmail;
[_sut validateWithCompletion:^(BOOL validated, NSError *error) {
assertThatBool(validated, equalToBool(NO));
assertThat(error, notNilValue());
}];
}
- (void) testThatEmailIdentificationQueuesAnOperation {
id httpClientMock = mock(BITHockeyAppClient.class);
_sut.hockeyAppClient = httpClientMock;
_sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppEmail;
[_sut storeInstallationIdentifier:@"meh" withType:BITAuthenticatorIdentificationTypeHockeyAppEmail];
_sut.authenticationSecret = @"double";
[_sut authenticationViewController:nil
handleAuthenticationWithEmail:@"stephan@dd.de"
password:@"nopass"
completion:nil];
[verify(httpClientMock) enqeueHTTPOperation:anything()];
}
#pragma mark - User identification type
- (void) testUserIdentificationShowsViewController {
_sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppUser;
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
[_sut identifyWithCompletion:nil];
[verifyCount(delegateMock, times(1)) authenticator:_sut willShowAuthenticationController:(id)anything()];
}
#pragma mark - Generic validation tests
- (void) testThatValidationFailsIfNotIdentified {
_sut.identified = NO;
_sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppUser;
[_sut validateWithCompletion:^(BOOL validated, NSError *error) {
assertThatBool(validated, equalToBool(NO));
assertThatInt(error.code, equalToInt(BITAuthenticatorNotIdentified));
}];
}
- (void) testThatValidationCreatesAGETRequest {
id httpClientMock = mock(BITHockeyAppClient.class);
_sut.hockeyAppClient = httpClientMock;
_sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppEmail;
[_sut storeInstallationIdentifier:@"meh" withType:BITAuthenticatorIdentificationTypeHockeyAppEmail];
_sut.authenticationSecret = @"double";
[_sut validateWithCompletion:nil];
[verify(httpClientMock) getPath:(id)anything()
parameters:(id)anything()
completion:(id)anything()];
}
#pragma mark - Authentication
- (void) testThatEnabledRestrictionTriggersValidation {
id clientMock = mock(BITHockeyAppClient.class);
_sut.hockeyAppClient = clientMock;
_sut.authenticationSecret = @"sekret";
_sut.restrictApplicationUsage = YES;
_sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppEmail;
[_sut storeInstallationIdentifier:@"asd" withType:BITAuthenticatorIdentificationTypeHockeyAppEmail];
[_sut authenticate];
[verify(clientMock) getPath:(id)anything() parameters:(id)anything() completion:(id)anything()];
}
- (void) testThatDisabledRestrictionDoesntTriggerValidation {
id clientMock = mock(BITHockeyAppClient.class);
_sut.hockeyAppClient = clientMock;
_sut.authenticationSecret = @"sekret";
_sut.restrictApplicationUsage = NO;
_sut.identificationType = BITAuthenticatorIdentificationTypeHockeyAppEmail;
[_sut storeInstallationIdentifier:@"asd" withType:BITAuthenticatorIdentificationTypeHockeyAppEmail];
[_sut authenticate];
[verifyCount(clientMock, never()) getPath:(id)anything() parameters:(id)anything() completion:(id)anything()];
}
#pragma mark - Lifetime checks
- (void) testThatAuthenticationTriggersOnStart {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
_sut.identificationType = BITAuthenticatorIdentificationTypeDevice;
[_sut startManager]; [_sut startManager];
@ -328,135 +304,44 @@ static void *kInstallationIdentification = &kInstallationIdentification;
- (void) testThatValidationTriggersOnDidBecomeActive { - (void) testThatValidationTriggersOnDidBecomeActive {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate)); id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock; _sut.delegate = delegateMock;
_sut.validationType = BITAuthenticatorValidationTypeOnAppActive; _sut.identificationType = BITAuthenticatorIdentificationTypeDevice;
_sut.restrictApplicationUsage = YES;
[_sut applicationDidBecomeActive:nil]; [_sut applicationDidBecomeActive:nil];
[verify(delegateMock) authenticator:_sut willShowAuthenticationController:(id)anything()]; [verify(delegateMock) authenticator:_sut willShowAuthenticationController:(id)anything()];
} }
#pragma mark - Validation helper checks
- (void) testThatValidationTriggersOnNewVersion { - (void) testThatValidationTriggersOnNewVersion {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate)); _sut.restrictApplicationUsage = YES;
_sut.delegate = delegateMock; _sut.restrictionEnforcementFrequency = BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch;
_sut.validationType = BITAuthenticatorValidationTypeOnFirstLaunch; _sut.validated = YES;
_sut.lastAuthenticatedVersion = @"111xxx"; _sut.lastAuthenticatedVersion = @"111xxx";
assertThatBool(_sut.needsValidation, equalToBool(YES));
[_sut startManager];
[verify(delegateMock) authenticator:_sut willShowAuthenticationController:(id)anything()];
} }
- (void) testThatValidationDoesNotTriggerOnSameVersion { - (void) testThatValidationDoesNotTriggerOnSameVersion {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate)); _sut.restrictApplicationUsage = YES;
_sut.delegate = delegateMock; _sut.restrictionEnforcementFrequency = BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch;
_sut.validationType = BITAuthenticatorValidationTypeOnFirstLaunch; _sut.validated = YES;
_sut.lastAuthenticatedVersion = _sut.executableUUID; _sut.lastAuthenticatedVersion = _sut.executableUUID;
assertThatBool(_sut.needsValidation, equalToBool(NO));
[_sut startManager];
[verifyCount(delegateMock, never()) authenticator:_sut willShowAuthenticationController:(id)anything()];
}
- (void) testThatFailedValidationCallsTheCompletionBlock {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
_sut.validationType = BITAuthenticatorValidationTypeOnFirstLaunch;
__block BOOL validated = YES;
__block NSError *error = nil;
tValidationCompletion completion = ^(BOOL validated_, NSError *error_) {
validated = validated_;
error = error_;
};
[_sut validateInstallationWithCompletion:completion];
[_sut validationFailedWithError:[NSError errorWithDomain:kBITAuthenticatorErrorDomain code:0 userInfo:nil]
completion:completion];
assertThatBool(validated, equalToBool(NO));
assertThat(error, notNilValue());
}
- (void) testThatFailedRequiredValidationShowsAuthenticationViewController {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
_sut.validationType = BITAuthenticatorValidationTypeOnAppActive;
[_sut validationFailedWithError:[NSError errorWithDomain:kBITAuthenticatorErrorDomain code:0 userInfo:nil]
completion:_sut.defaultValidationCompletionBlock];
[verifyCount(delegateMock, times(1)) authenticator:_sut willShowAuthenticationController:(id)anything()];
}
- (void) testThatFailedOptionalValidationDoesNotShowAuthenticationViewController {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
_sut.validationType = BITAuthenticatorValidationTypeOptional;
[_sut validationFailedWithError:nil
completion:_sut.defaultValidationCompletionBlock];
[verifyCount(delegateMock, never()) authenticator:_sut willShowAuthenticationController:(id)anything()];
}
- (void) testThatSuccessValidationCallsTheCompletionBlock {
id delegateMock = mockProtocol(@protocol(BITAuthenticatorDelegate));
_sut.delegate = delegateMock;
_sut.validationType = BITAuthenticatorValidationTypeOnFirstLaunch;
__block BOOL validated = NO;
__block NSError *error = nil;
tValidationCompletion completion = ^(BOOL validated_, NSError *error_) {
validated = validated_;
error = error_;
};
[_sut validateInstallationWithCompletion:completion];
[_sut validationSucceededWithCompletion:completion];
assertThatBool(validated, equalToBool(YES));
assertThat(error, nilValue());
}
- (void) testThatAuthTokenIsResettingWhenVendorIdentifierChanged {
MyDeviceWithIdentifierForVendor *device = [MyDeviceWithIdentifierForVendor new];
_sut.currentDevice = (id)device;
[_sut didAuthenticateWithToken:@"SuperToken"];
NSString *ident = [_sut installationIdentification];
assertThat(ident, equalTo(@"SuperToken"));
device.identifierForVendor = [NSUUID UUID];
ident = [_sut installationIdentification];
assertThat(ident, isNot(equalTo(@"SuperToken")));
} }
#pragma mark - Test installationIdentificationValidated Flag #pragma mark - Test installationIdentificationValidated Flag
- (void) testThatFlagIsResetOnFailedValidation {
_sut.validationType = BITAuthenticatorValidationTypeOnFirstLaunch;
assertThatBool(_sut.installationIdentificationValidated, equalToBool(NO));
}
- (void) testThatFlagIsSetOnFailedValidation {
_sut.validationType = BITAuthenticatorValidationTypeOnFirstLaunch;
[_sut validationSucceededWithCompletion:nil];
assertThatBool(_sut.installationIdentificationValidated, equalToBool(YES));
}
- (void) testThatApplicationBackgroundingResetsValidatedFlagInValidationTypeOnAppActive { - (void) testThatApplicationBackgroundingResetsValidatedFlagInValidationTypeOnAppActive {
_sut.validationType = BITAuthenticatorValidationTypeOnAppActive; _sut.restrictionEnforcementFrequency = BITAuthenticatorAppRestrictionEnforcementOnAppActive;
//trigger flag set to YES _sut.validated = YES;
[_sut validationSucceededWithCompletion:nil];
[_sut applicationWillResignActive:nil]; [_sut applicationWillResignActive:nil];
assertThatBool(_sut.installationIdentificationValidated, equalToBool(NO)); assertThatBool(_sut.isValidated, equalToBool(NO));
} }
- (void) testThatApplicationBackgroundingKeepValidatedFlag { - (void) testThatApplicationBackgroundingKeepValidatedFlag {
_sut.validationType = BITAuthenticatorValidationTypeOnFirstLaunch; _sut.restrictApplicationUsage = BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch;
//trigger flag set to YES _sut.validated = YES;
[_sut validationSucceededWithCompletion:nil];
[_sut applicationWillResignActive:nil]; [_sut applicationWillResignActive:nil];
assertThatBool(_sut.installationIdentificationValidated, equalToBool(YES)); assertThatBool(_sut.isValidated, equalToBool(YES));
}
- (void) testThatInitialAuthSetsValidatedFlag {
_sut.validationType = BITAuthenticatorValidationTypeOnFirstLaunch;
[_sut didAuthenticateWithToken:@"MyToken"];
assertThatBool(_sut.installationIdentificationValidated, equalToBool(YES));
} }
@end @end