Website Logo. Upload to /source/logo.png ; disable in /source/_includes/logo.html

Hacker School Log

Laura Skelton | Summer 2014 Batch

Hacker School Day 10: OAuth Challenges and Setting Goals

I put a ton of time in on the Secret Handshake iOS app in perfecting the OAuth login procedure. Originally, I was using an OAuth library from Google to open the Hacker School login page in a webview within the app. The problem with this was that it couldn’t take advantage of Safari’s auto fill password functionality, or if you were already logged in on the browser.

I first tried to use the iOS library to post the authentication code request to the Hacker School website on Safari. Unfortunately, an iOS app is unable to do a POST request to another app. I ended up writing a simple PHP file on my server that I redirect the user to from the Secret Handshake app. The PHP file handles sending the user to the Hacker School login page and then sending a POST request to get an access token from Hacker School. It then sends the returned token back to the Secret Handshake app via a custom URL scheme secrethandshake://oauth and an attached query string containing the token.

In the end, after struggling to find any examples of what a successful OAuth request should look like, and to find a working library I could use to get things up and running, I am not using any 3rd party libraries at all with my OAuth implementation, the code handling the OAuth requests is relatively brief, and I even managed to get token refreshing up and running to avoid repeated logins.

This is the PHP OAuth manager file in its entirety.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<?php

    /**
     * Server OAuth Handler for iOS App
     *
     * iOS App launches Safari to this page on SecretHandshake server
     * This page sends the user to the Hacker School login page to get authorization code
     * It takes the auth code and sends it back to the server to get an access token
     * It redirects the user back to the app via a custom URL scheme
     * and sends the access and refresh tokens as query parameters to the app
     * which stores them for future use.
     */

    function getAuthenticationUrl($auth_endpoint, $client_id, $redirect_uri)
    {
        $parameters = array(
                                        'response_type' => 'code',
                                        'client_id'     => $client_id,
                                        'redirect_uri'  => $redirect_uri
                                        );
        return $auth_endpoint . '?' . http_build_query($parameters, null, '&');
    }

    function getAccessToken($token_endpoint, $grant_type, array $parameters, $client_id, $client_secret)
    {
        $parameters['grant_type'] = $grant_type;
        $parameters['client_id'] = $client_id;
        $parameters['client_secret'] = $client_secret;

        $http_headers = array();

        return executeRequest($token_endpoint, $parameters, 'POST', $http_headers, 0);
    }

    function executeRequest($url, $parameters = array(), $http_method = 'POST', array $http_headers = null, $form_content_type = 0)
    {

        $postBody = http_build_query($parameters);
        $requestHttpContext["content"] = $postBody;
        $requestHttpContext["method"] = 'POST';

        $options = array( 'http' => $requestHttpContext );
        $context = stream_context_create( $options );

        $response = file_get_contents($url, false, $context);

        return $response;

    }

    $client_id = 'my-client-id';
    $client_secret = 'my-client-secret';
    $authorization_endpoint = 'my-authorization-endpoint';
    $redirect_uri = 'my-redirect-uri';
    $token_endpoint = 'my-token-endpoint';


    if (!isset($_GET['code']))
    {
        $auth_url = getAuthenticationUrl($authorization_endpoint, $client_id, $redirect_uri);
        header('Location: ' . $auth_url);
        die('Redirect');
    } else {
        $params = array(
                        'code' => $_GET['code'],
                        'redirect_uri' => $redirect_uri
                        );

        $response = getAccessToken($token_endpoint, 'authorization_code', $params, $client_id, $client_secret);

        $info = json_decode($response, true);
        $querystring = http_build_query($info);
        $appurl = 'secrethandshake://oauth' . '?' . $querystring;
        header('Location: ' . $appurl);
        die('Redirect');

    }

?>

On the iPhone, I capture the custom URL query string in the App Delegate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    // secrethandshake://oauth?access_token=324235253442

    if ([[url host] isEqualToString:@"oauth"]) {
        // parse the authentication code query
        [self authorizeFromExternalURL:url];
    }

    return YES;
}

- (void)authorizeFromExternalURL:(NSURL *)url
{
    [[OAuthHandler sharedHandler] handleAuthTokenURL:url];
}

Then I parse the query string in my OAuth Handler and store the access token and the refresh token in my user defaults.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-(void)handleAuthTokenURL:(NSURL *)url
{
    // handle query here
    NSDictionary *dict = [self parseQueryString:[url query]];

    if ([dict objectForKey:@"error"] != nil) {
        [self.delegate oauthHandlerDidFailWithError:[dict objectForKey:@"error"]];
    } else if ([dict objectForKey:@"access_token"] != nil && [dict objectForKey:@"refresh_token"] != nil) {
        // Save the access token and refresh token
        [[NSUserDefaults standardUserDefaults] setObject:[dict objectForKey:@"access_token"] forKey:kSHAccessTokenKey];
        [[NSUserDefaults standardUserDefaults] setObject:[dict objectForKey:@"refresh_token"] forKey:kSHRefreshTokenKey];

        [self.delegate oauthHandlerDidAuthorize];
    } else {
        [self.delegate oauthHandlerDidFailWithError:@"Access token not found. Failed to log in to Hacker School."];
    }

}

- (NSDictionary *)parseQueryString:(NSString *)query
{
    NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:6];
    NSArray *pairs = [query componentsSeparatedByString:@"&"];

    for (NSString *pair in pairs) {
        NSArray *elements = [pair componentsSeparatedByString:@"="];
        NSString *key = [[elements objectAtIndex:0] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        NSString *val = [[elements objectAtIndex:1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

        [dict setObject:val forKey:key];
        elements = nil;
        key = nil;
        val = nil;
    }
    pairs = nil;
    return dict;
}

To refresh the token, I just post to Hacker School’s /oauth/token page with the query string grant_type=refresh_token&refresh_token=my_refresh_token along with my client id and client secret, and store the new access token to sign my queries.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-(void)refreshToken:(id)sender
{
    NSURL *tokenURL = [NSURL URLWithString:@"https://www.hackerschool.com/oauth/token"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:tokenURL];

    [request setHTTPMethod:@"POST"];
    NSString *postString = [NSString stringWithFormat:@"grant_type=refresh_token&client_id=%@&client_secret=%@&refresh_token=%@", kMyClientID, kMyClientSecret, [[NSUserDefaults standardUserDefaults] objectForKey:kSHRefreshTokenKey]];
    [request setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];

    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {

                               NSString *responseBody = [ [NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

                               NSLog(@"response: %@", responseBody);

                               NSData *jsonData = [responseBody dataUsingEncoding:NSUTF8StringEncoding];
                               NSError *e;
                               NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:&e];

                               if (e != nil) {
                                   NSLog(@"error: %@", e);
                                   [self launchExternalSignIn:nil];
                               } else if ([jsonDict objectForKey:@"error"] != nil){
                                   NSLog(@"error refreshing token: %@", [jsonDict objectForKey:@"error"]);
                                   [self launchExternalSignIn:nil];
                               } else {
                                   [[NSUserDefaults standardUserDefaults] setObject:[jsonDict objectForKey:@"access_token"] forKey:kSHAccessTokenKey];
                                   [[NSUserDefaults standardUserDefaults] setObject:[jsonDict objectForKey:@"refresh_token"] forKey:kSHRefreshTokenKey];
                                   [self.delegate oauthHandlerDidAuthorize];
                               }

                           }];

}

It took a lot of time to get this up and running due to the lack of clear documentation on OAuth 2.0 requests, but I’m happy with how it turned out and very happy to have gotten rid of all of the external libraries. Rewriting the entire thing in a concise way let me fully understand everything that was happening, which makes the debugging process so much simpler. Hopefully this will help someone else struggling with OAuth login from an app. The source code is available on GitHub.

Testing the app was exciting. Several people loaded the app onto their phones and logged in, and we were all getting alerts with each others’ photos, which was pretty cool. The background notifications of nearby users were working perfectly. I’m looking into setting up a custom implementation of iBeacon using Bluetooth LE in background mode that will allow the phones to broadcast periodically in the background as well, so that no one needs to have the app open to be notified that someone is nearby.

I had a great conversation with Tom in the morning about what I’ve been working on at Hacker School and what I should focus on for the coming weeks to get the most out of it. I left feeling pretty good about the various directions I’ve taken and how much/what/how fast I’ve been learning. I’m happy with the balance I’ve had between iOS projects, totally new projects (Arduino and cool machine learning/neural network stuff), learning new languages (Swift!), and programming basics (getting familiar with the data structures and algorithms basics that would have been covered in CS courses had I taken that path).

I came away with some great new ideas for things that would be good and/or fun to focus on in the coming weeks and clearer goals for the rest of Hacker School. A side effect of being around so much creative energy and cool projects is that you can get overwhelmed with fear of missing out on what everyone else is doing, even if that’s not what you came here to work on. Part of that is a fun and enriching distraction, like Arduino, and part of it just ends up being noise that confuses your focus. If a bunch of people are excited about writing compilers and learning Rust, should I be doing those things too?

I have a few ideas for some quick iOS apps that I’d like to try to implement in Swift, that are more of a game/drawing style of app that use more math, graphics, and touch interface than the social utility apps I’ve written in the past, which tended to take advantage of Apple’s great built in user interface elements more than custom graphics and animations. Looking forward to the coming weeks!