Wednesday, October 1, 2008

Netflix API - Netflix Specific OAuth iPhone Code - Part 4

I have already discussed how to get OAuth to build for an iPhone in Part 2. To use OAuth to call Netflix there are two small changes needed. The first one is that the way that characters are escaped in URLs needs to be tightened up a bit, otherwise the signature strings will work some of the time and fail when they happen to contain the wrong character sequence. This took a while to figure out...

The file NSString+URLEncoding.m needs to have a few characters (space, plus and asterisk) added as shown below:

Note, I'm having a hard time getting code to look good here, its hard to find a way to render code in a narrow column that doesn't mess up the formatting and works in more than one browser using blogger.com tools and templates.

- (NSString *)encodedURLString {
NSString *result = (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
(CFStringRef)self,
NULL,
CFSTR("?=& +*"), // legal URL characters to be escaped
kCFStringEncodingUTF8); // encoding
return result;
}

- (NSString *)encodedURLParameterString {
NSString *result = (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
(CFStringRef)self,
NULL,
CFSTR(":/= +*"),
kCFStringEncodingUTF8);
return result;
}


The second thing is that when the authentication is complete, Netflix returns a token that contains an encoded user identifier, as well as a secret and a key. The standard OAuth code only expects the secret and key, so a new NetflixToken class was created to hold the extra information, and to persist it in the iPhone's defaults store along with the secret and key. This means that once the user has signed into OAuth once, this information is saved and they never have to sign in again unless either Netflix or the User revokes the token. One more method was added to remove an entry from the defaults store, for use during logout.

First the NetflixToken.h header file:

//
// NetflixToken.h
// Instant Test
//
// Created by Adrian Cockcroft on 9/11/08.
//

#import
#import "OAToken.h"

@interface NetflixToken : NSObject {
@protected
NSString *key;
NSString *secret;
NSString *user;
}
@property(copy, readwrite) NSString *key;
@property(copy, readwrite) NSString *secret;
@property(copy, readwrite) NSString *user;

- (id)initWithKey:(NSString *)aKey secret:(NSString *)aSecret user:(NSString *)aUser;
- (id)initWithUserDefaultsUsingServiceProviderName:(NSString *)provider prefix:(NSString *)prefix;
- (id)initWithHTTPResponseBody:(NSString *)body;
- (int)storeInUserDefaultsWithServiceProviderName:(NSString *)provider prefix:(NSString *)prefix;
- (int)removeFromUserDefaultsWithServiceProviderName:(NSString *)provider prefix:(NSString *)prefix;
- (OAToken *)oaToken;

@end


Then the code itself, this is all based on a simple extension of OAToken, which is part of the OAuth code base mentioned in part 2.


//
// NetflixToken.m
// Instant Test
//
// Created by Adrian Cockcroft on 9/11/08.
//

#import "NetflixToken.h"

@implementation NetflixToken

@synthesize key, secret, user;

#pragma mark init

- (id)init {
[super init];
self.key = @"";
self.secret = @"";
self.user = @"";
return self;
}

- (id)initWithKey:(NSString *)aKey secret:(NSString *)aSecret user:(NSString *)aUser {
[super init];
self.key = aKey;
self.secret = aSecret;
self.user = aUser;
return self;
}

- (OAToken *)oaToken {
return [[OAToken alloc] initWithKey:self.key secret:self.secret];
}

- (id)initWithHTTPResponseBody:(NSString *)body {
[super init];
NSArray *pairs = [body componentsSeparatedByString:@"&"];

for (NSString *pair in pairs) {
NSArray *elements = [pair componentsSeparatedByString:@"="];
if ([[elements objectAtIndex:0] isEqualToString:@"oauth_token"]) {
self.key = [elements objectAtIndex:1];
} else if ([[elements objectAtIndex:0] isEqualToString:@"oauth_token_secret"]) {
self.secret = [elements objectAtIndex:1];
} else if ([[elements objectAtIndex:0] isEqualToString:@"user_id"]) {
self.user = [elements objectAtIndex:1];
}
}

return self;
}


- (id)initWithUserDefaultsUsingServiceProviderName:(NSString *)provider prefix:(NSString *)prefix
{
[super init];
NSString *theKey = [[NSUserDefaults standardUserDefaults] stringForKey:[NSString stringWithFormat:@"OAUTH_%@_%@_KEY", prefix, provider]];
NSString *theSecret = [[NSUserDefaults standardUserDefaults] stringForKey:[NSString stringWithFormat:@"OAUTH_%@_%@_SECRET", prefix, provider]];
NSString *theUser = [[NSUserDefaults standardUserDefaults] stringForKey:[NSString stringWithFormat:@"NETFLIX_%@_%@_USER", prefix, provider]];
if (theKey == NULL || theSecret == NULL)
return(nil);
self.key = theKey;
self.secret = theSecret;
self.user = theUser;
return(self);
}


- (int)storeInUserDefaultsWithServiceProviderName:(NSString *)provider prefix:(NSString *)prefix
{
[[NSUserDefaults standardUserDefaults] setObject:self.key forKey:[NSString stringWithFormat:@"OAUTH_%@_%@_KEY", prefix, provider]];
[[NSUserDefaults standardUserDefaults] setObject:self.secret forKey:[NSString stringWithFormat:@"OAUTH_%@_%@_SECRET", prefix, provider]];
[[NSUserDefaults standardUserDefaults] setObject:self.user forKey:[NSString stringWithFormat:@"NETFLIX_%@_%@_USER", prefix, provider]];
[[NSUserDefaults standardUserDefaults] synchronize];
return(0);
}

- (int)removeFromUserDefaultsWithServiceProviderName:(NSString *)provider prefix:(NSString *)prefix
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:[NSString stringWithFormat:@"OAUTH_%@_%@_KEY", prefix, provider]];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:[NSString stringWithFormat:@"OAUTH_%@_%@_SECRET", prefix, provider]];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:[NSString stringWithFormat:@"NETFLIX_%@_%@_USER", prefix, provider]];
[[NSUserDefaults standardUserDefaults] synchronize];
return(0);
}
@end

1 comment:

Phil said...

It looks like the first "import" in NetflixToken.h is extraneous.