Untangling Facebook Authentication on iOS

Authentication is often an afterthought when developing apps. It is a step users will need to complete just once before moving on to the actual features of your product. Hinge’s iOS client was a complicated state machine for a long time. There were a number of paths the user can take and we had zero test cases to ensure we were handling all of them. The mentality was very much, “if it ain’t broke, don’t fix it.”

Our outlook changed when Facebook announced their plans to deprecate v1.0 of the Graph API. We were going to have to update the Facebook SDK and replace many of the classes and methods throughout the app. We took this as an opportunity to revisit the architecture of our authentication process.

Facebook encourages the use of the Singleton Pattern and this had been abused in our code. Our Facebook object had a massive API which led to muddled responsibilities and hidden dependencies. It was confusing to know where in the authentication process a user was in at any given time. Lots of potential steps in the flow were overlooked.

We switched to creating a single instance of our HNGFacebookSession class in the HNGLoginViewController and passing it along through the app. We had immediate wins from this approach. Passing along the HNGFacebookSession object meant no more hidden dependencies. Dependency injection meant simpler tests. Composition left open the possibility for swapping out authentication services at runtime.

Before

FacebookService (Singleton Object)

+ (void) logout
{   
    [FBSession.activeSession closeAndClearTokenInformation];
    [[FacebookService sharedService] setUser:nil]; // clear the current user.
    
    [[SessionController defaultController].machine changeStateTo:LoggedOut];
    
    // Doubling up on screens?
    if (![SharedAppDelegate rootController].presentedViewController)
    {
        [SharedAppDelegate displayOnboarding:^{
            [SharedAppDelegate switchToTab:kHomeTab];
        }];
    }
}

SessionController (Singleton Object)

- (void) executeStep
{
        ...
        case HasAPIVersion:
            [self checkFacebookAuth];
            break;

        case NeedsFBLogin:
            [SharedAppDelegate removeSplash];
            break;
        ...
        case LoggedOut:
        {
            [self.machine changeStateTo:HasAPIVersion];
        }
        ...
    }
}

After
HNGFacebookSession

- (void)logout
{
    [[FBSession activeSession] closeAndClearTokenInformation];
}

- (void)sessionStateDidChangeWithSession:(FBSession *)session state:(FBSessionState)state error:(NSError *)error
{
    switch (state) {
    ...   
        case FBSessionStateClosed:
            [self sessionEnded];
            break;
    }
}

- (void)sessionEnded
{   
    [self.delegate sessionDidLogout:self];
}

HNGLoginViewController

- (void)sessionDidLogout:(NSObject *)session
{   
    [self.navigationController popToRootViewControllerAnimated:YES];
    // Cleanup
}

Don’t repeat yourself repeat yourself


Despite all these wins we found ourselves with a lot of repeated code. We were presenting a login modal anytime our user wasn’t authenticated. This meant every view controller theoretically needed to be able to handle an invalid token by presenting the login modal and handling the corresponding delegate callbacks.

 

Untangling_Facebook_Authentication_on_iOS



Modal Login


This approach did not make sense for us. Not only because of the tedium but because our app won’t function without a valid user. There is no point in letting the user past the login screen if they aren’t authenticated.


Root View Controller


The suggested solution was to optionally swap out the root view controller during app launch if the user wasn’t logged in. We have an asynchronous login on every app launch where we check in with our backend, so this wasn’t a solution. We weren’t big on the idea of swapping out the root view controller at runtime either.

We elected to make the login view controller the root view controller of our app. Doing so made authentication a priority by forcing the user through the authentication process on every app launch. This approach had the additional benefit of being able to pop to the root view controller instead of presenting a modal if the user ever logged out.

Untangling_Facebook_Authentication_on_iOS_(1)


Login as Root


This worked great except for having one major flaw—users saw the login view controller on every app launch. We had some business logic to check for a cached Facebook access token to automate the process but the experience was not ideal for users.

We solved the problem by subclassing the navigation controller and adding an imitation Launch Image as a property. The imitation Launch Image is the frontmost view when the app becomes responsive and gives the illusion the app is still starting. We are running our authentication process underneath and remove the curtain once we know what view controller to drop the user into.

Untangling_Facebook_Authentication_on_iOS_(2)


Login as Root w/ Curtain


Our new authentication flow gives users a consistent navigational experience. There is one way to enter the app and you have to go out the way you came in. The simplicity also allows us to deal with one centralized delegate rather than a handful of notifications strewn about. Less time digging through the app to figure out what caused a side effect means more time working on important new features.

Authentication will continue to evolve to meet the needs of our users and technical partners. Facebook released a new SDK since we broke ground on this project and, as a result, we will be revisiting this architecture shortly. We’d love to hear ways in which you’ve simplified your authentication flow so we can improve further on our next iteration.