From c84b93efc3e68d253e018e80edffc159246f0e94 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Fri, 18 Aug 2017 18:55:33 -0700 Subject: [PATCH 01/14] Remove all references to google from the plugin And from the `AuthCompletionHandler` interface. Instead, wrap all references into the `AuthCompletionHandler` implementation Testing done: - initial signin + refresh both work --- src/ios/AuthCompletionHandler.h | 26 ++++++--- src/ios/AuthCompletionHandler.m | 96 ++++++++++++++++++++++++++++++++- src/ios/BEMJWTAuth.h | 3 +- src/ios/BEMJWTAuth.m | 86 ++++++++++++----------------- 4 files changed, 147 insertions(+), 64 deletions(-) diff --git a/src/ios/AuthCompletionHandler.h b/src/ios/AuthCompletionHandler.h index a91942c..cbc3601 100644 --- a/src/ios/AuthCompletionHandler.h +++ b/src/ios/AuthCompletionHandler.h @@ -7,22 +7,32 @@ // #import -#import -typedef void (^AuthCompletionCallback)(GIDGoogleUser *,NSError*); +typedef void (^AuthResultCallback)(NSString *,NSError*); -// static NSString *const kKeychainItemName = @"OAuth: Google Email"; -#define kKeychainItemName @"OAuth: Google Email" - -@interface AuthCompletionHandler : NSObject +// @interface AuthCompletionHandler : NSObject +@interface AuthCompletionHandler : NSObject +(AuthCompletionHandler*) sharedInstance; +@property (atomic, retain) UIViewController* viewController; + // Background refresh (no UI) -- (void) getValidAuth:(AuthCompletionCallback) authCompletionCallback; +// This is commented out because we want people to call the methods that +// return results directly, so that we can mock them for easier development +// - (void) getValidAuth:(AuthCompletionCallback) authCompletionCallback; + +// Handle the notification callback to complete the authentication +- (void) handleNotification:(NSNotification*) notification; // Register callback (either for -- (void) registerCallback:(AuthCompletionCallback) authCompletionCallback; +// - (void) registerCallback:(AuthCompletionCallback) authCompletionCallback; + +// Get token +- (void) getEmail:(AuthResultCallback)authResultCallback; +- (void) getJWT:(AuthResultCallback)authResultCallback; +- (void) getExpirationDate:(AuthResultCallback)authResultCallback; +- (void) uiSignIn:(AuthResultCallback)authResultCallback; // Background refresh (no UI) diff --git a/src/ios/AuthCompletionHandler.m b/src/ios/AuthCompletionHandler.m index 5c5a13c..aae4b0e 100644 --- a/src/ios/AuthCompletionHandler.m +++ b/src/ios/AuthCompletionHandler.m @@ -15,6 +15,18 @@ #import "BEMConnectionSettings.h" #import "BEMConstants.h" #import "LocalNotificationManager.h" +#import + + +typedef void (^AuthCompletionCallback)(GIDGoogleUser *,NSError*); +typedef NSString* (^ProfileRetValue)(GIDGoogleUser *); + +#define NOT_SIGNED_IN_CODE 1000 + +@interface AuthCompletionHandler () +// @property (atomic, retain) UIViewController* viewController; +// @property (atomic, retain) alreadyPresenting; +@end @implementation AuthCompletionHandler @@ -27,7 +39,15 @@ + (AuthCompletionHandler*)sharedInstance if (sharedInstance == nil) { NSLog(@"creating new AuthCompletionHandler sharedInstance"); sharedInstance = [AuthCompletionHandler new]; - [GIDSignIn sharedInstance].delegate = sharedInstance; + + GIDSignIn* signIn = [GIDSignIn sharedInstance]; + signIn.clientID = [[ConnectionSettings sharedInstance] getGoogleiOSClientID]; + // client secret is no longer required for this client + // signIn.serverClientID = [[ConnectionSettings sharedInstance] getGoogleiOSClientSecret]; + signIn.delegate = sharedInstance; + signIn.uiDelegate = sharedInstance; + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Finished setting clientId = %@ and serverClientID = %@", signIn.clientID, signIn.serverClientID]]; + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Finished setting delegate = %@ and uiDelegate = %@", signIn.delegate, signIn.uiDelegate]]; } return sharedInstance; } @@ -74,7 +94,81 @@ -(void)signIn:(GIDSignIn*)signIn didSignInForUser:(GIDGoogleUser *)user } } +-(void)handleNotification:(NSNotification *)notification +{ + NSURL* url = [notification object]; + NSDictionary* options = [notification userInfo]; + + [[GIDSignIn sharedInstance] handleURL:url + sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] + annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]; +} + // END: Silent auth methods +// BEGIN: callbacks for extracting value from the auth completion + +- (void) getEmail:(AuthResultCallback) authResultCallback +{ + GIDGoogleUser* currUser = [GIDSignIn sharedInstance].currentUser; + if (currUser != NULL) { + authResultCallback(currUser.profile.email, NULL); + } else { + NSError* error = [NSError errorWithDomain:@"BEMAuthError" + code:NOT_SIGNED_IN_CODE + userInfo:NULL]; + authResultCallback(NULL, error); + } +} + +- (void) getJWT:(AuthResultCallback)authResultCallback +{ + [self getValidAuth:[self getRedirectedCallback:authResultCallback withRetValue:^NSString *(GIDGoogleUser *user) { + return user.authentication.idToken; + }]]; +} + +- (void) getExpirationDate:(AuthResultCallback)authResultCallback +{ + [self getValidAuth:[self getRedirectedCallback:authResultCallback withRetValue:^NSString *(GIDGoogleUser *user) { + return user.authentication.idTokenExpirationDate.description; + }]]; +} + +-(AuthCompletionCallback) getRedirectedCallback:(AuthResultCallback)redirCallback withRetValue:(ProfileRetValue) retValueFunctor +{ + return ^(GIDGoogleUser *user, NSError *error) { + if (error == NULL) { + NSString* resultStr = retValueFunctor(user); + redirCallback(resultStr, NULL); + } else { + redirCallback(NULL, error); + } + }; +} + +// END: callbacks for extracting value from the auth completion + +// BEGIN: UI interaction + +- (void) uiSignIn:(AuthResultCallback)authResultCallback +{ + [self registerCallback:[self getRedirectedCallback:authResultCallback + withRetValue:^NSString *(GIDGoogleUser *user) { + return user.profile.email; + }]]; + [[GIDSignIn sharedInstance] signIn]; +} + +-(void) signIn:(GIDSignIn*)signIn presentViewController:(UIViewController *)loginScreen +{ + [self.viewController presentViewController:loginScreen animated:YES completion:NULL]; +} + +-(void) signIn:(GIDSignIn*)signIn dismissViewController:(UIViewController *)loginScreen +{ + [self.viewController dismissViewControllerAnimated:YES completion:NULL]; +} +// END: UI interaction @end diff --git a/src/ios/BEMJWTAuth.h b/src/ios/BEMJWTAuth.h index 45d1918..767f146 100644 --- a/src/ios/BEMJWTAuth.h +++ b/src/ios/BEMJWTAuth.h @@ -1,7 +1,6 @@ #import -#import -@interface BEMJWTAuth: CDVPlugin +@interface BEMJWTAuth: CDVPlugin - (void) getUserEmail:(CDVInvokedUrlCommand*)command; - (void) signIn:(CDVInvokedUrlCommand*)command; diff --git a/src/ios/BEMJWTAuth.m b/src/ios/BEMJWTAuth.m index fcf8276..ce3b904 100644 --- a/src/ios/BEMJWTAuth.m +++ b/src/ios/BEMJWTAuth.m @@ -4,22 +4,20 @@ #import "AuthCompletionHandler.h" #import "BEMBuiltinUserCache.h" -@interface BEMJWTAuth () +@interface BEMJWTAuth () @property (nonatomic, retain) CDVInvokedUrlCommand* command; @end @implementation BEMJWTAuth: CDVPlugin -typedef NSString* (^ProfileRetValue)(GIDGoogleUser *); + - (void)pluginInitialize { [LocalNotificationManager addNotification:@"BEMJWTAuth:pluginInitialize singleton -> initialize completion handler"]; - GIDSignIn* signIn = [GIDSignIn sharedInstance]; - signIn.clientID = [[ConnectionSettings sharedInstance] getGoogleiOSClientID]; - // signIn.serverClientID = [[ConnectionSettings sharedInstance] getGoogleiOSClientSecret]; - signIn.uiDelegate = self; - [[AuthCompletionHandler sharedInstance] getValidAuth:^(GIDGoogleUser *user, NSError *error) { - if (user == NULL) { + AuthCompletionHandler* authHandler = [AuthCompletionHandler sharedInstance]; + authHandler.viewController = self.viewController; + [authHandler getJWT:^(NSString *token, NSError *error) { + if (token == NULL) { NSDictionary* introDoneResult = [[BuiltinUserCache database] getLocalStorage:@"intro_done" withMetadata:NO]; [LocalNotificationManager addNotification:[NSString stringWithFormat:@"intro_done result = %@", introDoneResult]]; if (introDoneResult != NULL) { @@ -37,8 +35,6 @@ - (void)pluginInitialize } } }]; - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Finished setting clientId = %@ and serverClientID = %@", signIn.clientID, signIn.serverClientID]]; - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Finished setting delegate = %@ and uiDelegate = %@", signIn.delegate, signIn.uiDelegate]]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationLaunchedWithUrl:) name:CDVPluginHandleOpenURLNotification object:nil]; } @@ -47,18 +43,23 @@ - (void)getUserEmail:(CDVInvokedUrlCommand*)command NSString* callbackId = [command callbackId]; @try { - GIDGoogleUser* currUser = [GIDSignIn sharedInstance].currentUser; - if (currUser != NULL) { - CDVPluginResult* result = [CDVPluginResult - resultWithStatus:CDVCommandStatus_OK - messageAsString:currUser.profile.email]; - [self.commandDelegate sendPluginResult:result callbackId:callbackId]; - } else { - CDVPluginResult* result = [CDVPluginResult - resultWithStatus:CDVCommandStatus_OK - messageAsString:NULL]; - [self.commandDelegate sendPluginResult:result callbackId:callbackId]; - } + // Ideally, we would re-use getCallbackForCommand here, but that would return + // an error if the user did not exist. But the existing behavior is that it returns the + // message OK with result = NULL if the user does not exist. + // Maintaining that backwards compatible behavior for now... + [[AuthCompletionHandler sharedInstance] getEmail:^(NSString *userEmail, NSError *error) { + if (userEmail != NULL) { + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK + messageAsString:userEmail]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK + messageAsString:NULL]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + }]; } @catch (NSException *exception) { NSString* msg = [NSString stringWithFormat: @"While getting user email, error %@", exception]; @@ -73,11 +74,8 @@ - (void)getUserEmail:(CDVInvokedUrlCommand*)command - (void)signIn:(CDVInvokedUrlCommand*)command { @try { - [[AuthCompletionHandler sharedInstance] registerCallback:[self getCallback:^NSString *(GIDGoogleUser *user) { - return user.profile.email; - } forCommand:command]]; - [[GIDSignIn sharedInstance] signIn]; -} + [[AuthCompletionHandler sharedInstance] uiSignIn:[self getCallbackForCommand:command]]; + } @catch (NSException *exception) { NSString* msg = [NSString stringWithFormat: @"While getting user email, error %@", exception]; CDVPluginResult* result = [CDVPluginResult @@ -90,24 +88,20 @@ - (void)signIn:(CDVInvokedUrlCommand*)command - (void)getJWT:(CDVInvokedUrlCommand*)command { @try { - [[AuthCompletionHandler sharedInstance] getValidAuth:[self getCallback:^NSString *(GIDGoogleUser *user) { - return user.authentication.idToken; - } forCommand:command]]; - } - @catch (NSException *exception) { - NSString* msg = [NSString stringWithFormat: @"While getting user email, error %@", exception]; + [[AuthCompletionHandler sharedInstance] getJWT:[self getCallbackForCommand:command]]; + } + @catch (NSException *exception) { + NSString* msg = [NSString stringWithFormat: @"While getting JWT, error %@", exception]; CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:msg]; [self.commandDelegate sendPluginResult:result callbackId:[command callbackId]]; - } - + } } --(AuthCompletionCallback) getCallback:(ProfileRetValue) retValueFunctor forCommand:(CDVInvokedUrlCommand*)command +-(AuthResultCallback) getCallbackForCommand:(CDVInvokedUrlCommand*)command { - return ^(GIDGoogleUser *user, NSError *error) { + return ^(NSString *resultStr, NSError *error) { if (error == NULL) { - NSString* resultStr = retValueFunctor(user); CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:resultStr]; [self.commandDelegate sendPluginResult:result @@ -122,24 +116,10 @@ -(AuthCompletionCallback) getCallback:(ProfileRetValue) retValueFunctor forComma }; } --(void) signIn:(GIDSignIn*)signIn presentViewController:(UIViewController *)loginScreen -{ - [self.viewController presentViewController:loginScreen animated:YES completion:NULL]; -} - --(void) signIn:(GIDSignIn*)signIn dismissViewController:(UIViewController *)loginScreen -{ - [self.viewController dismissViewControllerAnimated:YES completion:NULL]; -} - (void)applicationLaunchedWithUrl:(NSNotification*)notification { - NSURL* url = [notification object]; - NSDictionary* options = [notification userInfo]; - - [[GIDSignIn sharedInstance] handleURL:url - sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] - annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]; + [[AuthCompletionHandler sharedInstance] handleNotification:notification]; } @end From eddfe9bd2bedf5f932f30ee2fcbc9de7bbecf497 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Sat, 19 Aug 2017 17:07:00 -0700 Subject: [PATCH 02/14] Remove all references to google from the plugin And from the interface to `GoogleAccountManagerAuth`. All references are only in the implementation. --- plugin.xml | 2 + src/android/AuthPendingResult.java | 153 ++++++++++++++++++++++ src/android/AuthResult.java | 35 +++++ src/android/GoogleAccountManagerAuth.java | 121 +++++++++++++---- src/android/JWTAuthPlugin.java | 80 +++++++---- src/android/UserProfile.java | 22 +--- 6 files changed, 343 insertions(+), 70 deletions(-) create mode 100644 src/android/AuthPendingResult.java create mode 100644 src/android/AuthResult.java diff --git a/plugin.xml b/plugin.xml index 0f3f09a..4be33ca 100644 --- a/plugin.xml +++ b/plugin.xml @@ -47,6 +47,8 @@ + + diff --git a/src/android/AuthPendingResult.java b/src/android/AuthPendingResult.java new file mode 100644 index 0000000..bb34a04 --- /dev/null +++ b/src/android/AuthPendingResult.java @@ -0,0 +1,153 @@ +package edu.berkeley.eecs.emission.cordova.jwtauth; + +import android.os.AsyncTask; +import android.support.annotation.NonNull; + +import com.google.android.gms.auth.api.Auth; +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; + +import java.util.concurrent.TimeUnit; + +import edu.berkeley.eecs.emission.BuildConfig; +import android.util.Log; + +public class AuthPendingResult extends PendingResult { + private static final String TAG = "AuthPendingResult"; + private final Object syncToken = new Object(); + + private AuthResult mAuthResult; + private boolean mCancelled; + private ResultCallback mResultCallback; + + protected AuthPendingResult() { + mAuthResult = null; + mResultCallback = null; + mCancelled = false; + } + + @NonNull + @Override + public AuthResult await() { + synchronized (syncToken) { + if (mAuthResult != null) { + return mAuthResult; + } // else + try { + syncToken.wait(); + if (BuildConfig.DEBUG) { + if (mAuthResult == null) { + throw new RuntimeException("in await, notify received," + + " but mAuthResult = null"); + } + } + } catch (InterruptedException e) { + mAuthResult = new AuthResult(new Status(CommonStatusCodes.INTERRUPTED), null, null); + } + return mAuthResult; + } + } + + @NonNull + @Override + public AuthResult await(long l, @NonNull TimeUnit timeUnit) { + synchronized (syncToken) { + if (mAuthResult != null) { + return mAuthResult; + } + try { + syncToken.wait(timeUnit.toMillis(l)); + if (BuildConfig.DEBUG) { + if (mAuthResult == null) { + throw new RuntimeException("in await, notify received," + + " but mAuthResult = null"); + } + } + } catch (InterruptedException e) { + mAuthResult = new AuthResult(new Status(CommonStatusCodes.INTERRUPTED), null, null); + } + } + return mAuthResult; + } + + @Override + public void cancel() { + synchronized(syncToken) { + mAuthResult = new AuthResult(new Status(CommonStatusCodes.CANCELED), null, null); + mCancelled = true; + syncToken.notify(); + if (mResultCallback != null) { + mResultCallback.onResult(mAuthResult); + } + } + } + + @Override + public boolean isCanceled() { + return mCancelled; + } + + @Override + public void setResultCallback(@NonNull ResultCallback resultCallback) { + synchronized (syncToken) { + mResultCallback = resultCallback; + if (mAuthResult != null) { + mResultCallback.onResult(mAuthResult); + } + } + } + + @Override + public void setResultCallback(@NonNull ResultCallback resultCallback, long l, @NonNull TimeUnit timeUnit) { + synchronized (syncToken) { + mResultCallback = resultCallback; + if (mAuthResult != null) { + mResultCallback.onResult(mAuthResult); + } + } + if (mAuthResult == null) { + AsyncTask sleepTask = new AsyncTask() { + @Override + protected Void doInBackground(Long... millis) { + try { + long millisecs = millis[0]; + Thread.sleep(millisecs); + synchronized (syncToken) { + if (mAuthResult == null) { + AuthResult timeoutResult = new AuthResult( + new com.google.android.gms.common.api.Status(CommonStatusCodes.TIMEOUT), null, null); + mResultCallback.onResult(timeoutResult); + mResultCallback = null; + } else { + Log.d(TAG, "Result already set, nothing to do"); + } + } + } catch (InterruptedException e) { + synchronized (syncToken) { + AuthResult timeoutResult = new AuthResult( + new com.google.android.gms.common.api.Status(CommonStatusCodes.INTERRUPTED), null, null); + mResultCallback.onResult(timeoutResult); + mResultCallback = null; + } + } + return null; + } + }; + sleepTask.execute(timeUnit.toMillis(l)); + } else { + Log.d(TAG, "Result already set, nothing to do"); + } + } + + protected void setResult(AuthResult result) { + synchronized (syncToken) { + mAuthResult = result; + syncToken.notifyAll(); + if (mResultCallback != null) { + mResultCallback.onResult(mAuthResult); + } + } + } +} diff --git a/src/android/AuthResult.java b/src/android/AuthResult.java new file mode 100644 index 0000000..235689d --- /dev/null +++ b/src/android/AuthResult.java @@ -0,0 +1,35 @@ +package edu.berkeley.eecs.emission.cordova.jwtauth; + +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; + +/** + * Result class for async operation. + * Unsure whether we should use PendingResult for callbacks + * but it is certainly more consistent with the new google APIs + */ + +public class AuthResult implements Result { + private Status status; + private String email; + private String token; + + public AuthResult(Status istatus, String iemail, String itoken) { + status = istatus; + email = iemail; + token = itoken; + } + + @Override + public Status getStatus() { + return status; + } + + public String getEmail() { + return email; + } + + public String getToken() { + return token; + } +} diff --git a/src/android/GoogleAccountManagerAuth.java b/src/android/GoogleAccountManagerAuth.java index 8d163db..69b0674 100644 --- a/src/android/GoogleAccountManagerAuth.java +++ b/src/android/GoogleAccountManagerAuth.java @@ -6,11 +6,17 @@ import com.google.android.gms.auth.GoogleAuthUtil; import com.google.android.gms.auth.UserRecoverableAuthException; import com.google.android.gms.common.AccountPicker; +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.Status; + +import android.accounts.AccountManager; import edu.berkeley.eecs.emission.cordova.connectionsettings.ConnectionSettings; +import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; import android.app.Activity; import android.app.AlertDialog; +import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; @@ -18,14 +24,24 @@ import android.provider.Settings; public class GoogleAccountManagerAuth { - - Activity mCtxt; - int mRequestCode; + private static final int REQUEST_CODE_PICK_ACCOUNT = 1000; + private static final int REQUEST_CODE_GET_TOKEN = 1001; public static String TAG = "GoogleAccountManagerAuth"; - public GoogleAccountManagerAuth(Activity ctxt, int requestCode) { + private Activity mActivity; + private Context mCtxt; + private AuthPendingResult mAuthPending; + + // This has to be a class instance instead of a singleton like in + // iOS because we are not supposed to store contexts in static variables + // singleton pattern has static GoogleAccountManagerAuth -> mCtxt + public GoogleAccountManagerAuth(Activity activity) { + mCtxt = activity; + mActivity = activity; + } + + public GoogleAccountManagerAuth(Context ctxt) { mCtxt = ctxt; - mRequestCode = requestCode; } /* @@ -37,7 +53,7 @@ public GoogleAccountManagerAuth(Activity ctxt, int requestCode) { * IT. */ - public void getUserName() { + public AuthPendingResult uiSignIn() { try { String[] accountTypes = new String[]{"com.google"}; @@ -50,11 +66,18 @@ public void getUserName() { Intent intent = AccountPicker.newChooseAccountIntent(null, null, accountTypes, true, null, null, null, null); - + // Note that because we are starting the activity using mCtxt, the activity callback // invoked will not be the one in this class, but the one in the original context. // In our current flow, that is the one in the MainActivity - mCtxt.startActivityForResult(intent, mRequestCode); + mAuthPending = new AuthPendingResult(); + if (mActivity == null) { + AuthResult result = new AuthResult(new Status(CommonStatusCodes.DEVELOPER_ERROR, "Context instead of activity while signing in"), null, null); + mAuthPending.setResult(result); + } else { + mActivity.startActivityForResult(intent, REQUEST_CODE_PICK_ACCOUNT); + } + return mAuthPending; } catch (ActivityNotFoundException e) { // If the user does not have a google account, then // this exception is thrown @@ -69,30 +92,82 @@ public void onClick(DialogInterface dialog, int which) { }); alertDialog.show(); } + return mAuthPending; + } + + /* + * BEGIN: Calls to get the data + * Going to configure these with listeners in order to support background operations + * It's really kind of amazing that GoogleAuthUtil doesn't enforce that, and the new + * GoogleSignIn code probably will + */ + + public AuthPendingResult getUserEmail() { + AuthPendingResult authPending = new AuthPendingResult(); + AuthResult result = new AuthResult( + new Status(CommonStatusCodes.SUCCESS), + UserProfile.getInstance(mCtxt).getUserEmail(), + null); + authPending.setResult(result); + return authPending; } - - public static String getServerToken(Context context, String userName) { - String serverToken = null; - if (ConnectionSettings.isSkipAuth(context)) { - System.out.println("isSkipAuth = true, serverToken = "+userName); - return userName; - } + + public AuthPendingResult getServerToken() { + AuthPendingResult authPending = new AuthPendingResult(); try { - String AUTH_SCOPE = "audience:server:client_id:"+ConnectionSettings.getGoogleWebAppClientID(context); - serverToken = GoogleAuthUtil.getToken(context, + String serverToken = null; + String AUTH_SCOPE = "audience:server:client_id:"+ConnectionSettings.getGoogleWebAppClientID(mCtxt); + String userName = UserProfile.getInstance(mCtxt).getUserEmail(); + serverToken = GoogleAuthUtil.getToken(mCtxt, userName, AUTH_SCOPE); + Log.i(mCtxt, TAG, "serverToken = "+serverToken); + AuthResult result = new AuthResult( + new Status(CommonStatusCodes.SUCCESS), + userName, + serverToken); + authPending.setResult(result); } catch (UserRecoverableAuthException e) { - // TODO Auto-generated catch block - context.startActivity(e.getIntent()); + PendingIntent intent = PendingIntent.getActivity(mCtxt, REQUEST_CODE_GET_TOKEN, + e.getIntent(), PendingIntent.FLAG_UPDATE_CURRENT); + AuthResult result = new AuthResult( + new Status(CommonStatusCodes.SUCCESS, e.getLocalizedMessage(), intent), + null, + null); + authPending.setResult(result); e.printStackTrace(); } catch (IOException e) { - // TODO Auto-generated catch block + authPending.setResult(getErrorResult(e.getLocalizedMessage())); e.printStackTrace(); } catch (GoogleAuthException e) { - // TODO Auto-generated catch block + authPending.setResult(getErrorResult(e.getLocalizedMessage())); e.printStackTrace(); } - System.out.println("serverToken = "+serverToken); - return serverToken; + return authPending; + } + + /* + * END: Calls to get the data + */ + + // Similar to handleNotification on iOS + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_CODE_PICK_ACCOUNT) { + if (resultCode == Activity.RESULT_OK) { + String userEmail = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); + UserProfile.getInstance(mCtxt).setUserEmail(userEmail); + AuthResult result = new AuthResult( + new Status(CommonStatusCodes.SUCCESS), + userEmail, + null); + mAuthPending.setResult(result); + } else { + mAuthPending.setResult(getErrorResult("Result code = " + resultCode)); + } + } + } + + private static AuthResult getErrorResult(String errorMessage) { + return new AuthResult(new Status(CommonStatusCodes.ERROR, errorMessage), null, null); } } diff --git a/src/android/JWTAuthPlugin.java b/src/android/JWTAuthPlugin.java index c86063a..8e0ebca 100644 --- a/src/android/JWTAuthPlugin.java +++ b/src/android/JWTAuthPlugin.java @@ -4,36 +4,76 @@ import org.json.JSONArray; import org.json.JSONException; -import android.accounts.AccountManager; import android.app.Activity; -import android.content.Context; import android.content.Intent; +import android.support.annotation.NonNull; import android.widget.Toast; +import com.google.android.gms.common.api.ResultCallback; + import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; public class JWTAuthPlugin extends CordovaPlugin { private static String TAG = "JWTAuthPlugin"; - private static final int REQUEST_CODE_PICK_ACCOUNT = 1000; - private CallbackContext savedContext; + private GoogleAccountManagerAuth mgama; + private static final int RESOLVE_ERROR_CODE = 2000; @Override - public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException { + public boolean execute(String action, JSONArray data, final CallbackContext callbackContext) throws JSONException { + mgama = new GoogleAccountManagerAuth(cordova.getActivity()); if (action.equals("getUserEmail")) { - Context ctxt = cordova.getActivity(); - String userEmail = UserProfile.getInstance(ctxt).getUserEmail(); - callbackContext.success(userEmail); + Activity ctxt = cordova.getActivity(); + AuthPendingResult result = mgama.getUserEmail(); + result.setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull AuthResult authResult) { + if (authResult.getStatus().isSuccess()) { + callbackContext.success(authResult.getEmail()); + } else { + callbackContext.error(authResult.getStatus().getStatusCode() + " : "+ + authResult.getStatus().getStatusMessage()); + } + } + }); return true; } else if (action.equals("signIn")) { + // NOTE: I tried setting the result callback to an instance of GoogleAccountManagerAuth, + // but it has to be a subclass of CordovaPlugin + // https://github.com/apache/cordova-android/blob/ad01d28351c13390aff4549258a0f06882df59f5/framework/src/org/apache/cordova/CordovaInterface.java#L49 cordova.setActivityResultCallback(this); - savedContext = callbackContext; // This will not actually return anything - instead we will get a callback in onActivityResult - new GoogleAccountManagerAuth(cordova.getActivity(), REQUEST_CODE_PICK_ACCOUNT).getUserName(); + AuthPendingResult result = mgama.uiSignIn(); + result.setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull AuthResult authResult) { + if (authResult.getStatus().isSuccess()) { + Toast.makeText(cordova.getActivity(), authResult.getEmail(), + Toast.LENGTH_SHORT).show(); + cordova.setActivityResultCallback(null); + callbackContext.success(authResult.getEmail()); + } else { + callbackContext.error(authResult.getStatus().getStatusCode() + " : "+ + authResult.getStatus().getStatusMessage()); + } + } + }); return true; } else if (action.equals("getJWT")) { - Context ctxt = cordova.getActivity(); - String token = GoogleAccountManagerAuth.getServerToken(ctxt, edu.berkeley.eecs.emission.cordova.jwtauth.UserProfile.getInstance(ctxt).getUserEmail()); - callbackContext.success(token); + Activity ctxt = cordova.getActivity(); + AuthPendingResult result = mgama.getServerToken(); + result.setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull AuthResult authResult) { + if (authResult.getStatus().isSuccess()) { + callbackContext.success(authResult.getToken()); + } else { + callbackContext.error(authResult.getStatus().getStatusCode() + " : "+ + authResult.getStatus().getStatusMessage()); + } + // TODO: Figure out how to handle pending status codes here + // Would be helpful if I could actually generate some to test :) + } + }); return true; } else { return false; @@ -43,18 +83,6 @@ public boolean execute(String action, JSONArray data, CallbackContext callbackCo @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(cordova.getActivity(), TAG, "requestCode = " + requestCode + " resultCode = " + resultCode); - if (requestCode == REQUEST_CODE_PICK_ACCOUNT) { - if (resultCode == Activity.RESULT_OK) { - String userEmail = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); - Context ctxt = cordova.getActivity(); - Toast.makeText(ctxt, userEmail, Toast.LENGTH_SHORT).show(); - UserProfile.getInstance(ctxt).setUserEmail(userEmail); - UserProfile.getInstance(ctxt).setGoogleAuthDone(true); - cordova.setActivityResultCallback(null); - savedContext.success(userEmail); - } else { - savedContext.error("Request code = "+resultCode); - } - } + mgama.onActivityResult(requestCode, resultCode, data); } } diff --git a/src/android/UserProfile.java b/src/android/UserProfile.java index 7f0524c..4ca4d46 100644 --- a/src/android/UserProfile.java +++ b/src/android/UserProfile.java @@ -115,27 +115,7 @@ public void setUserEmail(String userEmail) { this.userEmail = userEmail; saveToFile(); } - public boolean isGoogleAuthDone() { - if (userEmail == null) { - loadFromFile(); - } - return googleAuthDone; - } - public void setGoogleAuthDone(boolean googleAuthDone) { - this.googleAuthDone = googleAuthDone; - saveToFile(); - } - public boolean isLinkWithMovesDone() { - if (userEmail == null) { - loadFromFile(); - } - return linkWithMovesDone; - } - public void setLinkWithMovesDone(boolean linkWithMovesDone) { - this.linkWithMovesDone = linkWithMovesDone; - saveToFile(); - } - + public synchronized static UserProfile getInstance(Context context) { if (theInstance == null) { theInstance = new UserProfile(context); From 268d52e46bc206ddf2b11be0f671da0c268c56e1 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Sat, 19 Aug 2017 19:37:12 -0700 Subject: [PATCH 03/14] Create a factory and an protocol for auth token creation And switch the existing google sign-in auth completion handler to implement that protocol. All external references now use the factory directly. Almost there! --- src/ios/AuthCompletionHandler.h | 10 ++++---- src/ios/AuthCompletionHandler.m | 3 +-- src/ios/AuthTokenCreationFactory.h | 16 +++++++++++++ src/ios/AuthTokenCreationFactory.m | 28 ++++++++++++++++++++++ src/ios/AuthTokenCreator.h | 38 ++++++++++++++++++++++++++++++ src/ios/BEMJWTAuth.m | 14 ++++++----- 6 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 src/ios/AuthTokenCreationFactory.h create mode 100644 src/ios/AuthTokenCreationFactory.m create mode 100644 src/ios/AuthTokenCreator.h diff --git a/src/ios/AuthCompletionHandler.h b/src/ios/AuthCompletionHandler.h index cbc3601..c569b05 100644 --- a/src/ios/AuthCompletionHandler.h +++ b/src/ios/AuthCompletionHandler.h @@ -7,16 +7,16 @@ // #import +#import "AuthTokenCreator.h" -typedef void (^AuthResultCallback)(NSString *,NSError*); - -// @interface AuthCompletionHandler : NSObject -@interface AuthCompletionHandler : NSObject +@interface AuthCompletionHandler : NSObject +(AuthCompletionHandler*) sharedInstance; + @property (atomic, retain) UIViewController* viewController; +/* // Background refresh (no UI) // This is commented out because we want people to call the methods that // return results directly, so that we can mock them for easier development @@ -35,6 +35,6 @@ typedef void (^AuthResultCallback)(NSString *,NSError*); - (void) uiSignIn:(AuthResultCallback)authResultCallback; // Background refresh (no UI) - +*/ @end diff --git a/src/ios/AuthCompletionHandler.m b/src/ios/AuthCompletionHandler.m index aae4b0e..45c121e 100644 --- a/src/ios/AuthCompletionHandler.m +++ b/src/ios/AuthCompletionHandler.m @@ -24,7 +24,6 @@ #define NOT_SIGNED_IN_CODE 1000 @interface AuthCompletionHandler () -// @property (atomic, retain) UIViewController* viewController; // @property (atomic, retain) alreadyPresenting; @end @@ -41,7 +40,7 @@ + (AuthCompletionHandler*)sharedInstance sharedInstance = [AuthCompletionHandler new]; GIDSignIn* signIn = [GIDSignIn sharedInstance]; - signIn.clientID = [[ConnectionSettings sharedInstance] getGoogleiOSClientID]; + signIn.clientID = [[ConnectionSettings sharedInstance] getClientID]; // client secret is no longer required for this client // signIn.serverClientID = [[ConnectionSettings sharedInstance] getGoogleiOSClientSecret]; signIn.delegate = sharedInstance; diff --git a/src/ios/AuthTokenCreationFactory.h b/src/ios/AuthTokenCreationFactory.h new file mode 100644 index 0000000..9b0470d --- /dev/null +++ b/src/ios/AuthTokenCreationFactory.h @@ -0,0 +1,16 @@ +// +// AuthTokenCreationFactory.h +// emission +// +// Created by Kalyanaraman Shankari on 8/19/17. +// +// + +#import +#import "AuthTokenCreator.h" + +@interface AuthTokenCreationFactory : NSObject + ++(id) getInstance; + +@end diff --git a/src/ios/AuthTokenCreationFactory.m b/src/ios/AuthTokenCreationFactory.m new file mode 100644 index 0000000..426c312 --- /dev/null +++ b/src/ios/AuthTokenCreationFactory.m @@ -0,0 +1,28 @@ +// +// AuthTokenCreationFactory.m +// emission +// +// Created by Kalyanaraman Shankari on 8/19/17. +// +// + +#import "AuthTokenCreationFactory.h" +#import "BEMConnectionSettings.h" +#import "AuthCompletionHandler.h" + +@implementation AuthTokenCreationFactory + ++(id) getInstance +{ + ConnectionSettings* settings = [ConnectionSettings sharedInstance]; + if ([settings.authMethod isEqual: @"google-signin-lib"]) { + return [AuthCompletionHandler sharedInstance]; + } else { + // Return google sign-in handler by default so that we know that + // this will never return null + return [AuthCompletionHandler sharedInstance]; + } +} + + +@end diff --git a/src/ios/AuthTokenCreator.h b/src/ios/AuthTokenCreator.h new file mode 100644 index 0000000..98f4526 --- /dev/null +++ b/src/ios/AuthTokenCreator.h @@ -0,0 +1,38 @@ +// +// AuthTokenCreator.h +// emission +// +// Created by Kalyanaraman Shankari on 8/19/17. +// +// Standard protocol that various auth implementations will implement +// + +#import +typedef void (^AuthResultCallback)(NSString *,NSError*); + +@protocol AuthTokenCreator + +@property (atomic, retain) UIViewController* viewController; + +// Background refresh (no UI) +// This is commented out because we want people to call the methods that +// return results directly, so that we can mock them for easier development +// - (void) getValidAuth:(AuthCompletionCallback) authCompletionCallback; + +// Handle the notification callback to complete the authentication +- (void) handleNotification:(NSNotification*) notification; + +// Register callback (either for +// - (void) registerCallback:(AuthCompletionCallback) authCompletionCallback; + +// Get token +- (void) getEmail:(AuthResultCallback)authResultCallback; +- (void) getJWT:(AuthResultCallback)authResultCallback; +- (void) getExpirationDate:(AuthResultCallback)authResultCallback; +- (void) uiSignIn:(AuthResultCallback)authResultCallback; + +// Background refresh (no UI) + + +@end + diff --git a/src/ios/BEMJWTAuth.m b/src/ios/BEMJWTAuth.m index ce3b904..2217620 100644 --- a/src/ios/BEMJWTAuth.m +++ b/src/ios/BEMJWTAuth.m @@ -1,7 +1,8 @@ #import "BEMJWTAuth.h" #import "LocalNotificationManager.h" #import "BEMConnectionSettings.h" -#import "AuthCompletionHandler.h" +#import "AuthTokenCreationFactory.h" +#import "AuthTokenCreator.h" #import "BEMBuiltinUserCache.h" @interface BEMJWTAuth () @@ -14,7 +15,8 @@ @implementation BEMJWTAuth: CDVPlugin - (void)pluginInitialize { [LocalNotificationManager addNotification:@"BEMJWTAuth:pluginInitialize singleton -> initialize completion handler"]; - AuthCompletionHandler* authHandler = [AuthCompletionHandler sharedInstance]; + + id authHandler = [AuthTokenCreationFactory getInstance]; authHandler.viewController = self.viewController; [authHandler getJWT:^(NSString *token, NSError *error) { if (token == NULL) { @@ -47,7 +49,7 @@ - (void)getUserEmail:(CDVInvokedUrlCommand*)command // an error if the user did not exist. But the existing behavior is that it returns the // message OK with result = NULL if the user does not exist. // Maintaining that backwards compatible behavior for now... - [[AuthCompletionHandler sharedInstance] getEmail:^(NSString *userEmail, NSError *error) { + [[AuthTokenCreationFactory getInstance] getEmail:^(NSString *userEmail, NSError *error) { if (userEmail != NULL) { CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK @@ -74,7 +76,7 @@ - (void)getUserEmail:(CDVInvokedUrlCommand*)command - (void)signIn:(CDVInvokedUrlCommand*)command { @try { - [[AuthCompletionHandler sharedInstance] uiSignIn:[self getCallbackForCommand:command]]; + [[AuthTokenCreationFactory getInstance] uiSignIn:[self getCallbackForCommand:command]]; } @catch (NSException *exception) { NSString* msg = [NSString stringWithFormat: @"While getting user email, error %@", exception]; @@ -88,7 +90,7 @@ - (void)signIn:(CDVInvokedUrlCommand*)command - (void)getJWT:(CDVInvokedUrlCommand*)command { @try { - [[AuthCompletionHandler sharedInstance] getJWT:[self getCallbackForCommand:command]]; + [[AuthTokenCreationFactory getInstance] getJWT:[self getCallbackForCommand:command]]; } @catch (NSException *exception) { NSString* msg = [NSString stringWithFormat: @"While getting JWT, error %@", exception]; @@ -119,7 +121,7 @@ -(AuthResultCallback) getCallbackForCommand:(CDVInvokedUrlCommand*)command - (void)applicationLaunchedWithUrl:(NSNotification*)notification { - [[AuthCompletionHandler sharedInstance] handleNotification:notification]; + [[AuthTokenCreationFactory getInstance] handleNotification:notification]; } @end From 847905deba2480df36d741e1a47f84af96bde3f7 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Sat, 19 Aug 2017 21:56:50 -0700 Subject: [PATCH 04/14] Create a factory and an protocol for auth token creation And switch the existing google sign-in auth completion handler to implement that protocol. All external references now use the factory directly. Almost there! These are the android changes --- src/android/AuthTokenCreationFactory.java | 22 ++++++++++++++++++++++ src/android/AuthTokenCreator.java | 23 +++++++++++++++++++++++ src/android/GoogleAccountManagerAuth.java | 21 ++++++++------------- src/android/JWTAuthPlugin.java | 13 ++++++------- 4 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 src/android/AuthTokenCreationFactory.java create mode 100644 src/android/AuthTokenCreator.java diff --git a/src/android/AuthTokenCreationFactory.java b/src/android/AuthTokenCreationFactory.java new file mode 100644 index 0000000..595e2ca --- /dev/null +++ b/src/android/AuthTokenCreationFactory.java @@ -0,0 +1,22 @@ +package edu.berkeley.eecs.emission.cordova.jwtauth; + +import android.content.Context; + +import edu.berkeley.eecs.emission.cordova.connectionsettings.ConnectionSettings; + +/** + * Created by shankari on 8/19/17. + * + * Factory to generate AuthTokenCreator instances based on the connection settings + */ + +public class AuthTokenCreationFactory { + public static AuthTokenCreator getInstance(Context ctxt) { + String authMethod = ConnectionSettings.getAuthMethod(ctxt); + if ("google-authutil".equals(authMethod)) { + return new GoogleAccountManagerAuth(ctxt); + } else { + throw new RuntimeException("No auth creator found for method "+authMethod); + } + } +} diff --git a/src/android/AuthTokenCreator.java b/src/android/AuthTokenCreator.java new file mode 100644 index 0000000..9096e4c --- /dev/null +++ b/src/android/AuthTokenCreator.java @@ -0,0 +1,23 @@ +package edu.berkeley.eecs.emission.cordova.jwtauth; + +import android.content.Intent; +import java.net.URL; + +/** + * Created by shankari on 8/19/17. + * + * Interface that defines the methods that all auth handlers must define. + */ + +public interface AuthTokenCreator { + // Method to sign in via the UI + public AuthPendingResult uiSignIn(); + + // Method to retrieve signed-in user information + // Result is only guaranteed to have requested information filled in + public AuthPendingResult getUserEmail(); + public AuthPendingResult getServerToken(); + + // Callback to get the signin information, if provided through activity result + public void onActivityResult(int requestCode, int resultCode, Intent data); +} diff --git a/src/android/GoogleAccountManagerAuth.java b/src/android/GoogleAccountManagerAuth.java index 69b0674..62d2275 100644 --- a/src/android/GoogleAccountManagerAuth.java +++ b/src/android/GoogleAccountManagerAuth.java @@ -23,7 +23,7 @@ import android.content.Intent; import android.provider.Settings; -public class GoogleAccountManagerAuth { +class GoogleAccountManagerAuth implements AuthTokenCreator { private static final int REQUEST_CODE_PICK_ACCOUNT = 1000; private static final int REQUEST_CODE_GET_TOKEN = 1001; public static String TAG = "GoogleAccountManagerAuth"; @@ -35,22 +35,17 @@ public class GoogleAccountManagerAuth { // This has to be a class instance instead of a singleton like in // iOS because we are not supposed to store contexts in static variables // singleton pattern has static GoogleAccountManagerAuth -> mCtxt - public GoogleAccountManagerAuth(Activity activity) { - mCtxt = activity; - mActivity = activity; - } - - public GoogleAccountManagerAuth(Context ctxt) { + GoogleAccountManagerAuth(Context ctxt) { mCtxt = ctxt; + if (mCtxt instanceof Activity) { + mActivity = (Activity)mCtxt; + } } /* * This just invokes the account chooser. The chosen username is returned - * as a callback to the passed in activity. Since the activity is different - * for native and cordova, we don't handle the callback here. Instead, the - * expectation is that the activity will set the username in the user profile. - * It is the activity's responsibility to do this. The LIBRARY WILL NOT DO - * IT. + * as a callback to the passed in activity, which then calls onActivityResult here + * to set the username. */ public AuthPendingResult uiSignIn() { @@ -97,7 +92,7 @@ public void onClick(DialogInterface dialog, int which) { /* * BEGIN: Calls to get the data - * Going to configure these with listeners in order to support background operations + * Going to configure these with background (pending) results in order to support background operations * It's really kind of amazing that GoogleAuthUtil doesn't enforce that, and the new * GoogleSignIn code probably will */ diff --git a/src/android/JWTAuthPlugin.java b/src/android/JWTAuthPlugin.java index 8e0ebca..52002a7 100644 --- a/src/android/JWTAuthPlugin.java +++ b/src/android/JWTAuthPlugin.java @@ -15,15 +15,15 @@ public class JWTAuthPlugin extends CordovaPlugin { private static String TAG = "JWTAuthPlugin"; - private GoogleAccountManagerAuth mgama; + private AuthTokenCreator tokenCreator; private static final int RESOLVE_ERROR_CODE = 2000; @Override public boolean execute(String action, JSONArray data, final CallbackContext callbackContext) throws JSONException { - mgama = new GoogleAccountManagerAuth(cordova.getActivity()); + tokenCreator = AuthTokenCreationFactory.getInstance(cordova.getActivity()); if (action.equals("getUserEmail")) { Activity ctxt = cordova.getActivity(); - AuthPendingResult result = mgama.getUserEmail(); + AuthPendingResult result = tokenCreator.getUserEmail(); result.setResultCallback(new ResultCallback() { @Override public void onResult(@NonNull AuthResult authResult) { @@ -42,7 +42,7 @@ public void onResult(@NonNull AuthResult authResult) { // https://github.com/apache/cordova-android/blob/ad01d28351c13390aff4549258a0f06882df59f5/framework/src/org/apache/cordova/CordovaInterface.java#L49 cordova.setActivityResultCallback(this); // This will not actually return anything - instead we will get a callback in onActivityResult - AuthPendingResult result = mgama.uiSignIn(); + AuthPendingResult result = tokenCreator.uiSignIn(); result.setResultCallback(new ResultCallback() { @Override public void onResult(@NonNull AuthResult authResult) { @@ -59,8 +59,7 @@ public void onResult(@NonNull AuthResult authResult) { }); return true; } else if (action.equals("getJWT")) { - Activity ctxt = cordova.getActivity(); - AuthPendingResult result = mgama.getServerToken(); + AuthPendingResult result = tokenCreator.getServerToken(); result.setResultCallback(new ResultCallback() { @Override public void onResult(@NonNull AuthResult authResult) { @@ -83,6 +82,6 @@ public void onResult(@NonNull AuthResult authResult) { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(cordova.getActivity(), TAG, "requestCode = " + requestCode + " resultCode = " + resultCode); - mgama.onActivityResult(requestCode, resultCode, data); + tokenCreator.onActivityResult(requestCode, resultCode, data); } } From 30ad1d10b6f3c3358c2c4e987233e029f8d70f95 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Sat, 19 Aug 2017 22:00:15 -0700 Subject: [PATCH 05/14] Add new created files to the plugin spec Files represent the creator and creator factory interfaces from 847905deba2480df36d741e1a47f84af96bde3f7 and 268d52e46bc206ddf2b11be0f671da0c268c56e1 --- plugin.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugin.xml b/plugin.xml index 4be33ca..b3777f9 100644 --- a/plugin.xml +++ b/plugin.xml @@ -49,6 +49,8 @@ + + @@ -66,9 +68,12 @@ + + + From 08952ed5c5202ea9b8db4d1144830169cd698f3f Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Sun, 20 Aug 2017 02:02:51 -0700 Subject: [PATCH 06/14] Some more cleanup of the creator interface The main difference is that instead of passing the activity (on android) or the viewcontroller (on iOS), we pass the plugin as an argument to the token creator, *for the signIn case*. For anything other than the sign in case, we don't actually need any UI stuff, so this allows us to avoid the awkward instanceof check for activity on android, and the equally awkward need to remember to set the view controller on iOS. And passing in the plugin also allows us to set activity result callback from the google-specific implementation instead of the generic plugin (we may not use the activity callbacks for all implementations) and makes it easier to implement the dev implementation that needs to execute javascript. Includes minor fix to the way that files are copied over. --- plugin.xml | 2 +- src/android/AuthTokenCreator.java | 7 +++-- src/android/GoogleAccountManagerAuth.java | 36 ++++++++++++++--------- src/android/JWTAuthPlugin.java | 14 ++++----- src/ios/AuthCompletionHandler.h | 3 -- src/ios/AuthCompletionHandler.m | 10 ++++--- src/ios/AuthTokenCreator.h | 8 ++--- src/ios/BEMJWTAuth.m | 3 +- 8 files changed, 44 insertions(+), 39 deletions(-) diff --git a/plugin.xml b/plugin.xml index b3777f9..1621a56 100644 --- a/plugin.xml +++ b/plugin.xml @@ -73,7 +73,7 @@ - + diff --git a/src/android/AuthTokenCreator.java b/src/android/AuthTokenCreator.java index 9096e4c..8c868ad 100644 --- a/src/android/AuthTokenCreator.java +++ b/src/android/AuthTokenCreator.java @@ -1,7 +1,7 @@ package edu.berkeley.eecs.emission.cordova.jwtauth; import android.content.Intent; -import java.net.URL; +import org.apache.cordova.CordovaPlugin; /** * Created by shankari on 8/19/17. @@ -11,7 +11,7 @@ public interface AuthTokenCreator { // Method to sign in via the UI - public AuthPendingResult uiSignIn(); + public AuthPendingResult uiSignIn(CordovaPlugin plugin); // Method to retrieve signed-in user information // Result is only guaranteed to have requested information filled in @@ -20,4 +20,7 @@ public interface AuthTokenCreator { // Callback to get the signin information, if provided through activity result public void onActivityResult(int requestCode, int resultCode, Intent data); + + // Callback to get the signin information, if provided through a custom URL + public void onNewIntent(Intent intent); } diff --git a/src/android/GoogleAccountManagerAuth.java b/src/android/GoogleAccountManagerAuth.java index 62d2275..567bb99 100644 --- a/src/android/GoogleAccountManagerAuth.java +++ b/src/android/GoogleAccountManagerAuth.java @@ -23,12 +23,14 @@ import android.content.Intent; import android.provider.Settings; +import org.apache.cordova.CordovaPlugin; + class GoogleAccountManagerAuth implements AuthTokenCreator { private static final int REQUEST_CODE_PICK_ACCOUNT = 1000; private static final int REQUEST_CODE_GET_TOKEN = 1001; public static String TAG = "GoogleAccountManagerAuth"; - private Activity mActivity; + private CordovaPlugin mPlugin; private Context mCtxt; private AuthPendingResult mAuthPending; @@ -37,9 +39,6 @@ class GoogleAccountManagerAuth implements AuthTokenCreator { // singleton pattern has static GoogleAccountManagerAuth -> mCtxt GoogleAccountManagerAuth(Context ctxt) { mCtxt = ctxt; - if (mCtxt instanceof Activity) { - mActivity = (Activity)mCtxt; - } } /* @@ -47,8 +46,9 @@ class GoogleAccountManagerAuth implements AuthTokenCreator { * as a callback to the passed in activity, which then calls onActivityResult here * to set the username. */ - - public AuthPendingResult uiSignIn() { + @Override + public AuthPendingResult uiSignIn(CordovaPlugin plugin) { + mPlugin = plugin; try { String[] accountTypes = new String[]{"com.google"}; @@ -66,12 +66,12 @@ public AuthPendingResult uiSignIn() { // invoked will not be the one in this class, but the one in the original context. // In our current flow, that is the one in the MainActivity mAuthPending = new AuthPendingResult(); - if (mActivity == null) { - AuthResult result = new AuthResult(new Status(CommonStatusCodes.DEVELOPER_ERROR, "Context instead of activity while signing in"), null, null); - mAuthPending.setResult(result); - } else { - mActivity.startActivityForResult(intent, REQUEST_CODE_PICK_ACCOUNT); - } + // NOTE: I tried setting the result callback to an instance of GoogleAccountManagerAuth, + // but it has to be a subclass of CordovaPlugin + // https://github.com/apache/cordova-android/blob/ad01d28351c13390aff4549258a0f06882df59f5/framework/src/org/apache/cordova/CordovaInterface.java#L49 + mPlugin.cordova.setActivityResultCallback(mPlugin); + // This will not actually return anything - instead we will get a callback in onActivityResult + mPlugin.cordova.getActivity().startActivityForResult(intent, REQUEST_CODE_PICK_ACCOUNT); return mAuthPending; } catch (ActivityNotFoundException e) { // If the user does not have a google account, then @@ -96,7 +96,7 @@ public void onClick(DialogInterface dialog, int which) { * It's really kind of amazing that GoogleAuthUtil doesn't enforce that, and the new * GoogleSignIn code probably will */ - + @Override public AuthPendingResult getUserEmail() { AuthPendingResult authPending = new AuthPendingResult(); AuthResult result = new AuthResult( @@ -107,6 +107,7 @@ public AuthPendingResult getUserEmail() { return authPending; } + @Override public AuthPendingResult getServerToken() { AuthPendingResult authPending = new AuthPendingResult(); try { @@ -145,12 +146,14 @@ public AuthPendingResult getServerToken() { */ // Similar to handleNotification on iOS - + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE_PICK_ACCOUNT) { if (resultCode == Activity.RESULT_OK) { String userEmail = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); UserProfile.getInstance(mCtxt).setUserEmail(userEmail); + mPlugin.cordova.setActivityResultCallback(null); + mPlugin = null; AuthResult result = new AuthResult( new Status(CommonStatusCodes.SUCCESS), userEmail, @@ -162,6 +165,11 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } } + @Override + public void onNewIntent(Intent intent) { + Log.d(mCtxt, TAG, "in google auth code, onIntent("+intent.getDataString()+" called, ignoring"); + } + private static AuthResult getErrorResult(String errorMessage) { return new AuthResult(new Status(CommonStatusCodes.ERROR, errorMessage), null, null); } diff --git a/src/android/JWTAuthPlugin.java b/src/android/JWTAuthPlugin.java index 52002a7..5ed0388 100644 --- a/src/android/JWTAuthPlugin.java +++ b/src/android/JWTAuthPlugin.java @@ -37,19 +37,13 @@ public void onResult(@NonNull AuthResult authResult) { }); return true; } else if (action.equals("signIn")) { - // NOTE: I tried setting the result callback to an instance of GoogleAccountManagerAuth, - // but it has to be a subclass of CordovaPlugin - // https://github.com/apache/cordova-android/blob/ad01d28351c13390aff4549258a0f06882df59f5/framework/src/org/apache/cordova/CordovaInterface.java#L49 - cordova.setActivityResultCallback(this); - // This will not actually return anything - instead we will get a callback in onActivityResult - AuthPendingResult result = tokenCreator.uiSignIn(); + AuthPendingResult result = tokenCreator.uiSignIn(this); result.setResultCallback(new ResultCallback() { @Override public void onResult(@NonNull AuthResult authResult) { if (authResult.getStatus().isSuccess()) { Toast.makeText(cordova.getActivity(), authResult.getEmail(), Toast.LENGTH_SHORT).show(); - cordova.setActivityResultCallback(null); callbackContext.success(authResult.getEmail()); } else { callbackContext.error(authResult.getStatus().getStatusCode() + " : "+ @@ -84,4 +78,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(cordova.getActivity(), TAG, "requestCode = " + requestCode + " resultCode = " + resultCode); tokenCreator.onActivityResult(requestCode, resultCode, data); } + + @Override + public void onNewIntent(Intent intent) { + Log.d(cordova.getActivity(), TAG, "onNewIntent(" + intent.getDataString() + ")"); + tokenCreator.onNewIntent(intent); + } } diff --git a/src/ios/AuthCompletionHandler.h b/src/ios/AuthCompletionHandler.h index c569b05..94a2826 100644 --- a/src/ios/AuthCompletionHandler.h +++ b/src/ios/AuthCompletionHandler.h @@ -13,9 +13,6 @@ +(AuthCompletionHandler*) sharedInstance; - -@property (atomic, retain) UIViewController* viewController; - /* // Background refresh (no UI) // This is commented out because we want people to call the methods that diff --git a/src/ios/AuthCompletionHandler.m b/src/ios/AuthCompletionHandler.m index 45c121e..80018be 100644 --- a/src/ios/AuthCompletionHandler.m +++ b/src/ios/AuthCompletionHandler.m @@ -15,6 +15,7 @@ #import "BEMConnectionSettings.h" #import "BEMConstants.h" #import "LocalNotificationManager.h" +#import #import @@ -24,7 +25,7 @@ #define NOT_SIGNED_IN_CODE 1000 @interface AuthCompletionHandler () -// @property (atomic, retain) alreadyPresenting; + @property (atomic, retain) CDVPlugin* mPlugin; @end @implementation AuthCompletionHandler @@ -150,8 +151,9 @@ -(AuthCompletionCallback) getRedirectedCallback:(AuthResultCallback)redirCallbac // BEGIN: UI interaction -- (void) uiSignIn:(AuthResultCallback)authResultCallback +- (void) uiSignIn:(AuthResultCallback)authResultCallback withPlugin:(CDVPlugin*) plugin { + self.mPlugin = plugin; [self registerCallback:[self getRedirectedCallback:authResultCallback withRetValue:^NSString *(GIDGoogleUser *user) { return user.profile.email; @@ -161,12 +163,12 @@ - (void) uiSignIn:(AuthResultCallback)authResultCallback -(void) signIn:(GIDSignIn*)signIn presentViewController:(UIViewController *)loginScreen { - [self.viewController presentViewController:loginScreen animated:YES completion:NULL]; + [self.mPlugin.viewController presentViewController:loginScreen animated:YES completion:NULL]; } -(void) signIn:(GIDSignIn*)signIn dismissViewController:(UIViewController *)loginScreen { - [self.viewController dismissViewControllerAnimated:YES completion:NULL]; + [self.mPlugin.viewController dismissViewControllerAnimated:YES completion:NULL]; } // END: UI interaction diff --git a/src/ios/AuthTokenCreator.h b/src/ios/AuthTokenCreator.h index 98f4526..02b48b0 100644 --- a/src/ios/AuthTokenCreator.h +++ b/src/ios/AuthTokenCreator.h @@ -8,12 +8,11 @@ // #import +#import typedef void (^AuthResultCallback)(NSString *,NSError*); @protocol AuthTokenCreator -@property (atomic, retain) UIViewController* viewController; - // Background refresh (no UI) // This is commented out because we want people to call the methods that // return results directly, so that we can mock them for easier development @@ -22,14 +21,11 @@ typedef void (^AuthResultCallback)(NSString *,NSError*); // Handle the notification callback to complete the authentication - (void) handleNotification:(NSNotification*) notification; -// Register callback (either for -// - (void) registerCallback:(AuthCompletionCallback) authCompletionCallback; - // Get token - (void) getEmail:(AuthResultCallback)authResultCallback; - (void) getJWT:(AuthResultCallback)authResultCallback; - (void) getExpirationDate:(AuthResultCallback)authResultCallback; -- (void) uiSignIn:(AuthResultCallback)authResultCallback; +- (void) uiSignIn:(AuthResultCallback)authResultCallback withPlugin:(CDVPlugin*) plugin; // Background refresh (no UI) diff --git a/src/ios/BEMJWTAuth.m b/src/ios/BEMJWTAuth.m index 2217620..fc6f801 100644 --- a/src/ios/BEMJWTAuth.m +++ b/src/ios/BEMJWTAuth.m @@ -17,7 +17,6 @@ - (void)pluginInitialize [LocalNotificationManager addNotification:@"BEMJWTAuth:pluginInitialize singleton -> initialize completion handler"]; id authHandler = [AuthTokenCreationFactory getInstance]; - authHandler.viewController = self.viewController; [authHandler getJWT:^(NSString *token, NSError *error) { if (token == NULL) { NSDictionary* introDoneResult = [[BuiltinUserCache database] getLocalStorage:@"intro_done" withMetadata:NO]; @@ -76,7 +75,7 @@ - (void)getUserEmail:(CDVInvokedUrlCommand*)command - (void)signIn:(CDVInvokedUrlCommand*)command { @try { - [[AuthTokenCreationFactory getInstance] uiSignIn:[self getCallbackForCommand:command]]; + [[AuthTokenCreationFactory getInstance] uiSignIn:[self getCallbackForCommand:command] withPlugin:self]; } @catch (NSException *exception) { NSString* msg = [NSString stringWithFormat: @"While getting user email, error %@", exception]; From f9b82d8e8a9e8583cce613cf37583e4e68fa0526 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Sun, 20 Aug 2017 08:17:19 -0700 Subject: [PATCH 07/14] Rename AuthCompletionHandler to reflect that it is a google-specific implementation `AuthCompletionHandler` is a really generic name for something that is really the google-specific implementation. Rename to match the android code, specially when we're reinstalling the plugin anyway --- plugin.xml | 4 ++-- src/ios/{AuthCompletionHandler.h => GoogleSigninAuth.h} | 0 src/ios/{AuthCompletionHandler.m => GoogleSigninAuth.m} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/ios/{AuthCompletionHandler.h => GoogleSigninAuth.h} (100%) rename src/ios/{AuthCompletionHandler.m => GoogleSigninAuth.m} (100%) diff --git a/plugin.xml b/plugin.xml index 1621a56..c2b360a 100644 --- a/plugin.xml +++ b/plugin.xml @@ -67,12 +67,12 @@ - + - + diff --git a/src/ios/AuthCompletionHandler.h b/src/ios/GoogleSigninAuth.h similarity index 100% rename from src/ios/AuthCompletionHandler.h rename to src/ios/GoogleSigninAuth.h diff --git a/src/ios/AuthCompletionHandler.m b/src/ios/GoogleSigninAuth.m similarity index 100% rename from src/ios/AuthCompletionHandler.m rename to src/ios/GoogleSigninAuth.m From a2f008a11f6be0e8e336f113682bf45da69fe179 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Sun, 20 Aug 2017 08:17:45 -0700 Subject: [PATCH 08/14] Fix headers and class names to match the rename We renamed `AuthCompletionHandler` to `GoogleSigninAuth` in f9b82d8e8a9e8583cce613cf37583e4e68fa0526. This fixes the code to return use it properly. Checking in as separate commit to capture the `git mv` properly --- src/ios/AuthTokenCreationFactory.h | 1 - src/ios/AuthTokenCreationFactory.m | 6 +++--- src/ios/GoogleSigninAuth.h | 27 +++------------------------ src/ios/GoogleSigninAuth.m | 24 ++++++++++++------------ 4 files changed, 18 insertions(+), 40 deletions(-) diff --git a/src/ios/AuthTokenCreationFactory.h b/src/ios/AuthTokenCreationFactory.h index 9b0470d..fe6f9d5 100644 --- a/src/ios/AuthTokenCreationFactory.h +++ b/src/ios/AuthTokenCreationFactory.h @@ -7,7 +7,6 @@ // #import -#import "AuthTokenCreator.h" @interface AuthTokenCreationFactory : NSObject diff --git a/src/ios/AuthTokenCreationFactory.m b/src/ios/AuthTokenCreationFactory.m index 426c312..ba9f57a 100644 --- a/src/ios/AuthTokenCreationFactory.m +++ b/src/ios/AuthTokenCreationFactory.m @@ -8,7 +8,7 @@ #import "AuthTokenCreationFactory.h" #import "BEMConnectionSettings.h" -#import "AuthCompletionHandler.h" +#import "GoogleSigninAuth.h" @implementation AuthTokenCreationFactory @@ -16,11 +16,11 @@ @implementation AuthTokenCreationFactory { ConnectionSettings* settings = [ConnectionSettings sharedInstance]; if ([settings.authMethod isEqual: @"google-signin-lib"]) { - return [AuthCompletionHandler sharedInstance]; + return [GoogleSigninAuth sharedInstance]; } else { // Return google sign-in handler by default so that we know that // this will never return null - return [AuthCompletionHandler sharedInstance]; + return [GoogleSigninAuth sharedInstance]; } } diff --git a/src/ios/GoogleSigninAuth.h b/src/ios/GoogleSigninAuth.h index 94a2826..e4f44ba 100644 --- a/src/ios/GoogleSigninAuth.h +++ b/src/ios/GoogleSigninAuth.h @@ -1,5 +1,5 @@ // -// AuthCompletionHandler.h +// GoogleSigninAuth.h // E-Mission // // Created by Kalyanaraman Shankari on 4/3/14. @@ -9,29 +9,8 @@ #import #import "AuthTokenCreator.h" -@interface AuthCompletionHandler : NSObject +@interface GoogleSigninAuth: NSObject -+(AuthCompletionHandler*) sharedInstance; - -/* -// Background refresh (no UI) -// This is commented out because we want people to call the methods that -// return results directly, so that we can mock them for easier development -// - (void) getValidAuth:(AuthCompletionCallback) authCompletionCallback; - -// Handle the notification callback to complete the authentication -- (void) handleNotification:(NSNotification*) notification; - -// Register callback (either for -// - (void) registerCallback:(AuthCompletionCallback) authCompletionCallback; - -// Get token -- (void) getEmail:(AuthResultCallback)authResultCallback; -- (void) getJWT:(AuthResultCallback)authResultCallback; -- (void) getExpirationDate:(AuthResultCallback)authResultCallback; -- (void) uiSignIn:(AuthResultCallback)authResultCallback; - -// Background refresh (no UI) -*/ ++(GoogleSigninAuth*) sharedInstance; @end diff --git a/src/ios/GoogleSigninAuth.m b/src/ios/GoogleSigninAuth.m index 80018be..41bb1d8 100644 --- a/src/ios/GoogleSigninAuth.m +++ b/src/ios/GoogleSigninAuth.m @@ -1,5 +1,5 @@ // -// AuthCompletionHandler.m +// GoogleSigninAuth.m // E-Mission // // Created by Kalyanaraman Shankari on 4/3/14. @@ -11,7 +11,7 @@ So we go to our familiar listener pattern */ -#import "AuthCompletionHandler.h" +#import "GoogleSigninAuth.h" #import "BEMConnectionSettings.h" #import "BEMConstants.h" #import "LocalNotificationManager.h" @@ -19,26 +19,26 @@ #import -typedef void (^AuthCompletionCallback)(GIDGoogleUser *,NSError*); +typedef void (^GoogleSigninCallback)(GIDGoogleUser *,NSError*); typedef NSString* (^ProfileRetValue)(GIDGoogleUser *); #define NOT_SIGNED_IN_CODE 1000 -@interface AuthCompletionHandler () +@interface GoogleSigninAuth () @property (atomic, retain) CDVPlugin* mPlugin; @end -@implementation AuthCompletionHandler +@implementation GoogleSigninAuth -static AuthCompletionHandler *sharedInstance; +static GoogleSigninAuth *sharedInstance; NSString* const STATUS_KEY = @"success"; NSString* const BEMJWTAuthComplete = @"BEMJWTAuthComplete"; -+ (AuthCompletionHandler*)sharedInstance ++ (GoogleSigninAuth*)sharedInstance { if (sharedInstance == nil) { - NSLog(@"creating new AuthCompletionHandler sharedInstance"); - sharedInstance = [AuthCompletionHandler new]; + NSLog(@"creating new GoogleSigninAuth sharedInstance"); + sharedInstance = [GoogleSigninAuth new]; GIDSignIn* signIn = [GIDSignIn sharedInstance]; signIn.clientID = [[ConnectionSettings sharedInstance] getClientID]; @@ -58,13 +58,13 @@ + (AuthCompletionHandler*)sharedInstance * that the client can use to show the sign in screen. */ -- (void) getValidAuth:(AuthCompletionCallback) authCompletionCallback +- (void) getValidAuth:(GoogleSigninCallback) authCompletionCallback { [self registerCallback:authCompletionCallback]; [[GIDSignIn sharedInstance] signInSilently]; } -- (void) registerCallback:(AuthCompletionCallback)authCompletionCallback +- (void) registerCallback:(GoogleSigninCallback)authCompletionCallback { // pattern from `addObserverForName` docs // https://developer.apple.com/reference/foundation/nsnotificationcenter/1411723-addobserverforname @@ -135,7 +135,7 @@ - (void) getExpirationDate:(AuthResultCallback)authResultCallback }]]; } --(AuthCompletionCallback) getRedirectedCallback:(AuthResultCallback)redirCallback withRetValue:(ProfileRetValue) retValueFunctor +-(GoogleSigninCallback) getRedirectedCallback:(AuthResultCallback)redirCallback withRetValue:(ProfileRetValue) retValueFunctor { return ^(GIDGoogleUser *user, NSError *error) { if (error == NULL) { From 7f33c0106f9a584453bc59ed3ceaf9e62d26d4a1 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Sun, 20 Aug 2017 08:39:26 -0700 Subject: [PATCH 09/14] Restore incorrectly removed header obvs I need the header, because otherwise the return type of `getInstance` is unknown. Was removed in a2f008a11f6be0e8e336f113682bf45da69fe179 --- src/ios/AuthTokenCreationFactory.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ios/AuthTokenCreationFactory.h b/src/ios/AuthTokenCreationFactory.h index fe6f9d5..9b0470d 100644 --- a/src/ios/AuthTokenCreationFactory.h +++ b/src/ios/AuthTokenCreationFactory.h @@ -7,6 +7,7 @@ // #import +#import "AuthTokenCreator.h" @interface AuthTokenCreationFactory : NSObject From cdb5ac163803640855bbfac4b9e1da462ba76979 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Mon, 21 Aug 2017 13:05:52 -0700 Subject: [PATCH 10/14] Fix the case in which the app was not launched as part of the auth callback In that case, `tokenCreator` is null and we shouldn't crash. Instead, we can just ignore the call to the custom URL. --- src/android/JWTAuthPlugin.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/android/JWTAuthPlugin.java b/src/android/JWTAuthPlugin.java index 5ed0388..60d544f 100644 --- a/src/android/JWTAuthPlugin.java +++ b/src/android/JWTAuthPlugin.java @@ -6,6 +6,7 @@ import android.app.Activity; import android.content.Intent; +import android.net.Uri; import android.support.annotation.NonNull; import android.widget.Toast; @@ -82,6 +83,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { @Override public void onNewIntent(Intent intent) { Log.d(cordova.getActivity(), TAG, "onNewIntent(" + intent.getDataString() + ")"); - tokenCreator.onNewIntent(intent); + if (tokenCreator != null) { + tokenCreator.onNewIntent(intent); + } else { + Log.i(cordova.getActivity(), TAG, "tokenCreator = null, ignoring intent"+intent.getDataString()); + } } } From 4d5eeccba3d18ae03518e805193bbb9188941851 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Mon, 21 Aug 2017 13:07:40 -0700 Subject: [PATCH 11/14] Add support for a simple dev mode authentication In this, the user simply enters their email address. If the email address matches one that was used to load data before this, the account is re-used, which makes it easier to review and re-test data. It also ensure that sessions are maintained across app restarts without logging in every time, which makes debugging a lot easier. And finally, it is an example of a javascript-based auth callback, which should make it much easier to integrate with other auth systems if they don't already have nice libraries to do it for us. NOTE: the javascript stuff is extremely kludgy and ugly-looking, and I will clean it up shortly after some more experimentation. --- src/android/AuthTokenCreationFactory.java | 7 +- src/android/DummyDevAuth.java | 126 ++++++++++++++++++++++ src/ios/AuthTokenCreationFactory.m | 10 +- src/ios/DummyDevAuth.h | 14 +++ src/ios/DummyDevAuth.m | 119 ++++++++++++++++++++ 5 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 src/android/DummyDevAuth.java create mode 100644 src/ios/DummyDevAuth.h create mode 100644 src/ios/DummyDevAuth.m diff --git a/src/android/AuthTokenCreationFactory.java b/src/android/AuthTokenCreationFactory.java index 595e2ca..d726b86 100644 --- a/src/android/AuthTokenCreationFactory.java +++ b/src/android/AuthTokenCreationFactory.java @@ -15,8 +15,13 @@ public static AuthTokenCreator getInstance(Context ctxt) { String authMethod = ConnectionSettings.getAuthMethod(ctxt); if ("google-authutil".equals(authMethod)) { return new GoogleAccountManagerAuth(ctxt); + } else if ("dummy-dev".equals(authMethod)) { + return new DummyDevAuth(ctxt); } else { - throw new RuntimeException("No auth creator found for method "+authMethod); + // Return dummy dev sign-in handler by default so that: + // - we know that this will never return null + // - dev users can start working without any configuration stuff + return new DummyDevAuth(ctxt); } } } diff --git a/src/android/DummyDevAuth.java b/src/android/DummyDevAuth.java new file mode 100644 index 0000000..31a22b3 --- /dev/null +++ b/src/android/DummyDevAuth.java @@ -0,0 +1,126 @@ +package edu.berkeley.eecs.emission.cordova.jwtauth; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.TextUtils; +import android.webkit.JavascriptInterface; +import android.webkit.URLUtil; + +import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; + +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.Status; + +import org.apache.cordova.CordovaPlugin; + +/** + * Created by shankari on 8/21/17. + * + * Implementation of the dummy dev auth code to allow developers to login with multiple user IDs + * for testing + to provide another exemplar of logging in properly :) + */ + +class DummyDevAuth implements AuthTokenCreator { + private CordovaPlugin mPlugin; + private AuthPendingResult mAuthPending; + private Context mCtxt; + + private static final String TAG = "DummyDevAuth"; + private static final String METHOD_PARAM_KEY = "method"; + private static final String TOKEN_PARAM_KEY = "token"; + private static final String EXPECTED_HOST = "auth"; + private static final String EXPECTED_METHOD = "dummy-dev"; + + // This has to be a class instance instead of a singleton like in + // iOS because we are not supposed to store contexts in static variables + // singleton pattern has static GoogleAccountManagerAuth -> mCtxt + DummyDevAuth(Context ctxt) { + mCtxt = ctxt; + } + + @Override + public AuthPendingResult uiSignIn(CordovaPlugin plugin) { + this.mAuthPending = new AuthPendingResult(); + this.mPlugin = plugin; + + String[] devJSScriptLines = {"var email = window.prompt('Dev mode: Enter email', '')", + // "window.alert('email = '+email)", + "var callbackURL = 'emission://auth?method=dummy-dev&token='+email", + // "window.alert('callbackURL = '+callbackURL)", + "var callbackWindow = cordova.InAppBrowser.open(callbackURL, '_system')", + "callbackWindow.addEventListener('loadstart', function(event) {var protocol = event.url.substring(0, event.url.indexOf('://')); if (protocol == 'emission') { setTimeout(callbackWindow.close(), 5000);}})", + "callbackWindow.addEventListener('loaderr', function(event) {alert('Error '+event.message+' loading '+event.url); callbackWindow.close();})" + }; + final String devJSScript = TextUtils.join(";\n", devJSScriptLines); + Log.d(mCtxt, TAG, "About to execute script"); + Log.d(mCtxt, TAG, devJSScript); + final CordovaPlugin finalPlugin = plugin; + plugin.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + finalPlugin.webView.loadUrl("javascript:"+devJSScript); + } + }); + return mAuthPending; + } + + @Override + public AuthPendingResult getUserEmail() { + return readStoredUserEmail(mCtxt); + } + + @Override + public AuthPendingResult getServerToken() { + // For the dummy-dev case, the token is the user email + return readStoredUserEmail(mCtxt); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.d(mCtxt, TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data.getDataString()); + Log.i(mCtxt, TAG, "onActivityResult unused in `dummy-dev, ignoring call..."); + } + + @Override + public void onNewIntent(Intent intent) { + Log.i(mCtxt, TAG, "received intent with url "+intent.getDataString()); + Uri launchUrl = intent.getData(); + if (EXPECTED_HOST.equals(launchUrl.getHost())) { + String method = launchUrl.getQueryParameter(METHOD_PARAM_KEY); + if(method != null && EXPECTED_METHOD.equals(method)) { + String userEmail = launchUrl.getQueryParameter(TOKEN_PARAM_KEY); + if (userEmail != null) { + UserProfile.getInstance(mCtxt).setUserEmail(userEmail); + AuthResult authResult = new AuthResult( + new Status(CommonStatusCodes.SUCCESS), + userEmail, + userEmail); + mAuthPending.setResult(authResult); + } else { + Log.i(mCtxt, TAG, "Received uri with query params = "+launchUrl.getQuery() + +" key "+TOKEN_PARAM_KEY+" missing, ignoring"); + } + } else { + Log.i(mCtxt, TAG, "Received uri with query params = "+launchUrl.getQuery() + +" key "+METHOD_PARAM_KEY+" missing or incorrect, ignoring"); + } + } else { + // TODO: Should I return the callback with an error? It is possible that there are multiple URLs being handled, + // in which case we should not return prematurely, but wait for _our_ URL to complete. But if we don't look + // for it, we may be stuck overever. + Log.i(mCtxt, TAG, "Received uri with feature = "+launchUrl.getHost() + +" expected "+EXPECTED_HOST+" ignoring "); + } + } + + private AuthPendingResult readStoredUserEmail(Context ctxt) { + AuthPendingResult authPending = new AuthPendingResult(); + String userEmail = UserProfile.getInstance(ctxt).getUserEmail(); + AuthResult result = new AuthResult( + new Status(CommonStatusCodes.SUCCESS), + userEmail, userEmail); + authPending.setResult(result); + return authPending; + } +} diff --git a/src/ios/AuthTokenCreationFactory.m b/src/ios/AuthTokenCreationFactory.m index ba9f57a..e4eb949 100644 --- a/src/ios/AuthTokenCreationFactory.m +++ b/src/ios/AuthTokenCreationFactory.m @@ -9,6 +9,7 @@ #import "AuthTokenCreationFactory.h" #import "BEMConnectionSettings.h" #import "GoogleSigninAuth.h" +#import "DummyDevAuth.h" @implementation AuthTokenCreationFactory @@ -17,10 +18,13 @@ @implementation AuthTokenCreationFactory ConnectionSettings* settings = [ConnectionSettings sharedInstance]; if ([settings.authMethod isEqual: @"google-signin-lib"]) { return [GoogleSigninAuth sharedInstance]; + } else if ([settings.authMethod isEqual: @"dummy-dev"]) { + return [DummyDevAuth sharedInstance]; } else { - // Return google sign-in handler by default so that we know that - // this will never return null - return [GoogleSigninAuth sharedInstance]; + // Return dummy dev sign-in handler by default so that: + // - we know that this will never return null + // - dev users can start working without any configuration stuff + return [DummyDevAuth sharedInstance]; } } diff --git a/src/ios/DummyDevAuth.h b/src/ios/DummyDevAuth.h new file mode 100644 index 0000000..438640d --- /dev/null +++ b/src/ios/DummyDevAuth.h @@ -0,0 +1,14 @@ +// +// DummyDevAuth.h +// emission +// +// Created by Kalyanaraman Shankari on 8/20/17. +// +// + +#import +#import "AuthTokenCreator.h" + +@interface DummyDevAuth : NSObject ++ (DummyDevAuth*) sharedInstance; +@end diff --git a/src/ios/DummyDevAuth.m b/src/ios/DummyDevAuth.m new file mode 100644 index 0000000..a3890e6 --- /dev/null +++ b/src/ios/DummyDevAuth.m @@ -0,0 +1,119 @@ +// +// DummyDevAuth.m +// emission +// +// Created by Kalyanaraman Shankari on 8/20/17. +// +// + +#import "DummyDevAuth.h" +#import "LocalNotificationManager.h" + +#define EXPECTED_METHOD @"dummy-dev" +#define EXPECTED_HOST @"auth" +#define METHOD_PARAM_KEY @"method" +#define TOKEN_PARAM_KEY @"token" +#define STORAGE_KEY @"dev-auth" + +@interface DummyDevAuth () +@property (atomic, copy) AuthResultCallback mResultCallback; +@end + +@implementation DummyDevAuth + +static DummyDevAuth *sharedInstance; + ++(DummyDevAuth*) sharedInstance { + if (sharedInstance == NULL) { + NSLog(@"creating new DummyDevAuth sharedInstance"); + sharedInstance = [DummyDevAuth new]; + } + return sharedInstance; +} + +-(void)handleNotification:(NSNotification *)notification +{ + NSURL* url = [notification object]; + [LocalNotificationManager + addNotification:[NSString + stringWithFormat:@"in handleNotification, received url = %@", url]]; + + NSURLComponents* urlComp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + NSString* feature = [urlComp host]; + [LocalNotificationManager + addNotification:[NSString stringWithFormat:@"in handleNotification, feature = %@", feature]]; + + if ([feature isEqualToString:EXPECTED_HOST]) { + NSArray *queryItems = [urlComp queryItems]; + NSURLQueryItem* methodParam = queryItems[0]; + NSURLQueryItem* tokenParam = queryItems[1]; + + if (([methodParam.name isEqualToString:METHOD_PARAM_KEY]) && ([methodParam.value isEqualToString:EXPECTED_METHOD])) { + // For the dummy-dev method name + if ([tokenParam.name isEqualToString:TOKEN_PARAM_KEY]) { + NSString* userName = tokenParam.value; + [LocalNotificationManager addNotification: + [NSString stringWithFormat:@"in handleNotification, received userName %@", + userName]]; + [[NSUserDefaults standardUserDefaults] setObject:userName forKey:STORAGE_KEY]; + self.mResultCallback(userName, NULL); + } else { + [LocalNotificationManager addNotification: + [NSString stringWithFormat:@"in handleNotification, tokenParam key = %@, expected %@, ignoring...", + tokenParam.name, TOKEN_PARAM_KEY]]; + } + } else { + [LocalNotificationManager addNotification: + [NSString stringWithFormat:@"in handleNotification, methodParam = %@, expected %@, ignoring...", + methodParam, @{METHOD_PARAM_KEY: EXPECTED_METHOD}]]; + // TODO: Should I return the callback with an error? It is possible that there are multiple URLs being handled, + // in which case we should not return prematurely, but wait for _our_ URL to complete. But if we don't look + // for it, we may be stuck overever. + } + } else { + [LocalNotificationManager addNotification: + [NSString stringWithFormat:@"in handleNotification, recived URL for feature %@, expected %@, ignoring...", + feature, @"auth"]]; + } +} + +- (NSString*) getStoredUsername +{ + return [[NSUserDefaults standardUserDefaults] objectForKey:STORAGE_KEY]; +} + +- (void) getEmail:(AuthResultCallback) authResultCallback +{ + authResultCallback([self getStoredUsername], NULL); +} + +- (void) getJWT:(AuthResultCallback) authResultCallback +{ + // For the dummy-dev method, token = username + authResultCallback([self getStoredUsername], NULL); +} + +- (void) getExpirationDate:(AuthResultCallback) authResultCallback +{ + authResultCallback(@"never", NULL); +} + +- (void) uiSignIn:(AuthResultCallback)authResultCallback withPlugin:(CDVPlugin *)plugin +{ + self.mResultCallback = authResultCallback; + NSArray* devJSScriptLines = @[@"var email = window.prompt('Dev mode: Enter email', '')", + // @"window.alert('email = '+email)", + @"var callbackURL = 'emission://auth?method=dummy-dev&token='+email", + // @"window.alert('callbackURL = '+callbackURL)", + @"var callbackWindow = cordova.InAppBrowser.open(callbackURL, '_system')", + @"callbackWindow.addEventListener('loadstart', function(event) {var protocol = event.url.substring(0, event.url.indexOf('://')); if (protocol == 'emission') { setTimeout(callbackWindow.close(), 5000);}})", + @"callbackWindow.addEventListener('loaderr', function(event) {alert('Error '+event.message+' loading '+event.url); callbackWindow.close();})" + ]; + NSString* devJSScript = [devJSScriptLines componentsJoinedByString:@";\n"]; + [LocalNotificationManager addNotification:@"About to execute script"]; + [LocalNotificationManager addNotification:devJSScript]; + [plugin.commandDelegate evalJs:devJSScript]; + +} + +@end From 9d39fe53f2714ad02494fdf99702f7ec805f5e0c Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Mon, 21 Aug 2017 16:20:26 -0700 Subject: [PATCH 12/14] Add the files for the new dummy auth code to the plugin This should have been part of 4d5eeccba3d18ae03518e805193bbb9188941851 but I forgot :( --- plugin.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugin.xml b/plugin.xml index c2b360a..74a8c9c 100644 --- a/plugin.xml +++ b/plugin.xml @@ -46,6 +46,7 @@ + @@ -68,11 +69,13 @@ + + From 34318a6386074588a549330e5f9c49ee9fb9ab1b Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Mon, 21 Aug 2017 16:22:23 -0700 Subject: [PATCH 13/14] Move the javascript for the dev launch to the javascript file - Unifies the code between ios and android - Makes it easier to see (wrapping everything in " for the code strings and then ' for the strings in the code was getting confusing) - This also makes it easier to see multi-line code - We can also attach the debugger to the function, which might be useful as this gets more complex --- src/android/DummyDevAuth.java | 13 ++----------- src/ios/DummyDevAuth.m | 10 +--------- www/jwtauth.js | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/android/DummyDevAuth.java b/src/android/DummyDevAuth.java index 31a22b3..3f44eef 100644 --- a/src/android/DummyDevAuth.java +++ b/src/android/DummyDevAuth.java @@ -44,17 +44,8 @@ public AuthPendingResult uiSignIn(CordovaPlugin plugin) { this.mAuthPending = new AuthPendingResult(); this.mPlugin = plugin; - String[] devJSScriptLines = {"var email = window.prompt('Dev mode: Enter email', '')", - // "window.alert('email = '+email)", - "var callbackURL = 'emission://auth?method=dummy-dev&token='+email", - // "window.alert('callbackURL = '+callbackURL)", - "var callbackWindow = cordova.InAppBrowser.open(callbackURL, '_system')", - "callbackWindow.addEventListener('loadstart', function(event) {var protocol = event.url.substring(0, event.url.indexOf('://')); if (protocol == 'emission') { setTimeout(callbackWindow.close(), 5000);}})", - "callbackWindow.addEventListener('loaderr', function(event) {alert('Error '+event.message+' loading '+event.url); callbackWindow.close();})" - }; - final String devJSScript = TextUtils.join(";\n", devJSScriptLines); - Log.d(mCtxt, TAG, "About to execute script"); - Log.d(mCtxt, TAG, devJSScript); + final String devJSScript = "window.cordova.plugins.BEMJWTAuth.launchDevAuth()"; + Log.d(mCtxt, TAG, "About to execute script: "+devJSScript); final CordovaPlugin finalPlugin = plugin; plugin.cordova.getActivity().runOnUiThread(new Runnable() { @Override diff --git a/src/ios/DummyDevAuth.m b/src/ios/DummyDevAuth.m index a3890e6..73b2416 100644 --- a/src/ios/DummyDevAuth.m +++ b/src/ios/DummyDevAuth.m @@ -101,15 +101,7 @@ - (void) getExpirationDate:(AuthResultCallback) authResultCallback - (void) uiSignIn:(AuthResultCallback)authResultCallback withPlugin:(CDVPlugin *)plugin { self.mResultCallback = authResultCallback; - NSArray* devJSScriptLines = @[@"var email = window.prompt('Dev mode: Enter email', '')", - // @"window.alert('email = '+email)", - @"var callbackURL = 'emission://auth?method=dummy-dev&token='+email", - // @"window.alert('callbackURL = '+callbackURL)", - @"var callbackWindow = cordova.InAppBrowser.open(callbackURL, '_system')", - @"callbackWindow.addEventListener('loadstart', function(event) {var protocol = event.url.substring(0, event.url.indexOf('://')); if (protocol == 'emission') { setTimeout(callbackWindow.close(), 5000);}})", - @"callbackWindow.addEventListener('loaderr', function(event) {alert('Error '+event.message+' loading '+event.url); callbackWindow.close();})" - ]; - NSString* devJSScript = [devJSScriptLines componentsJoinedByString:@";\n"]; + NSString* devJSScript = @"window.cordova.plugins.BEMJWTAuth.launchDevAuth()"; [LocalNotificationManager addNotification:@"About to execute script"]; [LocalNotificationManager addNotification:devJSScript]; [plugin.commandDelegate evalJs:devJSScript]; diff --git a/www/jwtauth.js b/www/jwtauth.js index ab4e944..3eaa847 100644 --- a/www/jwtauth.js +++ b/www/jwtauth.js @@ -22,9 +22,37 @@ var JWTAuth = { }); }, - getJWT: function(successCallback, errorCallback) { - exec(successCallback, errorCallback, "JWTAuth", "getJWT", []); + getJWT: function() { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "JWTAuth", "getJWT", []); + }); }, + + launchDevAuth: function() { + var email = window.prompt('Dummy dev mode: Enter email', ''); + // window.alert('email = '+email); + var callbackURL = 'emission://auth?method=dummy-dev&token='+email; + // window.alert('callbackURL = '+callbackURL); + var callbackWindow = cordova.InAppBrowser.open(callbackURL, '_system'); + // Make sure we close the window automatically + // Note that we can't do this on loadend + // https://github.com/e-mission/cordova-jwt-auth/issues/17#issuecomment-323645807 + callbackWindow.addEventListener('loadstart', function(event) { + // Do we even need to check the protocol? This is a callback on *this* + // IAB instance. And we don't load anything in this IAB + // instance other than the emission URL + // Let's leave it in for now since it doesn't hurt anything + var protocol = event.url.substring(0, event.url.indexOf('://')); + if (protocol == 'emission') { + setTimeout(callbackWindow.close(), 5000); + } + }); + + callbackWindow.addEventListener('loaderr', function(event) { + alert('Error '+event.message+' loading '+event.url); + callbackWindow.close(); + }); + } } module.exports = JWTAuth; From f25bf22b86af8d90c8b5f2bc7deaede94e1074f8 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Mon, 21 Aug 2017 16:39:18 -0700 Subject: [PATCH 14/14] Bump up version number to reflect major changes to this version --- plugin.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.xml b/plugin.xml index 74a8c9c..a919d9a 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.1.0"> JWTAuth Get the user email and associated JWT tokens from both native