Posts Tagged ‘mSSO’

Nov11

A Single Sign-on Pattern for Enterprise iOS Applications

In conversations with clients, we continue to hear how important single sign-on is to their enterprise mobile application strategy. According to a study earlier this year by Kelton Research 250 IT Managers, 21% of respondents indicated that they plan to deploy 20, or more, enterprise applications to their organization this year.

In order for enterprises to realize the productivity gains factored into the investment decisions for those applications, IT Managers must explore options for enabling single sign-on for their users. In this post, we’ll outline a mobile single sign-on (mSSO) pattern for enterprise iOS applications.

Assumptions

  1. Your company has a centralized single sign-on service exposed on the network – LDAP, Novell Access Manager, etc. This approach will work with a number of identity management architectures, but you will need to tailor credential management accordingly. For this demo, our service calls are simulated with hardcoded tokens.
  2. Your company has a standardized credential timeout period or a way of dynamically distributing that value to applications. For this demo, we’ll use a static 30 minute timeout period.
  3. You are able to sign and distribute builds to a device. This is important because keychain activities do not function in the simulator.

Implementation

Approach

Our approach will consist of creating three applications – two mock business applications and one logout application. Each application may contain its own logout functionality, but a single logout application makes the user’s activity of killing a session intuitive. In practice, the mSSO pattern would be implemented as a stand-alone library which could be easily dropped into each of your various enterprise applications.

The business applications retrieve a unique authentication token which is included with each service request. This token will be stored in the device’s keychain, which will be shared across our suite of applications. We will also maintain an ‘expiration date’ within the keychain so that subsequent requests from our various apps can (if required) preemptively prompt the user to authenticate.

Authentication for this demo may be simplistic, but existing patterns cover implementing authentication in a service-based mobile application. For example, the ‘store credentials’ logic presented in this example would live somewhere in that pattern’s “authenticateOperation”.

One possible extension of the mSSO pattern is to confirm that your credentials have not expired before issuing a call to the service. You would use the mSSO pattern to sign your network requests, but rely on the service and response handlers to inform you that credentials have expired. Checking credential expiration prior to making a service call limits unnecessary network traffic.

Configuration

There are two key steps that must be completed in order for your applications to share keychain access.

  1. Each application included in your mSSO effort must share a Bundle Seed ID, which allows shared keychain access between our suite of applications. This is configured within the iOS management portal. We’ll configure our applications using the default ‘Team ID’ selection.

  2. We also need to enable and add an entitlements file that specifies that this application should be able to access the shared keychain. This step is done after the Xcode project has been created.
    • Select your app target in Xcode and choose the ‘Summary’ tab.
    • Choose ‘Enable Entitlements’ at the bottom.
    • Set the Entitlements File name to “mSSO” and hit return. Select ‘Create’ when prompted.
    • Unless needed for your application, remove iCloud configuration settings.
    • Add a keychain value titled “mSSO”, our Bundle Seed ID is prepended to this value for us.

Development

We’ll walk through how to create App1 in detail, and then let you work through App2 and the Logout application. Let’s start by opening Xcode and creating a single view application. Before we get started, add the Security.framework and create your entitlement files as outlined above. Here is a good primer for interacting with the keychain.

  1. Add the custom mSSOUtils and DateUtils classes as outlined below. Make sure that you import accordingly. Due to changes in iOS related to ARC (developer account required), you will need to disable ARC for the mSSOUtils class. You can do so by selecting your target in Xcode and viewing the Build Phases tab. Expand the Compile Sources section and double click the mSSOUtils class to add the -fno-objc-arc compiler flag. You may need to clean and build.
    mSSOUtils:

    #define kmSSOKeychainGroup @"3Q4M6DQ9WM.mSSO"
    #define kAuthenticationServiceName @"com.captechconsulting.msso"
    #define kCredentialToken @"mSSOAuthenticationToken"
    #define kCredentialExpiration @"mSSOCredentialsExpirationDate"
    #define kExpirationTimeout 60.0 * 30    // 30 minute timeout
     
    // *** PRIVATE METHODS DEF *** //
    @interface mSSOUtils (Private)
    + (NSMutableDictionary *) keychainSearch:(NSString *)identifier;
    + (NSString *) getValueForIdentifier:(NSString *)identifer;
    + (BOOL) setValue:(NSString *)value forIdentifier:(NSString *)identifier;
    + (void) deleteValueForIdentifier:(NSString *)identifier;
    @end
     
    @implementation mSSOUtils:
     
    + (BOOL) authenticateWithUsername:(NSString *)username andPassword:(NSString *)password {
        // for testing purposes, each call to this method authenticates successfully
     
        // set the token - app specific - change this in your App2 implementation
        if ([self setValue:@"TokenSetFromApp1" forIdentifier:kCredentialToken]) {
            // token set, now set the credential expiration
            [self extendCredentials];
        } else {
            NSLog(@"Unable to set token.");
        }
     
        return YES;
    }
     
    + (void) logout {
        // destroy token AND expiration date
        [self deleteValueForIdentifier:kCredentialToken];
        [self deleteValueForIdentifier:kCredentialExpiration];
    }
     
    // credential management
    + (void) extendCredentials {
        NSDate *newExpireDate = [DateUtils dateWithTimeout:kExpirationTimeout];
        NSString *newExpireString = [DateUtils stringFromDate:newExpireDate withFormat:kDateFormat];
        BOOL success = [self setValue:newExpireString forIdentifier:kCredentialExpiration];
        if (!success) {
            NSLog(@"Unable to extend credentials.");
        }
    }
     
    + (BOOL) credentialsExpired {
     
        // if no token exists, call credentials expired
        if ([self credentialToken] == nil) {
            return YES;
        }
     
        NSDate *expirationDate = [self credentialExpirationDate];
        if (expirationDate) {
            // check for expiration
            return [DateUtils dateInPast:expirationDate];
        }
     
        // if there is no expiration date, default to 'expired'
        return YES;
    }
     
     
    // sign the request with current credentials - we'll add the token as an HTTP header field
    + (NSMutableURLRequest *) signRequest:(NSMutableURLRequest *)request {
        NSString *token = [self credentialToken];
        if (token) {
            [request addValue:token forHTTPHeaderField:@"auth-token"];
        }
        return request;
    }
     
    // methods to retrieve credential information
    + (NSString *) credentialToken {
        NSString *token = [self getValueForIdentifier:kCredentialToken];
        return token;
    }
     
    + (NSDate *) credentialExpirationDate {
        NSString *expirationDateString = [self getValueForIdentifier:kCredentialExpiration];
        if (expirationDateString) {
            // convert to date
            return [DateUtils dateFromString:expirationDateString withFormat:kDateFormat];
        }
        return nil;
    }
     
    + (void) displayAuthenticateView:(UIViewController *)vc {
        authenticateViewController *authenticateView = [[authenticateViewController alloc] initWithNibName:@"authenticateViewController" bundle:nil];
        UINavigationController *nc = [[UINavigationController alloc] initWithRootViewController:authenticateView];
        [vc presentModalViewController:nc animated:YES];
    }
     
    #pragma mark -
    #pragma mark PRIVATE METHODS
    + (NSMutableDictionary *) keychainSearch:(NSString *)identifier {
        NSMutableDictionary *keychainSearch = [[[NSMutableDictionary alloc] init] autorelease];
     
        [keychainSearch setObject:kmSSOKeychainGroup forKey:(id)kSecAttrAccessGroup];   // inform the search that we're using the shared keychain
        [keychainSearch setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];   // set the type to generic password - other options are certification, internet password, etc
     
        NSData *encodedIdentifier = [identifier dataUsingEncoding:NSUTF8StringEncoding];
        [keychainSearch setObject:encodedIdentifier forKey:(id)kSecAttrGeneric];
        [keychainSearch setObject:encodedIdentifier forKey:(id)kSecAttrAccount];
        [keychainSearch setObject:kAuthenticationServiceName forKey:(id)kSecAttrService];
     
        return keychainSearch;
    }
     
    + (NSString *) getValueForIdentifier:(NSString *)identifier {
        NSMutableDictionary *search = [self keychainSearch:identifier];
     
        [search setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit]; // limit it to the first result
        [search setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];    // return data vs a dictionary of attributes
     
        NSData *value = nil;
        OSStatus status = SecItemCopyMatching((CFDictionaryRef)search,
                                              (CFTypeRef *)&value);
     
        if (status == noErr) {
            return [NSString stringWithUTF8String:[value bytes]];;
        }
     
        return nil;
    }
     
    + (BOOL) setValue:(NSString *)value forIdentifier:(NSString *)identifier {
     
        // check if value exists
        NSString *existingValue = [self getValueForIdentifier:identifier];
        if (existingValue) {
     
            if (![existingValue isEqualToString:value]) {
                // update value
                NSMutableDictionary *search = [self keychainSearch:identifier];
     
                NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding];
                NSMutableDictionary *update = [NSMutableDictionary dictionaryWithObjectsAndKeys:valueData, (id)kSecValueData, nil];
     
                OSStatus status = SecItemUpdate((CFDictionaryRef)search,
                                                (CFDictionaryRef)update);
     
                if (status == errSecSuccess) {
                    return YES;
                }
                return NO;
            }
     
        } else {
     
            // create new entry
            NSMutableDictionary *add = [self keychainSearch:identifier];
     
            NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding];
            [add setObject:valueData forKey:(id)kSecValueData];
     
            OSStatus status = SecItemAdd((CFDictionaryRef)add,NULL);
     
            if (status == errSecSuccess) {
                return YES;
            }
            return NO;
     
        }
     
        return YES;
    }
     
    + (void) deleteValueForIdentifier:(NSString *)identifier {
        NSMutableDictionary *search = [self keychainSearch:identifier];
        SecItemDelete((CFDictionaryRef)search);
    }

    DateUtils:

    + (NSDate *) dateFromString:(NSString *)string withFormat:(NSString *)format {
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    	[dateFormatter setDateFormat:format];
    	NSDate *date = [dateFormatter dateFromString:string];
    	return date;
    }
     
    + (NSString *) stringFromDate:(NSDate *)date withFormat:(NSString *)format; {
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    	[dateFormatter setDateFormat:format];
    	NSString *dateString = [dateFormatter stringFromDate:date];
        return dateString;
    }
     
    + (NSDate *) dateWithTimeout:(NSTimeInterval)timeout {
        NSDate *now = [NSDate date];
        NSDate *timeoutDate = [now dateByAddingTimeInterval:timeout];
        return timeoutDate;
    }
     
    + (BOOL) dateInPast:(NSDate *)date {
        if ([date compare:[NSDate date]] == NSOrderedAscending) {
            return YES;
        }
        return NO;
    }
  2. Within the generated ViewController, add two UILabel outlets/properties – token and expiration date – and a “Logout” button. You’ll need to add two custom methods: logout and a selector to handle the foreground notification we register to receive.
    - (IBAction) logout:(id)sender {
        [mSSOUtils logout];
        [mSSOUtils displayAuthenticateView:self];
    }
     
    - (void) enterForeground:(id)sender {
        // reset labels as we've entered the foreground
        self.token.text = [mSSOUtils credentialToken];
        self.expiration.text = [DateUtils stringFromDate:[mSSOUtils credentialExpirationDate] withFormat:kDateFormat];
    }
  3. You should only need to update two view lifecycle methods within ViewControllerviewDidLoad and viewWillAppear. Within viewDidLoad, we register to receive a notification when the app is brought to the foreground that triggers our UI updates. The additions to viewWillAppear simply update our labels if there is data in the keychain.
    - (void)viewDidLoad {
        [super viewDidLoad];
        // register for enter foreground notification to update labels
        [[NSNotificationCenter defaultCenter] addObserver:self 
                                                 selector:@selector(enterForeground:)
                                                     name:UIApplicationWillEnterForegroundNotification
                                                   object:nil];
    }
    - (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];
     
        self.token.text = [mSSOUtils credentialToken];
        self.expiration.text = [DateUtils stringFromDate:[mSSOUtils credentialExpirationDate] withFormat:kDateFormat];
    }
  4. Add an authenticateViewController to your project. This will be displayed modally when the users credentials need to be re-challenged. This example simply has a login button, but this is where you would include typical login fields.
    - (IBAction) authenticate:(id)sender {
        // if authentication is successful, dismiss the view
        if ([mSSOUtils authenticateWithUsername:@"username" andPassword:@"hashedPassword"]) {
            [self dismissModalViewControllerAnimated:YES];
        }   
    }
  5. Last up, we’ll need to update our application delegate to confirm our credentials are valid when the app launches or is brought back from the background. You’ll need to update the didFinishLaunchingWithOptions and applicationWillEnterForeground methods as noted below:
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
        // Override point for customization after application launch.
        self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
        self.window.rootViewController = self.viewController;
        [self.window makeKeyAndVisible];
     
        // credentials don't exist or are expired - display the authenticate view
        if ([mSSOUtils credentialsExpired]) {
            [mSSOUtils displayAuthenticateView:self.viewController];
        }
     
        return YES;
    }
    - (void)applicationWillEnterForeground:(UIApplication *)application {
        // credentials don't exist or are expired - display the authenticate view
        if ([mSSOUtils credentialsExpired]) {
            [mSSOUtils displayAuthenticateView:self.viewController];
        }
    }
  6. Now, rinse and repeat for App2. You can follow the same steps for the Logout application, but you really just need a single view that calls [mSSOUtils logout] on launch and informs the user their credentials have been terminated.
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
        // Override point for customization after application launch.
        self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
        self.window.rootViewController = self.viewController;
        [self.window makeKeyAndVisible];
     
        // logout to kill credentials
        [mSSOUtils logout];
     
        return YES;
    }
    - (void)applicationWillEnterForeground:(UIApplication *)application {   
        // logout to kill credentials
        [mSSOUtils logout];
    }

Here are a couple helpful hints while developing your solution:

  • We’ve disabled ARC for the mSSOUtils class, which means you need to handle memory management yourself.
  • When building your application, if you get a Mach-O Linker error, ensure that you’ve added the Security.framework.

Testing

Our testing won’t get too crazy, but at this point you should be all set to install our suite of apps on your device.

We’ll start by opening App1 and simulating an authentication call. Once the modal view is dismissed, our token and expiration date should be updated – expiration being now + 30 minutes. From here, jump to App2 where you should see the App1 token/expiration. Logout and re-authenticate within App2, which will update our token and expiration date. Now, we’ll wait 30 minutes and test whether our token expires. After 30 minutes, open App1, you should be prompted with an authentication view. Authenticate and then open the Logout app. Enter App2 from the multi-task tray, you should be prompted to authenticate once again.

The test sequence above has been captured in the screenshots below. Note: for brevity, I’ve excluded screenshots of each authentication view except for the final step.

Closing

This post presents a pattern for implementing single sign-on for your enterprise iOS applications. It should give you the foundation needed to begin implementing single sign-on in your applications. I’ve attached the source for App1. If you’ve got questions or are interested in additional source files, I’m @nathanhjones on Twitter.

Project files: mssoapp1.zip
Did you like this? Share it: