androidkotlingoogle-drive-apigoogle-signinandroid-credential-manager

How to authenticate Google Sign In using CredentialManager and authorize to Google Drive API in Android?


I am trying to achieve the latest Google sign in criteria for authentication then authorize for Google Drive API.

I am calling requestGDriveSignInUpdated() method for a button click.

private fun requestGDriveSignInUpdated() {
        val credentialManager = CredentialManager.create(requireContext())
        val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
            .setFilterByAuthorizedAccounts(false)
            .setServerClientId(MY_WEB_CLIENT_ID)
            .build()

        val credentialRequest: GetCredentialRequest = GetCredentialRequest.Builder()
            .addCredentialOption(googleIdOption)
            .build()

        val cancellationSignal: CancellationSignal = CancellationSignal()
        cancellationSignal.setOnCancelListener {
            Log.d("/////", "Preparing credentials with Google was cancelled.")
            Toast.makeText(requireContext(), "Cancelled.", Toast.LENGTH_SHORT).show()
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            credentialManager.getCredentialAsync(
                OfflineMusicApp.instance,
                credentialRequest,
                cancellationSignal,
                Executors.newSingleThreadExecutor(),
                object : CredentialManagerCallback<GetCredentialResponse, GetCredentialException> {
                    override fun onResult(result: GetCredentialResponse) {
                        handleSignIn(result)
                    }

                    override fun onError(e: GetCredentialException) {
                        Log.d(TAG, "GetCredentialException: ${e.message}")
                    }
                }
            )
        }
    }

Then in the handleSignIn() method I get googleIdTokenCredential.

private fun handleSignIn(result: GetCredentialResponse) {
        when (val credential = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            result.credential
        } else {
            null
        }) {

            is CustomCredential -> {
                if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
                    try {
                        // I am not sure what to do with this googleIdTokenCredential
                        val googleIdTokenCredential = GoogleIdTokenCredential
                            .createFrom(credential.data)

                        Log.d(TAG, "Google id token account name: ${googleIdTokenCredential.displayName}")
                        requestGoogleDriveAuthorization()

                        // Send googleIdTokenCredential to your server for validation and authentication
                    } catch (e: GoogleIdTokenParsingException) {
                        Log.e(TAG, "Received an invalid google id token response", e)
                    }
                } else {
                    // Catch any unrecognized custom credential type here.
                    Log.e(TAG, "Unexpected type of credential")
                }
            }

            else -> {
                // Catch any unrecognized credential type here.
                Log.e(TAG, "Unexpected type of credential")
            }
        }
    }

But in the google documentation it says "Use googleIdTokenCredential and extract id to validate and authenticate on your server".

But I have no clue how to do that as there is no example code for it in the documentation.

In requestGoogleDriveAuthorization() method I tried to authorize Google Drive API using Google Identity Services but null pointer exception for null account is shown.

private fun requestGoogleDriveAuthorization() {
        val requestedScopes = listOf(Scope(DriveScopes.DRIVE_READONLY))
        val authorizationRequest = AuthorizationRequest.Builder()
            .setRequestedScopes(requestedScopes)
            .build()

        Identity.getAuthorizationClient(requireContext())
            .authorize(authorizationRequest)
            .addOnSuccessListener { authorizationResult ->
                if (authorizationResult.hasResolution()) {
                    val pendingIntent = authorizationResult.pendingIntent
                    try {
                        Log.d(TAG, "Start intent sender")
                        startIntentSenderForResult(
                            pendingIntent!!.intentSender,
                            REQUEST_AUTHORIZE,
                            null, 0, 0, 0, null
                        )
                    } catch (e: IntentSender.SendIntentException) {
                        Log.e(TAG, "Couldn't start Authorization UI: " + e.localizedMessage)
                    }
                } else {
                    Log.d(TAG, "Making GDrive client")
                    val account = authorizationResult.toGoogleSignInAccount()
                    if (account != null) {
                        Log.d(TAG, "Auth result account name: ${account.email}")
                        googleAccountCredential = getCredential(account)
                        lifecycleScope.launch {
                            GDriveClient.makeDriveBuilder(credential = googleAccountCredential)
                        }
                        startActivity(Intent(this.requireActivity(), TracksDownloadActivity::class.java).apply {
                            putExtra("signInOption", "googleDrive")
                        })
                    } else {
                        Log.e(TAG, "Authorization result account is null")
                    }
                }
            }
            .addOnFailureListener { e ->
                Log.e(TAG, "Failed to authorize", e)
            }
    }

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_AUTHORIZE) {
            val authorizationResult = Identity.getAuthorizationClient(requireContext()).getAuthorizationResultFromIntent(data)
            val account: GoogleSignInAccount? = authorizationResult.toGoogleSignInAccount()
            if (account != null) {
                googleAccountCredential = getCredential(account)
                lifecycleScope.launch {
                    GDriveClient.makeDriveBuilder(credential = googleAccountCredential)
                }
                startActivity(Intent(this.requireActivity(), TracksDownloadActivity::class.java).apply {
                    putExtra("signInOption", "googleDrive")
                })
            } else {
                Log.e(TAG, "onActivityResult: Authorization result account is null")
            }
        }
    }

private fun getCredential(account: GoogleSignInAccount): GoogleAccountCredential {
        return GoogleAccountCredential.usingOAuth2(
            requireContext(),
            listOf(DriveScopes.DRIVE_READONLY)
        ).apply {
            selectedAccount = account.account
        }
    }

object GDriveClient {
    private var drive: Drive? = null
    private val APP_NAME = "APP_NAME"
    private lateinit var googleCredentialToken: String

    suspend fun makeDriveBuilder(credential: GoogleAccountCredential) {
        withContext(Dispatchers.IO) {
            drive = Drive.Builder(
                NetHttpTransport(),
                GsonFactory(),
                credential
            ).setApplicationName(APP_NAME).build()
            googleCredentialToken = credential.token
        }
    }

    fun getDriveBuilder(): Drive? {
        return drive
    }

    fun getCredentialToken(): String {
        return googleCredentialToken
    }

}

I am quite confused about these errors. Can anyone help me out? What am I missing? Is my authentication properly done? How to resolve the null account exception problem?


Solution

  • There are many different pieces in your question so it is kind of hard to answer them all in one place; let me try anyway. First, in order to use the Authorization APIs, you don't necessarily need to first authenticate the user to your app. Of course, you can do that if user authentication into your app is also something that you want to do based on user's google account.

    Next, for the authentication part, when you get the success result back, you can extract the corresponding ID Token from the result. Usually, you'd want to extract some info/metadata from the ID Token and also do a (server-side) validation of the ID Token to make sure it was truly signed by Google (in order to trust that).

    ID token contains additional useful information, such as email address (you can get that from the result directly, if you want), the user's Display Name, an avatar for the use and a unique identifier for the user (it is recommended not to use the email address but instead use this unique identifier as the "key" for the user in your database). How you can do these can be a different post; if you need help there to extract the info, etc, then post a separate question and I'll try to answer that there.

    Then you want to call the authorization API with the desired scope(s). Since you're doing that right after authentication, the "default" account is set for your app on the device so that API uses that account for authorization (had you skipped the authentication, you would have seen an account selector first). Then your users get a chance to grant permissions to your app to access their data on, say, Drive (based on the scopes that you had requested). When that happens, you get an AuthorizationResult back from the usual callback. To move forward with that, you can do something like the following (I am assuming you want to access user's drive):

    import com.google.api.services.drive.Drive;
    import com.google.auth.http.HttpCredentialsAdapter;
    import com.google.auth.oauth2.AccessToken;
    import com.google.auth.oauth2.GoogleCredentials;
    ...
    
    GoogleCredentials credentials =
        GoogleCredentials.create(new AccessToken(authorizationResult.getAccessToken(), null));
    Drive service = new Drive.Builder(
        new NetHttpTransport(), new GsonFactory(), new HttpCredentialsAdapter(credentials))
                .setApplicationName("YOUR-APP-NAME")
                .build();
    

    You can then use the service object to access the Drive APIs, as documented in the Drive APIs documentation

    If you have further questions, please let us know.