oAuth2 with iOS (Part 3)

Within this post we continue working on the iOS app. For previous posts in this series, see here :).
Within this third part we will start with integrating oAuth2 into the app.

Like before, all code can be found in GIT. The code like at the end of the project can be found here.

To implement oAuth2 into our app, we are going to use a library from Toto.  So lets add this as submodule into our git repo:

MacBook-Air-van-Paul:Symfony iOS Oauth Demo paulsohier$ git submodule add https://github.com/nxtbgthng/OAuth2Client.git
Cloning into 'Symfony iOS Oauth Demo/OAuth2Client'...
remote: Reusing existing pack: 1858, done.
remote: Counting objects: 31, done.
remote: Compressing objects: 100% (25/25), done.
remote: Total 1889 (delta 16), reused 10 (delta 6)
Receiving objects: 100% (1889/1889), 463.92 KiB | 341.00 KiB/s, done.
Resolving deltas: 100% (1193/1193), done.
Checking connectivity... done.


Now we can just drag the OAuth2Client.xcodeproj into your Xcode, and it is included in the project.
Toto included some instructions in his README to build to library (We did already the first two item!):

  • Place the OAuth2Client folder within your source root
  • Drag the OAuth2Client.xcodeproj into your project
  • Under your build target, select the Build Phases tab.
    • Under Target Dependencies add OAuth2Client
    • Under Link Binary With Libraries, add libOAuth2Client.a
  • Under Build Settings,
    • Add $(SRCROOT)/path/to/OAuth2Client Header Search Paths, set as recursive
    • Add -ObjC to Other Linker Flags
  • #import "NXOAuth2.h"
  • add the Security.framework as a build dependency

Now, lets try to use the library. In AppDelegate.m we add the following method: (Yes, the clientID and secret are still wrong).

+ (void)initialize;
{
    [[NXOAuth2AccountStore sharedStore] setClientID:@"xXxXxXxXxXxX"
                                             secret:@"xXxXxXxXxXxX"
                                   authorizationURL:[NSURL URLWithString:@"http://ip-6.nl/app_dev.php/oauth/v2/auth"]
                                           tokenURL:[NSURL URLWithString:@"http://ip-6.nl/app_dev.php/oauth/v2/token"]
                                        redirectURL:[NSURL URLWithString:@"https://...your redirect URL..."]
                                     forAccountType:@"oauthDemoService"];
    NSLog(@"Init done.");
}

When you try to run this code, everything should work fine, and your app should start. You should not see anything thats changed however.

So, lets create a new client. We did this before in the second part of the android app.

ipv6:/var/ip6/SymfonyOauthDemo# php app/console oauth-example:oauth-server:client:create --redirect-uri="http://ios.local" --grant-type="authorization_code" --grant-type="password" --grant-type="refresh_token" --grant-type="token" --grant-type="client_credentials"
Added a new client with public id 5_3mtmm27tp90kosg0wgcwg4ocwg08g4g4o8coocs48g8scco4ws, secret 294jb6g99bk04oko4sss0wckockoog80o84w48ksww4cckkggs

So, now we have a client ID, lets try it out. This is very simple, by calling requestAccessToAccountWithType. Lets do this in viewDidAppear in our main view controller:

- (void)viewDidAppear:(BOOL)animated
{
    [[NXOAuth2AccountStore sharedStore] requestAccessToAccountWithType:@"oauthDemoService"];
}

Now, lets see if it works. Once we open the app, it will nicely redirects us to a browser window with our symfony2 site. If you login and click allow, it will redirect you to http://ios.local. But hey, thats not what we want, is it? We want it to redirect back to your app. While in our android app we had the view directly in the app, with iOS we switch apps. This means we need to have something to switch back to our app. Luckily this is very easy with iOS. The only thing to do is register our own URL scheme. This is done in info.plist.

  • On the top of info.plist, click the add sign. Select here that you want to define a URL types. A new list is added.
  • You see item0, expand this item.
  • This is a dictionary with item, first we need to set the URL Identifier. Set this to something unique for your app. I did choose me.sohier.siod
  • Now click on the + sign to add a new item. Choose as key for URL schemes.
  • In item 0, we assign our scheme. In my case I choose for siod.

plist.info
If you have done everything right, it should  look like on the image left.
Now, we have a different redirect URL. Due to this, we need to create a new client with the changed URL:

ipv6:/var/ip6/SymfonyOauthDemo# php app/console oauth-example:oauth-server:client:create --redirect-uri="siod://ios.local" --grant-type="authorization_code" --grant-type="password" --grant-type="refresh_token" --grant-type="token" --grant-type="client_credentials"
Added a new client with public id 6_5xqk9omvc7wg48gs8wc88wck0k8ggs44gso0404w4gcsc0088, secret 2dw92x1w4u4g8g0o8wkkokook0co84g40kcg84g4wc0ok0okw8

Notice here the URL. After that, we updated our configuration in the delegate:

+ (void)initialize;
{
    [[NXOAuth2AccountStore sharedStore] setClientID:@"6_5xqk9omvc7wg48gs8wc88wck0k8ggs44gso0404w4gcsc0088"
                                             secret:@"2dw92x1w4u4g8g0o8wkkokook0co84g40kcg84g4wc0ok0okw8"
                                   authorizationURL:[NSURL URLWithString:@"http://ip-6.nl/app_dev.php/oauth/v2/auth"]
                                           tokenURL:[NSURL URLWithString:@"http://ip-6.nl/app_dev.php/oauth/v2/token"]
                                        redirectURL:[NSURL URLWithString:@"siod://ios.local"]
                                     forAccountType:@"oauthDemoService"];
    NSLog(@"Init done.");
}

So, lets see if everything works now. When starting the app again, we are redirecting to safari. After logging in and clicking allow we are send back to the app. So it looks like it works. But do we have our user details now? We didn’t add any code for it yet, so I guess not, hmm?

When the library received a client detail back, it sends a notification, in both a error case as a success case. So lets listen to these notifications in our view controller:

    [[NSNotificationCenter defaultCenter] addObserverForName:NXOAuth2AccountStoreAccountsDidChangeNotification
                                                      object:[NXOAuth2AccountStore sharedStore]
                                                       queue:nil
                                                  usingBlock:^(NSNotification *aNotification){
                                                      // Update your UI
                                                      
                                                      NSLog(@"Received OK.");
                                                  }];
    
    [[NSNotificationCenter defaultCenter] addObserverForName:NXOAuth2AccountStoreDidFailToRequestAccessNotification
                                                      object:[NXOAuth2AccountStore sharedStore]
                                                       queue:nil
                                                  usingBlock:^(NSNotification *aNotification){
                                                      NSError *error = [aNotification.userInfo objectForKey:NXOAuth2AccountStoreErrorKey];
                                                      // Do something with the error
                                                      NSLog(@"Received ERR.");
                                                  }];

We will, for now, just do a NSLog, while testing. So, now we followed all instructions from Toto. So my assumption is that we should see a nice NSLog item. Badly enough, while testing, I didn’t get a log item at all. After some more reading, and finding a few bug reports, you needed to do another step. It is, if you read well, also noted within the instructions:

If you are using an external browser, your application needs to handle the URL you have registered as an redirect URL for the account type. The service will redirect to that URL after the authentication process.

What this means is that we need to add another method to our AppDelegate:

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url;
{
    NSLog(@"Handling");
    
    BOOL handled = [[NXOAuth2AccountStore sharedStore] handleRedirectURL:url];
    
    if (!handled) {
        NSLog(@"The URL (%@) could not be handled. Maybe you want to do something with it.", url);
    }
    else
    {
        NSLog(@"Handled");
    }
    
    return handled;
}

This will handle the case when the app is opened by a URL. So lets give it another test. Now, after we are logged in, we should see several log items, with the last one being “Received OK.”. With my test, this was indeed the case:

2014-06-22 13:44:47.528 Symfony iOS Oauth Demo[665:346183] Handling
2014-06-22 13:44:47.539 Symfony iOS Oauth Demo[665:346183] Handled
2014-06-22 13:44:47.682 Symfony iOS Oauth Demo[665:346292] -[NXOAuth2PostBodyStream open] Stream has been reopened after close
2014-06-22 13:44:47.873 Symfony iOS Oauth Demo[665:346183] Received OK.

So, it looks like everything works. Now lets modify our code a bit. If we call multiple times the requestAccessToAccountWithType method, we will end up with several requests. Thats not something we want. We want to make sure we only have one, and don’t call it when we already have a account.

Lets add a second property to the interface:

@property (nonatomic, strong) NXOAuth2Account *account;

Now, lets modify our viewDidAppear:

- (void)viewDidAppear:(BOOL)animated
{
    for (NXOAuth2Account *acc in [[NXOAuth2AccountStore sharedStore] accountsWithAccountType:@"oauthDemoService"]) {
        self.account = acc;
        NSLog(@"Found a account");
        //[[NXOAuth2AccountStore sharedStore] removeAccount:acc];
        break;
    };

    if (!self.account)
    {
        [[NXOAuth2AccountStore sharedStore] requestAccessToAccountWithType:@"oauthDemoService"];
        
        [[NSNotificationCenter defaultCenter] addObserverForName:NXOAuth2AccountStoreAccountsDidChangeNotification
                                                          object:[NXOAuth2AccountStore sharedStore]
                                                           queue:nil
                                                      usingBlock:^(NSNotification *aNotification){
                                                          NSLog(@"Received OK.");
                                                          
                                                          id acc = [aNotification.userInfo objectForKey:@"NXOAuth2AccountStoreNewAccountUserInfoKey"];
                                                          if ([acc isKindOfClass:[NXOAuth2Account class]])
                                                          {
                                                              self.account = (NXOAuth2Account *) acc;
                                                              NSLog(@"Name: %@", self.account.accountType);
                                                          }
                                                          else
                                                          {
                                                              NSLog(@"Weird item");
                                                          }
                                                      }];
        
        [[NSNotificationCenter defaultCenter] addObserverForName:NXOAuth2AccountStoreDidFailToRequestAccessNotification
                                                          object:[NXOAuth2AccountStore sharedStore]
                                                           queue:nil
                                                      usingBlock:^(NSNotification *aNotification){
                                                          NSError *error = [aNotification.userInfo objectForKey:NXOAuth2AccountStoreErrorKey];
                                                          // Do something with the error
                                                          NSLog(@"Received ERR.");
                                                      }];
    }
}

So, when we find a account for our type, we assign it to our new property. If there is no account yet, we start with asking for one, and then assign it.

Before, our full UI was available while there was no account. So, when there is no account, we want to disable the add button. We also don’t want to initialise the fetch request yet.

First of, we need to modify the storyboard to disable to Add button by default. This can be simply be done by setting enabled to false. After that we need a outlet to the button, so we can enable it later on. This can be done by ctrl dragging into the code.

Now, if we start the app, you will notice the button is greyed out. So lets fix that. We need to modify both the setter for the managedContext, as for the account.
So, lets replace the setter for the managedObjectContext with this new code:

- (void)setManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
    
    _managedObjectContext = managedObjectContext;
    
    [self updateUI];
}
- (void)setAccount:(NXOAuth2Account *)account
{
    _account = account;
    
    [self updateUI];
}

- (void)updateUI
{
    if (self.managedObjectContext && self.account)
    {
        self.debug = YES;
        
        
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Demo"];
        request.predicate = nil;
        request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"title"
                                                                  ascending:YES
                                                                   selector:@selector(localizedStandardCompare:)]];
        
        self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                                            managedObjectContext:self.managedObjectContext
                                                                              sectionNameKeyPath:nil
                                                                                       cacheName:nil];
        
        self.addButton.enabled = YES;
    }
}

As you can see, I moved the initialisation of the fetchResultsController to the new method updateUI. This runs only when both the context as the account is available. It also enabled the add button.

Lastly, for this part of the series, we are going to update the interface to the view controller for the create method. We want the account here as well, so lets add it to there interface in CreateDemoViewController.h:

@property (nonatomic, strong) NXOAuth2Account *account;

And assign it in prepareForSegue:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"create-new-demo"])
    {
        CreateDemoViewController *vw = (CreateDemoViewController *) segue.destinationViewController;
        vw.managedObjectContext = self.managedObjectContext;
        vw.account = self.account;
    }
}

Now the app has fully support for oAuth2, the user account is saved locally, and we can continue with actually getting data from the server. But, thats for the next part in this series.

2 thoughts on “oAuth2 with iOS (Part 3)

  1. Great series, but I noticed that the part 3 link for the GIT repository is not working, there doesn’t seem to be a part 3 release.

Leave a Reply

Your email address will not be published. Required fields are marked *