739 lines
26 KiB
Objective-C
Executable File
739 lines
26 KiB
Objective-C
Executable File
/* Copyright (c) 2011 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|
|
|
|
#if !TARGET_OS_IPHONE
|
|
|
|
#import "GTMOAuth2WindowController.h"
|
|
|
|
@interface GTMOAuth2WindowController ()
|
|
@property (nonatomic, retain) GTMOAuth2SignIn *signIn;
|
|
@property (nonatomic, copy) NSURLRequest *initialRequest;
|
|
@property (nonatomic, retain) GTMCookieStorage *cookieStorage;
|
|
@property (nonatomic, retain) NSWindow *sheetModalForWindow;
|
|
|
|
- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil;
|
|
- (void)setupSheetTerminationHandling;
|
|
- (void)destroyWindow;
|
|
- (void)handlePrematureWindowClose;
|
|
- (BOOL)shouldUseKeychain;
|
|
- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request;
|
|
- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error;
|
|
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
|
|
|
|
- (void)handleCookiesForResponse:(NSURLResponse *)response;
|
|
- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request;
|
|
@end
|
|
|
|
const char *kKeychainAccountName = "OAuth";
|
|
|
|
@implementation GTMOAuth2WindowController
|
|
|
|
// IBOutlets
|
|
@synthesize keychainCheckbox = keychainCheckbox_,
|
|
webView = webView_,
|
|
webCloseButton = webCloseButton_,
|
|
webBackButton = webBackButton_;
|
|
|
|
// regular ivars
|
|
@synthesize signIn = signIn_,
|
|
initialRequest = initialRequest_,
|
|
cookieStorage = cookieStorage_,
|
|
sheetModalForWindow = sheetModalForWindow_,
|
|
keychainItemName = keychainItemName_,
|
|
initialHTMLString = initialHTMLString_,
|
|
shouldAllowApplicationTermination = shouldAllowApplicationTermination_,
|
|
externalRequestSelector = externalRequestSelector_,
|
|
shouldPersistUser = shouldPersistUser_,
|
|
userData = userData_,
|
|
properties = properties_;
|
|
|
|
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
|
// Create a controller for authenticating to Google services
|
|
+ (id)controllerWithScope:(NSString *)scope
|
|
clientID:(NSString *)clientID
|
|
clientSecret:(NSString *)clientSecret
|
|
keychainItemName:(NSString *)keychainItemName
|
|
resourceBundle:(NSBundle *)bundle {
|
|
return [[[self alloc] initWithScope:scope
|
|
clientID:clientID
|
|
clientSecret:clientSecret
|
|
keychainItemName:keychainItemName
|
|
resourceBundle:bundle] autorelease];
|
|
}
|
|
|
|
- (id)initWithScope:(NSString *)scope
|
|
clientID:(NSString *)clientID
|
|
clientSecret:(NSString *)clientSecret
|
|
keychainItemName:(NSString *)keychainItemName
|
|
resourceBundle:(NSBundle *)bundle {
|
|
Class signInClass = [[self class] signInClass];
|
|
GTMOAuth2Authentication *auth;
|
|
auth = [signInClass standardGoogleAuthenticationForScope:scope
|
|
clientID:clientID
|
|
clientSecret:clientSecret];
|
|
NSURL *authorizationURL = [signInClass googleAuthorizationURL];
|
|
return [self initWithAuthentication:auth
|
|
authorizationURL:authorizationURL
|
|
keychainItemName:keychainItemName
|
|
resourceBundle:bundle];
|
|
}
|
|
#endif
|
|
|
|
// Create a controller for authenticating to any service
|
|
+ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
|
|
authorizationURL:(NSURL *)authorizationURL
|
|
keychainItemName:(NSString *)keychainItemName
|
|
resourceBundle:(NSBundle *)bundle {
|
|
return [[[self alloc] initWithAuthentication:auth
|
|
authorizationURL:authorizationURL
|
|
keychainItemName:keychainItemName
|
|
resourceBundle:bundle] autorelease];
|
|
}
|
|
|
|
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
|
|
authorizationURL:(NSURL *)authorizationURL
|
|
keychainItemName:(NSString *)keychainItemName
|
|
resourceBundle:(NSBundle *)bundle {
|
|
if (bundle == nil) {
|
|
bundle = [NSBundle mainBundle];
|
|
}
|
|
|
|
NSString *nibName = [[self class] authNibName];
|
|
NSString *nibPath = [bundle pathForResource:nibName
|
|
ofType:@"nib"];
|
|
self = [super initWithWindowNibPath:nibPath
|
|
owner:self];
|
|
if (self != nil) {
|
|
// use the supplied auth and OAuth endpoint URLs
|
|
Class signInClass = [[self class] signInClass];
|
|
signIn_ = [[signInClass alloc] initWithAuthentication:auth
|
|
authorizationURL:authorizationURL
|
|
delegate:self
|
|
webRequestSelector:@selector(signIn:displayRequest:)
|
|
finishedSelector:@selector(signIn:finishedWithAuth:error:)];
|
|
keychainItemName_ = [keychainItemName copy];
|
|
|
|
// create local, temporary storage for WebKit cookies
|
|
cookieStorage_ = [[GTMCookieStorage alloc] init];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[signIn_ release];
|
|
[initialRequest_ release];
|
|
[cookieStorage_ release];
|
|
[delegate_ release];
|
|
#if NS_BLOCKS_AVAILABLE
|
|
[completionBlock_ release];
|
|
#endif
|
|
[sheetModalForWindow_ release];
|
|
[keychainItemName_ release];
|
|
[initialHTMLString_ release];
|
|
[userData_ release];
|
|
[properties_ release];
|
|
|
|
[super dealloc];
|
|
}
|
|
|
|
- (void)awakeFromNib {
|
|
// load the requested initial sign-in page
|
|
[self.webView setResourceLoadDelegate:self];
|
|
[self.webView setPolicyDelegate:self];
|
|
|
|
// the app may prefer some html other than blank white to be displayed
|
|
// before the sign-in web page loads
|
|
NSString *html = self.initialHTMLString;
|
|
if ([html length] > 0) {
|
|
[[self.webView mainFrame] loadHTMLString:html baseURL:nil];
|
|
}
|
|
|
|
// hide the keychain checkbox if we're not supporting keychain
|
|
BOOL hideKeychainCheckbox = ![self shouldUseKeychain];
|
|
|
|
const NSTimeInterval kJanuary2011 = 1293840000;
|
|
BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011);
|
|
if (isDateValid) {
|
|
// start the asynchronous load of the sign-in web page
|
|
[[self.webView mainFrame] performSelector:@selector(loadRequest:)
|
|
withObject:self.initialRequest
|
|
afterDelay:0.01
|
|
inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
|
|
} else {
|
|
// clock date is invalid, so signing in would fail with an unhelpful error
|
|
// from the server. Warn the user in an html string showing a watch icon,
|
|
// question mark, and the system date and time. Hopefully this will clue
|
|
// in brighter users, or at least let them make a useful screenshot to show
|
|
// to developers.
|
|
//
|
|
// Even better is for apps to check the system clock and show some more
|
|
// helpful, localized instructions for users; this is really a fallback.
|
|
NSString *htmlTemplate = @"<html><body><div align=center><font size='7'>"
|
|
@"⌚ ?<br><i>System Clock Incorrect</i><br>%@"
|
|
@"</font></div></body></html>";
|
|
NSString *errHTML = [NSString stringWithFormat:htmlTemplate, [NSDate date]];
|
|
|
|
[[webView_ mainFrame] loadHTMLString:errHTML baseURL:nil];
|
|
hideKeychainCheckbox = YES;
|
|
}
|
|
|
|
#if DEBUG
|
|
// Verify that Javascript is enabled
|
|
BOOL hasJS = [[webView_ preferences] isJavaScriptEnabled];
|
|
NSAssert(hasJS, @"GTMOAuth2: Javascript is required");
|
|
#endif
|
|
|
|
[keychainCheckbox_ setHidden:hideKeychainCheckbox];
|
|
}
|
|
|
|
+ (NSString *)authNibName {
|
|
// subclasses may override this to specify a custom nib name
|
|
return @"GTMOAuth2Window";
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
|
|
delegate:(id)delegate
|
|
finishedSelector:(SEL)finishedSelector {
|
|
// check the selector on debug builds
|
|
GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector,
|
|
@encode(GTMOAuth2WindowController *), @encode(GTMOAuth2Authentication *),
|
|
@encode(NSError *), 0);
|
|
|
|
delegate_ = [delegate retain];
|
|
finishedSelector_ = finishedSelector;
|
|
|
|
[self signInCommonForWindow:parentWindowOrNil];
|
|
}
|
|
|
|
#if NS_BLOCKS_AVAILABLE
|
|
- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
|
|
completionHandler:(void (^)(GTMOAuth2Authentication *, NSError *))handler {
|
|
completionBlock_ = [handler copy];
|
|
|
|
[self signInCommonForWindow:parentWindowOrNil];
|
|
}
|
|
#endif
|
|
|
|
- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil {
|
|
self.sheetModalForWindow = parentWindowOrNil;
|
|
hasDoneFinalRedirect_ = NO;
|
|
hasCalledFinished_ = NO;
|
|
|
|
[self.signIn startSigningIn];
|
|
}
|
|
|
|
- (void)cancelSigningIn {
|
|
// The user has explicitly asked us to cancel signing in
|
|
// (so no further callback is required)
|
|
hasCalledFinished_ = YES;
|
|
|
|
[delegate_ autorelease];
|
|
delegate_ = nil;
|
|
|
|
#if NS_BLOCKS_AVAILABLE
|
|
[completionBlock_ autorelease];
|
|
completionBlock_ = nil;
|
|
#endif
|
|
|
|
// The signIn object's cancel method will close the window
|
|
[self.signIn cancelSigningIn];
|
|
hasDoneFinalRedirect_ = YES;
|
|
}
|
|
|
|
- (IBAction)closeWindow:(id)sender {
|
|
// dismiss the window/sheet before we call back the client
|
|
[self destroyWindow];
|
|
[self handlePrematureWindowClose];
|
|
}
|
|
|
|
- (IBAction)toggleStorePasswordInKeychain:(id)sender {
|
|
if ([sender state] == NSOffState) {
|
|
NSBeginAlertSheet(NSLocalizedString(@"Do you want to disable saving your login data in your keychain?", nil), NSLocalizedString(@"Keep enabled", nil), NSLocalizedString(@"Disable", nil), nil, self.window, self, @selector(disableKeychainSheetDidEnd:returnCode:contextInfo:), nil, NULL, NSLocalizedString(@"If you do not save the password you will have to sign in every time you use this feature.", nil));
|
|
}
|
|
}
|
|
|
|
- (void)disableKeychainSheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
|
|
{
|
|
if (returnCode == NSAlertDefaultReturn) {
|
|
self.keychainCheckbox.state = NSOnState;
|
|
}
|
|
}
|
|
|
|
#pragma mark SignIn callbacks
|
|
|
|
- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request {
|
|
// this is the signIn object's webRequest method, telling the controller
|
|
// to either display the request in the webview, or close the window
|
|
//
|
|
// All web requests and all window closing goes through this routine
|
|
|
|
#if DEBUG
|
|
if ((isWindowShown_ && request != nil)
|
|
|| (!isWindowShown_ && request == nil)) {
|
|
NSLog(@"Window state unexpected for request %@", [request URL]);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (request != nil) {
|
|
// display the request
|
|
self.initialRequest = request;
|
|
|
|
NSWindow *parentWindow = self.sheetModalForWindow;
|
|
if (parentWindow) {
|
|
[self setupSheetTerminationHandling];
|
|
|
|
NSWindow *sheet = [self window];
|
|
[NSApp beginSheet:sheet
|
|
modalForWindow:parentWindow
|
|
modalDelegate:self
|
|
didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
|
|
contextInfo:nil];
|
|
} else {
|
|
// modeless
|
|
[self showWindow:self];
|
|
}
|
|
isWindowShown_ = YES;
|
|
} else {
|
|
// request was nil
|
|
[self destroyWindow];
|
|
}
|
|
}
|
|
|
|
- (void)setupSheetTerminationHandling {
|
|
NSWindow *sheet = [self window];
|
|
|
|
SEL sel = @selector(setPreventsApplicationTerminationWhenModal:);
|
|
if ([sheet respondsToSelector:sel]) {
|
|
// setPreventsApplicationTerminationWhenModal is available in NSWindow
|
|
// on 10.6 and later
|
|
BOOL boolVal = !self.shouldAllowApplicationTermination;
|
|
NSMethodSignature *sig = [sheet methodSignatureForSelector:sel];
|
|
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
|
|
[invocation setSelector:sel];
|
|
[invocation setTarget:sheet];
|
|
[invocation setArgument:&boolVal atIndex:2];
|
|
[invocation invoke];
|
|
}
|
|
}
|
|
|
|
- (void)destroyWindow {
|
|
// no request; close the window (but not immediately, in case
|
|
// we're called in response to some window event)
|
|
NSWindow *parentWindow = self.sheetModalForWindow;
|
|
if (parentWindow) {
|
|
[NSApp endSheet:[self window]];
|
|
} else {
|
|
[[self window] performSelector:@selector(close)
|
|
withObject:nil
|
|
afterDelay:0.1
|
|
inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
|
|
|
|
// Avoid more callbacks after the delayed close happens, as the window
|
|
// controller may be gone.
|
|
[[self webView] setResourceLoadDelegate:nil];
|
|
}
|
|
isWindowShown_ = NO;
|
|
}
|
|
|
|
- (void)handlePrematureWindowClose {
|
|
if (!hasDoneFinalRedirect_) {
|
|
// tell the sign-in object to tell the user's finished method
|
|
// that we're done
|
|
[self.signIn windowWasClosed];
|
|
hasDoneFinalRedirect_ = YES;
|
|
}
|
|
}
|
|
|
|
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
|
|
[sheet orderOut:self];
|
|
|
|
self.sheetModalForWindow = nil;
|
|
}
|
|
|
|
- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error {
|
|
if (!hasCalledFinished_) {
|
|
hasCalledFinished_ = YES;
|
|
|
|
if (error == nil) {
|
|
BOOL shouldUseKeychain = [self shouldUseKeychain];
|
|
if (shouldUseKeychain) {
|
|
BOOL canAuthorize = auth.canAuthorize;
|
|
BOOL isKeychainChecked = ([keychainCheckbox_ state] == NSOnState);
|
|
|
|
NSString *keychainItemName = self.keychainItemName;
|
|
|
|
if (isKeychainChecked && canAuthorize) {
|
|
// save the auth params in the keychain
|
|
[[self class] saveAuthToKeychainForName:keychainItemName
|
|
authentication:auth];
|
|
} else {
|
|
// remove the auth params from the keychain
|
|
[[self class] removeAuthFromKeychainForName:keychainItemName];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (delegate_ && finishedSelector_) {
|
|
SEL sel = finishedSelector_;
|
|
NSMethodSignature *sig = [delegate_ methodSignatureForSelector:sel];
|
|
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
|
|
[invocation setSelector:sel];
|
|
[invocation setTarget:delegate_];
|
|
[invocation setArgument:&self atIndex:2];
|
|
[invocation setArgument:&auth atIndex:3];
|
|
[invocation setArgument:&error atIndex:4];
|
|
[invocation invoke];
|
|
}
|
|
|
|
[delegate_ autorelease];
|
|
delegate_ = nil;
|
|
|
|
#if NS_BLOCKS_AVAILABLE
|
|
if (completionBlock_) {
|
|
completionBlock_(auth, error);
|
|
|
|
// release the block here to avoid a retain loop on the controller
|
|
[completionBlock_ autorelease];
|
|
completionBlock_ = nil;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
static Class gSignInClass = Nil;
|
|
|
|
+ (Class)signInClass {
|
|
if (gSignInClass == Nil) {
|
|
gSignInClass = [GTMOAuth2SignIn class];
|
|
}
|
|
return gSignInClass;
|
|
}
|
|
|
|
+ (void)setSignInClass:(Class)theClass {
|
|
gSignInClass = theClass;
|
|
}
|
|
|
|
#pragma mark Token Revocation
|
|
|
|
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
|
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
|
|
[[self signInClass] revokeTokenForGoogleAuthentication:auth];
|
|
}
|
|
#endif
|
|
|
|
#pragma mark WebView methods
|
|
|
|
- (NSURLRequest *)webView:(WebView *)sender resource:(id)identifier willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse fromDataSource:(WebDataSource *)dataSource {
|
|
// override WebKit's cookie storage with our own to avoid cookie persistence
|
|
// across sign-ins and interaction with the Safari browser's sign-in state
|
|
[self handleCookiesForResponse:redirectResponse];
|
|
request = [self addCookiesToRequest:request];
|
|
|
|
if (!hasDoneFinalRedirect_) {
|
|
hasDoneFinalRedirect_ = [self.signIn requestRedirectedToRequest:request];
|
|
if (hasDoneFinalRedirect_) {
|
|
// signIn has told the window to close
|
|
return nil;
|
|
}
|
|
}
|
|
return request;
|
|
}
|
|
|
|
- (void)webView:(WebView *)sender resource:(id)identifier didReceiveResponse:(NSURLResponse *)response fromDataSource:(WebDataSource *)dataSource {
|
|
// override WebKit's cookie storage with our own
|
|
[self handleCookiesForResponse:response];
|
|
}
|
|
|
|
- (void)webView:(WebView *)sender resource:(id)identifier didFinishLoadingFromDataSource:(WebDataSource *)dataSource {
|
|
NSString *title = [sender stringByEvaluatingJavaScriptFromString:@"document.title"];
|
|
if ([title length] > 0) {
|
|
[self.signIn titleChanged:title];
|
|
}
|
|
|
|
[signIn_ cookiesChanged:(NSHTTPCookieStorage *)cookieStorage_];
|
|
}
|
|
|
|
- (void)webView:(WebView *)sender resource:(id)identifier didFailLoadingWithError:(NSError *)error fromDataSource:(WebDataSource *)dataSource {
|
|
[self.signIn loadFailedWithError:error];
|
|
}
|
|
|
|
- (void)windowWillClose:(NSNotification *)note {
|
|
if (isWindowShown_) {
|
|
[self handlePrematureWindowClose];
|
|
}
|
|
isWindowShown_ = NO;
|
|
}
|
|
|
|
- (void)webView:(WebView *)webView
|
|
decidePolicyForNewWindowAction:(NSDictionary *)actionInformation
|
|
request:(NSURLRequest *)request
|
|
newFrameName:(NSString *)frameName
|
|
decisionListener:(id<WebPolicyDecisionListener>)listener {
|
|
SEL sel = self.externalRequestSelector;
|
|
if (sel) {
|
|
[delegate_ performSelector:sel
|
|
withObject:self
|
|
withObject:request];
|
|
} else {
|
|
// default behavior is to open the URL in NSWorkspace's default browser
|
|
NSURL *url = [request URL];
|
|
[[NSWorkspace sharedWorkspace] openURL:url];
|
|
}
|
|
[listener ignore];
|
|
}
|
|
|
|
#pragma mark Cookie management
|
|
|
|
// Rather than let the WebView use Safari's default cookie storage, we intercept
|
|
// requests and response to segregate and later discard cookies from signing in.
|
|
//
|
|
// This allows the application to actually sign out by discarding the auth token
|
|
// rather than the user being kept signed in by the cookies.
|
|
|
|
- (void)handleCookiesForResponse:(NSURLResponse *)response {
|
|
if (self.shouldPersistUser) {
|
|
// we'll let WebKit handle the cookies; they'll persist across apps
|
|
// and across runs of this app
|
|
return;
|
|
}
|
|
|
|
if ([response respondsToSelector:@selector(allHeaderFields)]) {
|
|
// grab the cookies from the header as NSHTTPCookies and store them locally
|
|
NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
|
|
if (headers) {
|
|
NSURL *url = [response URL];
|
|
NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers
|
|
forURL:url];
|
|
if ([cookies count] > 0) {
|
|
[cookieStorage_ setCookies:cookies];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request {
|
|
if (self.shouldPersistUser) {
|
|
// we'll let WebKit handle the cookies; they'll persist across apps
|
|
// and across runs of this app
|
|
return request;
|
|
}
|
|
|
|
// override WebKit's usual automatic storage of cookies
|
|
NSMutableURLRequest *mutableRequest = [[request mutableCopy] autorelease];
|
|
[mutableRequest setHTTPShouldHandleCookies:NO];
|
|
|
|
// add our locally-stored cookies for this URL, if any
|
|
NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]];
|
|
if ([cookies count] > 0) {
|
|
NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
|
|
NSString *cookieHeader = [headers objectForKey:@"Cookie"];
|
|
if (cookieHeader) {
|
|
[mutableRequest setValue:cookieHeader forHTTPHeaderField:@"Cookie"];
|
|
}
|
|
}
|
|
return mutableRequest;
|
|
}
|
|
|
|
#pragma mark Keychain support
|
|
|
|
+ (NSString *)prefsKeyForName:(NSString *)keychainItemName {
|
|
NSString *result = [@"OAuth2: " stringByAppendingString:keychainItemName];
|
|
return result;
|
|
}
|
|
|
|
+ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName
|
|
authentication:(GTMOAuth2Authentication *)auth {
|
|
|
|
[self removeAuthFromKeychainForName:keychainItemName];
|
|
|
|
// don't save unless we have a token that can really authorize requests
|
|
if (!auth.canAuthorize) return NO;
|
|
|
|
// make a response string containing the values we want to save
|
|
NSString *password = [auth persistenceResponseString];
|
|
|
|
SecKeychainRef defaultKeychain = NULL;
|
|
SecKeychainItemRef *dontWantItemRef= NULL;
|
|
const char *utf8ServiceName = [keychainItemName UTF8String];
|
|
const char *utf8Password = [password UTF8String];
|
|
|
|
OSStatus err = SecKeychainAddGenericPassword(defaultKeychain,
|
|
(UInt32) strlen(utf8ServiceName), utf8ServiceName,
|
|
(UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
|
|
(UInt32) strlen(utf8Password), utf8Password,
|
|
dontWantItemRef);
|
|
BOOL didSucceed = (err == noErr);
|
|
if (didSucceed) {
|
|
// write to preferences that we have a keychain item (so we know later
|
|
// that we can read from the keychain without raising a permissions dialog)
|
|
NSString *prefKey = [self prefsKeyForName:keychainItemName];
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
[defaults setBool:YES forKey:prefKey];
|
|
}
|
|
|
|
return didSucceed;
|
|
}
|
|
|
|
+ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName {
|
|
|
|
SecKeychainRef defaultKeychain = NULL;
|
|
SecKeychainItemRef itemRef = NULL;
|
|
const char *utf8ServiceName = [keychainItemName UTF8String];
|
|
|
|
// we don't really care about the password here, we just want to
|
|
// get the SecKeychainItemRef so we can delete it.
|
|
OSStatus err = SecKeychainFindGenericPassword (defaultKeychain,
|
|
(UInt32) strlen(utf8ServiceName), utf8ServiceName,
|
|
(UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
|
|
0, NULL, // ignore password
|
|
&itemRef);
|
|
if (err != noErr) {
|
|
// failure to find is success
|
|
return YES;
|
|
} else {
|
|
// found something, so delete it
|
|
err = SecKeychainItemDelete(itemRef);
|
|
CFRelease(itemRef);
|
|
|
|
// remove our preference key
|
|
NSString *prefKey = [self prefsKeyForName:keychainItemName];
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
[defaults removeObjectForKey:prefKey];
|
|
|
|
return (err == noErr);
|
|
}
|
|
}
|
|
|
|
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
|
+ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
|
|
clientID:(NSString *)clientID
|
|
clientSecret:(NSString *)clientSecret {
|
|
Class signInClass = [self signInClass];
|
|
NSURL *tokenURL = [signInClass googleTokenURL];
|
|
NSString *redirectURI = [signInClass nativeClientRedirectURI];
|
|
|
|
GTMOAuth2Authentication *auth;
|
|
auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
|
|
tokenURL:tokenURL
|
|
redirectURI:redirectURI
|
|
clientID:clientID
|
|
clientSecret:clientSecret];
|
|
|
|
[GTMOAuth2WindowController authorizeFromKeychainForName:keychainItemName
|
|
authentication:auth];
|
|
return auth;
|
|
}
|
|
#endif
|
|
|
|
+ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName
|
|
authentication:(GTMOAuth2Authentication *)newAuth {
|
|
[newAuth setAccessToken:nil];
|
|
|
|
// before accessing the keychain, check preferences to verify that we've
|
|
// previously saved a token to the keychain (so we don't needlessly raise
|
|
// a keychain access permission dialog)
|
|
NSString *prefKey = [self prefsKeyForName:keychainItemName];
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
BOOL flag = [defaults boolForKey:prefKey];
|
|
if (!flag) {
|
|
return NO;
|
|
}
|
|
|
|
BOOL didGetTokens = NO;
|
|
|
|
SecKeychainRef defaultKeychain = NULL;
|
|
const char *utf8ServiceName = [keychainItemName UTF8String];
|
|
SecKeychainItemRef *dontWantItemRef = NULL;
|
|
|
|
void *passwordBuff = NULL;
|
|
UInt32 passwordBuffLength = 0;
|
|
|
|
OSStatus err = SecKeychainFindGenericPassword(defaultKeychain,
|
|
(UInt32) strlen(utf8ServiceName), utf8ServiceName,
|
|
(UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
|
|
&passwordBuffLength, &passwordBuff,
|
|
dontWantItemRef);
|
|
if (err == noErr && passwordBuff != NULL) {
|
|
|
|
NSString *password = [[[NSString alloc] initWithBytes:passwordBuff
|
|
length:passwordBuffLength
|
|
encoding:NSUTF8StringEncoding] autorelease];
|
|
|
|
// free the password buffer that was allocated above
|
|
SecKeychainItemFreeContent(NULL, passwordBuff);
|
|
|
|
if (password != nil) {
|
|
[newAuth setKeysForResponseString:password];
|
|
didGetTokens = YES;
|
|
}
|
|
}
|
|
return didGetTokens;
|
|
}
|
|
|
|
#pragma mark User Properties
|
|
|
|
- (void)setProperty:(id)obj forKey:(NSString *)key {
|
|
if (obj == nil) {
|
|
// User passed in nil, so delete the property
|
|
[properties_ removeObjectForKey:key];
|
|
} else {
|
|
// Be sure the property dictionary exists
|
|
if (properties_ == nil) {
|
|
[self setProperties:[NSMutableDictionary dictionary]];
|
|
}
|
|
[properties_ setObject:obj forKey:key];
|
|
}
|
|
}
|
|
|
|
- (id)propertyForKey:(NSString *)key {
|
|
id obj = [properties_ objectForKey:key];
|
|
|
|
// Be sure the returned pointer has the life of the autorelease pool,
|
|
// in case self is released immediately
|
|
return [[obj retain] autorelease];
|
|
}
|
|
|
|
#pragma mark Accessors
|
|
|
|
- (GTMOAuth2Authentication *)authentication {
|
|
return self.signIn.authentication;
|
|
}
|
|
|
|
- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val {
|
|
self.signIn.networkLossTimeoutInterval = val;
|
|
}
|
|
|
|
- (NSTimeInterval)networkLossTimeoutInterval {
|
|
return self.signIn.networkLossTimeoutInterval;
|
|
}
|
|
|
|
- (BOOL)shouldUseKeychain {
|
|
NSString *name = self.keychainItemName;
|
|
return ([name length] > 0);
|
|
}
|
|
|
|
@end
|
|
|
|
#endif // #if !TARGET_OS_IPHONE
|
|
|
|
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|