blob: dadd75ba8677d17b8e5a5ac5c5e4c9c154e97591 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.wear.phone.interactions.authentication
import android.annotation.SuppressLint
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Binder
import android.os.Bundle
import android.os.IBinder
import android.os.RemoteException
import android.support.wearable.authentication.IAuthenticationRequestCallback
import android.support.wearable.authentication.IAuthenticationRequestService
import androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.KEY_ERROR_CODE
import androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.KEY_PACKAGE_NAME
import androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.KEY_RESPONSE_URL
import java.security.SecureRandom
/**
* Interface for specifying how the service handles the remote auth requests.
*/
public interface RemoteAuthRequestHandler {
/**
* Whether the auth service is enabled, return false would give an early out by sending the
* 3p app a response with error code of ERROR_UNSUPPORTED
*/
public fun isAuthSupported(): Boolean
/**
* Handle the auth request by sending it to the phone.
* Typically, if the paired phone is not connected, send a response with error code of
* ERROR_PHONE_UNAVAILABLE; otherwise listening for the response from the phone and send it
* back to the 3p app.
*
* [RemoteAuthService.sendResponseToCallback] is provided for sending response back to the
* callback provided by the 3p app.
*
*/
public fun sendAuthRequest(
request: OAuthRequest,
packageNameAndRequestId: Pair<String, Int>
)
}
/*
* Extend this service class to trigger the handling of the remote auth requests, the
* RemoteAuthRequestHandler is specified when the service is bound, typically:
*
* class AuthenticationService : RemoteAuthService {
* override fun onBind(intent: Intent): IBinder {
* return onBind(
* intent,
* object : RemoteAuthRequestHandler {
* override fun isAuthSupported(): Boolean {...}
* override fun sendAuthRequest(...) {
* ...
* sendResponseToCallback(...)
* }
* })
* }
* }
*/
public abstract class RemoteAuthService : Service() {
public companion object {
@JvmStatic
private val callbacksByPackageNameAndRequestID:
MutableMap<Pair<String, Int>, IAuthenticationRequestCallback> = HashMap()
/**
* To be called by the child class to invoke the callback with Response
*/
@SuppressLint("DocumentExceptions")
@JvmStatic
public fun sendResponseToCallback(
response: OAuthResponse,
packageNameAndRequestId: Pair<String, Int>
) {
try {
callbacksByPackageNameAndRequestID[packageNameAndRequestId]?.onResult(
buildBundleFromResponse(response, packageNameAndRequestId.first)
)
callbacksByPackageNameAndRequestID.remove(packageNameAndRequestId)
} catch (e: RemoteException) {
throw e.cause!!
}
}
internal fun getCallback(packageNameAndRequestId: Pair<String, Int>):
IAuthenticationRequestCallback? =
callbacksByPackageNameAndRequestID[packageNameAndRequestId]
internal fun buildBundleFromResponse(response: OAuthResponse, packageName: String): Bundle =
Bundle().apply {
putParcelable(KEY_RESPONSE_URL, response.getResponseUrl())
putInt(KEY_ERROR_CODE, response.getErrorCode())
putString(KEY_PACKAGE_NAME, packageName)
}
}
private val secureRandom: SecureRandom = SecureRandom()
/**
* To be called by child class when implementing the [Service.onBind], provide the
* RemoteAuthRequestHandler and return the IBinder.
*/
protected fun onBind(
@Suppress("UNUSED_PARAMETER") intent: Intent,
remoteAuthRequestHandler: RemoteAuthRequestHandler
): IBinder = RemoteAuthServiceBinder(this, remoteAuthRequestHandler)
/**
* Implementation of [Service.onUnbind]
*/
public override fun onUnbind(intent: Intent): Boolean {
callbacksByPackageNameAndRequestID.clear()
return super.onUnbind(intent)
}
/**
* Allow the child class to override the default behavior of the package name verification.
*
* By default, we check the request's package name belongs to the requester's UID.
*/
protected open fun verifyPackageName(context: Context, requestPackageName: String?): Boolean {
val packagesForUID: Array<String>? =
context.packageManager.getPackagesForUid(Binder.getCallingUid())
return !(
requestPackageName.isNullOrEmpty() ||
packagesForUID.isNullOrEmpty() ||
!(packagesForUID.contains(requestPackageName))
)
}
internal inner class RemoteAuthServiceBinder(
private val context: Context,
private val remoteAuthRequestHandler: RemoteAuthRequestHandler
) : IAuthenticationRequestService.Stub() {
override fun getApiVersion(): Int = IAuthenticationRequestService.API_VERSION
/**
* @throws SecurityException
*/
override fun openUrl(
request: Bundle,
authenticationRequestCallback: IAuthenticationRequestCallback
) {
val packageName = request.getString(RemoteAuthClient.KEY_PACKAGE_NAME)
if (remoteAuthRequestHandler.isAuthSupported()) {
if (!verifyPackageName(context, packageName)) {
throw SecurityException("Failed to verify the Requester's package name")
}
val packageNameAndRequestId = Pair(packageName!!, secureRandom.nextInt())
callbacksByPackageNameAndRequestID[packageNameAndRequestId] =
authenticationRequestCallback
val requestUrl: Uri? = request.getParcelable(RemoteAuthClient.KEY_REQUEST_URL)
remoteAuthRequestHandler.sendAuthRequest(
OAuthRequest(packageName, requestUrl!!),
packageNameAndRequestId
)
} else {
authenticationRequestCallback.onResult(
Bundle().apply { putInt(KEY_ERROR_CODE, RemoteAuthClient.ERROR_UNSUPPORTED) }
)
}
}
}
}