Introduce authentication service.
This service allows to request oauth2 token. The only current
implementation is for Android.
Also the implementation is currently using a single project for
authentication.
R=ppi@chromium.org, tonyg@chromium.org
BUG=https://github.com/domokit/mojo/issues/94
Review URL: https://codereview.chromium.org/1116653002
diff --git a/services/BUILD.gn b/services/BUILD.gn
index 86ee32f..d0a487b 100644
--- a/services/BUILD.gn
+++ b/services/BUILD.gn
@@ -30,6 +30,7 @@
if (is_android) {
deps += [
"//services/android:java_handler",
+ "//services/authentication",
"//services/location",
"//services/sensors",
]
diff --git a/services/android/intent_receiver.mojom b/services/android/intent_receiver.mojom
index a61df69..caa2f3f 100644
--- a/services/android/intent_receiver.mojom
+++ b/services/android/intent_receiver.mojom
@@ -12,13 +12,23 @@
// caller can then transform this intent into a PendingIntent using
// |PendingIntent#getService| and send it to another android application.
// Whenever the pending intent is executed, the receiver will be called with
- // the content of the received intent. To be noted: this will fail if the
- // received intent is active (contains either a Binder or a file descriptor).
- RegisterReceiver(IntentReceiver receiver) => (array<uint8> intent);
+ // the content of the received intent.
+ // To be noted, this will fail if the received intent is active (contains
+ // either a Binder or a file descriptor).
+ RegisterIntentReceiver(IntentReceiver receiver) => (array<uint8>? intent);
+
+ // This method takes an |IntentReceiver| and returns a serialized intent.
+ // The serialized intent can be deserialized using an android parcel. The
+ // caller can then add an intent it wants the system to send using
+ // |Activity#startActivityForResult| and then send it using
+ // |Context#startService|. Whenever the started activity sends a result, the
+ // receiver will be called with the content of the received intent. If the
+ // activity is cancelled, the receiver will be closed.
+ RegisterActivityResultReceiver(IntentReceiver receiver) =>
+ (array<uint8>? intent);
};
-// Receiver interface, to be used with
-// |IntentReceiverManager.RegisterReceiver|.
+// Receiver interface, to be used with |IntentReceiverManager|.
interface IntentReceiver {
OnIntent(array<uint8> intent);
};
diff --git a/services/authentication/BUILD.gn b/services/authentication/BUILD.gn
new file mode 100644
index 0000000..146942c
--- /dev/null
+++ b/services/authentication/BUILD.gn
@@ -0,0 +1,33 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//mojo/public/tools/bindings/mojom.gni")
+
+if (is_android) {
+ import("//services/android/rules.gni")
+ import("//build/config/android/config.gni")
+
+ mojo_android_java_application("authentication") {
+ sources = [
+ "src/org/chromium/mojo/authentication/AuthenticationApp.java",
+ "src/org/chromium/mojo/authentication/AuthenticationServiceImpl.java",
+ ]
+
+ mojo_main = "org.chromium.mojo.authentication.AuthenticationApp"
+
+ deps = [
+ ":bindings_java",
+ "//mojo/public/interfaces/application:application_java",
+ "//mojo/public/java:application",
+ "//services/android:bindings_java",
+ "//third_party/android_tools:google_play_services_default_java",
+ ]
+ }
+}
+
+mojom("bindings") {
+ sources = [
+ "authentication.mojom",
+ ]
+}
diff --git a/services/authentication/authentication.mojom b/services/authentication/authentication.mojom
new file mode 100644
index 0000000..a69e7de
--- /dev/null
+++ b/services/authentication/authentication.mojom
@@ -0,0 +1,27 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[JavaPackage="org.chromium.mojo.authentication"]
+module authentication;
+
+// Interface to handle user identity and authentication tokens.
+// TODO(qsr): This API only handles google accounts at this time. It will need
+// to be extended to allow generic account manager on the platform.
+interface AuthenticationService {
+ // Requests a Google account to use. In case of success, error will be null.
+ // In case of error, username will be null and error will contain a
+ // description of the error.
+ SelectAccount() => (string? username, string? error);
+
+ // Requests an oauth2 token for the given Google account with the given
+ // scopes. In case of error, username will be null and error will contain a
+ // description of the error.
+ GetOAuth2Token(string username, array<string> scopes) =>
+ (string? token, string? error);
+
+ // Requests to clear a previously acquired token. This should be called when a
+ // token is refused by a server component before requesting a new token to
+ // clear the token from any cache.
+ ClearOAuth2Token(string token);
+};
diff --git a/services/authentication/src/org/chromium/mojo/authentication/AuthenticationApp.java b/services/authentication/src/org/chromium/mojo/authentication/AuthenticationApp.java
new file mode 100644
index 0000000..3737648
--- /dev/null
+++ b/services/authentication/src/org/chromium/mojo/authentication/AuthenticationApp.java
@@ -0,0 +1,70 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.mojo.authentication;
+
+import android.content.Context;
+
+import org.chromium.mojo.application.ApplicationConnection;
+import org.chromium.mojo.application.ApplicationDelegate;
+import org.chromium.mojo.application.ApplicationRunner;
+import org.chromium.mojo.application.ServiceFactoryBinder;
+import org.chromium.mojo.system.Core;
+import org.chromium.mojo.system.MessagePipeHandle;
+import org.chromium.mojom.mojo.Shell;
+
+/**
+ * Android service application implementing the AuthenticationService interface.
+ */
+public class AuthenticationApp implements ApplicationDelegate {
+ private final Context mContext;
+ private final Core mCore;
+ private Shell mShell;
+
+ public AuthenticationApp(Context context, Core core) {
+ mContext = context;
+ mCore = core;
+ }
+
+ /**
+ * @see ApplicationDelegate#initialize(Shell, String[], String)
+ */
+ @Override
+ public void initialize(Shell shell, String[] args, String url) {
+ mShell = shell;
+ }
+
+ /**
+ * @see ApplicationDelegate#configureIncomingConnection(ApplicationConnection)
+ */
+ @Override
+ public boolean configureIncomingConnection(final ApplicationConnection connection) {
+ connection.addService(new ServiceFactoryBinder<AuthenticationService>() {
+
+ @Override
+ public void bindNewInstanceToMessagePipe(MessagePipeHandle pipe) {
+ AuthenticationService.MANAGER.bind(new AuthenticationServiceImpl(mContext, mCore,
+ connection.getRequestorUrl(), mShell),
+ pipe);
+ }
+
+ @Override
+ public String getInterfaceName() {
+ return AuthenticationService.MANAGER.getName();
+ }
+ });
+ return true;
+ }
+
+ /**
+ * @see ApplicationDelegate#quit()
+ */
+ @Override
+ public void quit() {}
+
+ public static void mojoMain(
+ Context context, Core core, MessagePipeHandle applicationRequestHandle) {
+ ApplicationRunner.run(new AuthenticationApp(context, core), core, applicationRequestHandle);
+ }
+}
diff --git a/services/authentication/src/org/chromium/mojo/authentication/AuthenticationServiceImpl.java b/services/authentication/src/org/chromium/mojo/authentication/AuthenticationServiceImpl.java
new file mode 100644
index 0000000..90be06b
--- /dev/null
+++ b/services/authentication/src/org/chromium/mojo/authentication/AuthenticationServiceImpl.java
@@ -0,0 +1,195 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.mojo.authentication;
+
+import android.accounts.AccountManager;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Parcel;
+
+import com.google.android.gms.auth.GoogleAuthException;
+import com.google.android.gms.auth.GoogleAuthUtil;
+import com.google.android.gms.auth.UserRecoverableAuthException;
+import com.google.android.gms.common.AccountPicker;
+
+import org.chromium.mojo.application.ShellHelper;
+import org.chromium.mojo.bindings.SideEffectFreeCloseable;
+import org.chromium.mojo.intent.IntentReceiver;
+import org.chromium.mojo.intent.IntentReceiverManager;
+import org.chromium.mojo.intent.IntentReceiverManager.RegisterActivityResultReceiverResponse;
+import org.chromium.mojo.system.Core;
+import org.chromium.mojo.system.MojoException;
+import org.chromium.mojom.mojo.Shell;
+
+import java.io.IOException;
+
+/**
+ * Implementation of AuthenticationService from services/authentication/authentication.mojom
+ */
+public class AuthenticationServiceImpl
+ extends SideEffectFreeCloseable implements AuthenticationService {
+ /**
+ * An callback that takes a serialized intent, add the intent the shell needs to send and start
+ * the container intent.
+ */
+ private final class RegisterActivityResultReceiverCallback
+ implements RegisterActivityResultReceiverResponse {
+ /**
+ * The intent that the requesting application needs to be run by shell on its
+behalf.
+ */
+ private final Intent mIntent;
+
+ private RegisterActivityResultReceiverCallback(Intent intent) {
+ mIntent = intent;
+ }
+
+ /**
+ * @see RegisterActivityResultReceiverResponse#call(byte[])
+ */
+ @Override
+ public void call(byte[] serializedIntent) {
+ Intent trampolineIntent = bytesToIntent(serializedIntent);
+ trampolineIntent.putExtra("intent", mIntent);
+ mContext.startService(trampolineIntent);
+ }
+ }
+
+ private final Activity mContext;
+ private final String mConsumerURL;
+ private final IntentReceiverManager mIntentReceiverManager;
+
+ public AuthenticationServiceImpl(Context context, Core core, String consumerURL, Shell shell) {
+ mContext = (Activity) context;
+ mConsumerURL = consumerURL;
+ mIntentReceiverManager = ShellHelper.connectToService(
+ core, shell, "mojo:android_handler", IntentReceiverManager.MANAGER);
+ }
+
+ /**
+ * @see AuthenticationService#onConnectionError(MojoException)
+ */
+ @Override
+ public void onConnectionError(MojoException e) {}
+
+ /**
+ * @see AuthenticationService#getOAuth2Token(String, String[],
+ * AuthenticationService.GetOAuth2TokenResponse)
+ */
+ @Override
+ public void getOAuth2Token(
+ final String username, final String[] scopes, final GetOAuth2TokenResponse callback) {
+ if (scopes.length == 0) {
+ callback.call(null, "scopes cannot be empty");
+ return;
+ }
+ StringBuilder scope = new StringBuilder("oauth2:");
+ for (int i = 0; i < scopes.length; ++i) {
+ if (i > 0) {
+ scope.append(" ");
+ }
+ scope.append(scopes[i]);
+ }
+ try {
+ callback.call(GoogleAuthUtil.getToken(mContext, username, scope.toString()), null);
+ } catch (final UserRecoverableAuthException e) {
+ // If an error occurs that the user can recover from, this exception will contain the
+ // intent to run to allow the user to act. This will use the intent manager to start it.
+ mIntentReceiverManager.registerActivityResultReceiver(new IntentReceiver() {
+ GetOAuth2TokenResponse mPendingCallback = callback;
+
+ @Override
+ public void close() {
+ call(null, "User denied the request.");
+ }
+
+ @Override
+ public void onConnectionError(MojoException e) {
+ call(null, e.getMessage());
+ }
+
+ @Override
+ public void onIntent(byte[] bytes) {
+ if (mPendingCallback == null) {
+ return;
+ }
+ getOAuth2Token(username, scopes, mPendingCallback);
+ mPendingCallback = null;
+ }
+
+ private void call(String token, String error) {
+ if (mPendingCallback == null) {
+ return;
+ }
+ mPendingCallback.call(token, error);
+ mPendingCallback = null;
+ }
+ }, new RegisterActivityResultReceiverCallback(e.getIntent()));
+ return;
+ } catch (IOException | GoogleAuthException e) {
+ // Unrecoverable error.
+ callback.call(null, e.getMessage());
+ }
+ }
+
+ /**
+ * @see AuthenticationService#selectAccount(AuthenticationService.SelectAccountResponse)
+ */
+ @Override
+ public void selectAccount(final SelectAccountResponse callback) {
+ String[] accountTypes = new String[] {"com.google"};
+ String message = "Select an account to use with application: " + mConsumerURL;
+ Intent accountPickerIntent = AccountPicker.newChooseAccountIntent(
+ null, null, accountTypes, false, message, null, null, null);
+
+ mIntentReceiverManager.registerActivityResultReceiver(new IntentReceiver() {
+ SelectAccountResponse mPendingCallback = callback;
+
+ @Override
+ public void close() {
+ call(null, "User denied the request.");
+ }
+
+ @Override
+ public void onConnectionError(MojoException e) {
+ call(null, e.getMessage());
+ }
+
+ @Override
+ public void onIntent(byte[] bytes) {
+ Intent intent = bytesToIntent(bytes);
+ call(intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME), null);
+ }
+
+ private void call(String username, String error) {
+ if (mPendingCallback == null) {
+ return;
+ }
+ mPendingCallback.call(username, error);
+ mPendingCallback = null;
+ }
+ }, new RegisterActivityResultReceiverCallback(accountPickerIntent));
+ }
+
+ /**
+ * @see AuthenticationService#clearOAuth2Token(String)
+ */
+ @Override
+ public void clearOAuth2Token(String token) {
+ try {
+ GoogleAuthUtil.clearToken(mContext, token);
+ } catch (GoogleAuthException | IOException e) {
+ // Nothing to do.
+ }
+ }
+
+ private static Intent bytesToIntent(byte[] bytes) {
+ Parcel p = Parcel.obtain();
+ p.unmarshall(bytes, 0, bytes.length);
+ p.setDataPosition(0);
+ return Intent.CREATOR.createFromParcel(p);
+ }
+}
diff --git a/services/device_info/src/org/chromium/services/device_info/DeviceInfoService.java b/services/device_info/src/org/chromium/services/device_info/DeviceInfoService.java
index f7747b8..d0fe6ef 100644
--- a/services/device_info/src/org/chromium/services/device_info/DeviceInfoService.java
+++ b/services/device_info/src/org/chromium/services/device_info/DeviceInfoService.java
@@ -77,8 +77,7 @@
* @see ApplicationDelegate#configureIncomingConnection(String, ApplicationConnection)
*/
@Override
- public boolean configureIncomingConnection(
- final String requestorUrl, ApplicationConnection connection) {
+ public boolean configureIncomingConnection(ApplicationConnection connection) {
final DeviceInfo info = this;
connection.addService(new ServiceFactoryBinder<DeviceInfo>() {
@Override
diff --git a/services/location/src/org/chromium/services/location/LocationServiceApp.java b/services/location/src/org/chromium/services/location/LocationServiceApp.java
index 545b927..685b8fa 100644
--- a/services/location/src/org/chromium/services/location/LocationServiceApp.java
+++ b/services/location/src/org/chromium/services/location/LocationServiceApp.java
@@ -98,11 +98,10 @@
}
/**
- * @see ApplicationDelegate#configureIncomingConnection(String, ApplicationConnection)
+ * @see ApplicationDelegate#configureIncomingConnection(ApplicationConnection)
*/
@Override
- public boolean configureIncomingConnection(
- final String requestorUrl, ApplicationConnection connection) {
+ public boolean configureIncomingConnection(ApplicationConnection connection) {
connection.addService(new ServiceFactoryBinder<LocationService>() {
@Override
public void bindNewInstanceToMessagePipe(MessagePipeHandle pipe) {
diff --git a/services/sensors/src/org/chromium/mojo/sensors/Sensors.java b/services/sensors/src/org/chromium/mojo/sensors/Sensors.java
index 526d78f..b182a00 100644
--- a/services/sensors/src/org/chromium/mojo/sensors/Sensors.java
+++ b/services/sensors/src/org/chromium/mojo/sensors/Sensors.java
@@ -35,8 +35,7 @@
* @see ApplicationDelegate#configureIncomingConnection(String, ApplicationConnection)
*/
@Override
- public boolean configureIncomingConnection(
- final String requestorUrl, ApplicationConnection connection) {
+ public boolean configureIncomingConnection(ApplicationConnection connection) {
connection.addService(new ServiceFactoryBinder<SensorService>() {
@Override
public void bindNewInstanceToMessagePipe(MessagePipeHandle pipe) {