RemoteAuthService.kt

/*
 * 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
     * [RemoteAuthClient.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.responseUrl)
                putInt(KEY_ERROR_CODE, response.errorCode)
                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) }
                )
            }
        }
    }
}