Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to migrate from GoogleCredential to GoogleCredentials and still get access to People API?

Background

For an app I'm working on, which uses People API using credentials (user login). Once the user gives the credentials, I can access various Google APIs, such as People API. An example is one to get a list of contacts:

https://developers.google.com/people/api/rest/v1/people.connections/list

I've noticed that the class com.google.api.client.googleapis.auth.oauth2.GoogleCredential has become deprecated:

https://googleapis.dev/java/google-api-client/latest/com/google/api/client/googleapis/auth/oauth2/GoogleCredential.html

The problem

The app has old code that is based on some old G+ code (here) to reach contacts via the Google account. Here's a snippet of the most important part of it, which causes me trouble of migrating away from it:

object GoogleOuthHelper {
    @WorkerThread
    fun setUp(context: Context, serverAuthCode: String?): Services {
        val httpTransport: HttpTransport = NetHttpTransport()
        val jsonFactory = JacksonFactory.getDefaultInstance()
        // Redirect URL for web based applications. Can be empty too.
        val redirectUrl = "urn:ietf:wg:oauth:2.0:oob"
        // Exchange auth code for access token
        val tokenResponse = GoogleAuthorizationCodeTokenRequest(
            httpTransport, jsonFactory, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET,
            serverAuthCode, redirectUrl)
            .execute()
        // Then, create a GoogleCredential object using the tokens from GoogleTokenResponse
        val credential = GoogleCredential.Builder()
            .setClientSecrets(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)
            .setTransport(httpTransport)
            .setJsonFactory(jsonFactory)
            .build()
        val accessToken = tokenResponse.accessToken
        getDefaultSecuredSharedPreferences(context).edit()
            .putString(SecuredSharedPreferences.KEY__GOOGLE_ACCESS_TOKEN, accessToken).apply()
        credential.setFromTokenResponse(tokenResponse)
        val appPackageName = context.packageName
        val peopleServiceApi = PeopleService.Builder(httpTransport, jsonFactory, credential)
            .setApplicationName(appPackageName)
            .build()
        val peopleService = peopleServiceApi.people()
        val otherContactsService = peopleServiceApi.otherContacts()
        val contactGroups = peopleServiceApi.contactGroups()
        return Services(peopleService, otherContactsService, contactGroups)
    }

    class Services(
        /**https://developers.google.com/people/api/rest/v1/people*/
        val peopleService: PeopleService.People,
        /**https://developers.google.com/people/api/rest/v1/otherContacts*/
        val otherContactsService: OtherContacts,
        /**https://developers.google.com/people/api/rest/v1/contactGroups*/
        val contactGroups: ContactGroups)
}

The problem is even from the very beginning:

The class GoogleCredentials doesn't seem to accept anything I got above it for the GoogleCredential class.

To add more to it, this function takes "serverAuthCode" as parameter, which is from GoogleSignInAccount, but to get it, I need to use the deprecated GoogleApiClient class:

        fun prepareGoogleApiClient(someContext: Context): GoogleApiClient {
            val context = someContext.applicationContext ?: someContext
            val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestServerAuthCode(GOOGLE_CLIENT_ID)
                .requestEmail()
                .requestScopes(
                    Scope(PeopleServiceScopes.CONTACTS_READONLY),
                    Scope(PeopleServiceScopes.USERINFO_PROFILE),
                    Scope(PeopleServiceScopes.USER_EMAILS_READ),
                    Scope(PeopleServiceScopes.CONTACTS),
                    Scope(PeopleServiceScopes.CONTACTS_OTHER_READONLY)
                )
                .build()
            return GoogleApiClient.Builder(context)
                .addApi(Auth.GOOGLE_SIGN_IN_API, gso)
                .build()
        }

And this is what I do with it:

val connectionResult = googleApiClient!!.blockingConnect()
if (!connectionResult.isSuccess)
    return
val operation = Auth.GoogleSignInApi.silentSignIn(googleApiClient)
val googleSignInResult: GoogleSignInResult = operation.await()
val googleSignInAccount = googleSignInResult.signInAccount
//use googleSignInAccount.serverAuthCode in setUp() function above

Gradle file has these dependencies:

// https://mvnrepository.com/artifact/com.google.auth/google-auth-library-oauth2-http
implementation 'com.google.auth:google-auth-library-oauth2-http:0.26.0'
// https://github.com/googleapis/google-api-java-client-services/tree/master/clients/google-api-services-people/v1#gradle   https://mvnrepository.com/artifact/com.google.apis/google-api-services-people
implementation 'com.google.apis:google-api-services-people:v1-rev20210515-1.31.0'

What I've tried

Other than looking at the docs (and failing to see a resemblance to what I have to handle), I tried to write about this here.

Sadly I can't find how to migrate away from the old code yet.

I tried to ask there how can I migrate (here and here) but didn't get an answer yet.

The questions

How can I migrate away from GoogleCredential to GoogleCredentials while still using the various APIs such as People API?

In other words: How can I avoid using any of those deprecated classes (GoogleCredential and GoogleApiClient), on Android, while still being able to use the various APIs?

like image 441
android developer Avatar asked May 12 '21 09:05

android developer


People also ask

How does Google OAuth works?

OAuth 2.0 allows users to share specific data with an application while keeping their usernames, passwords, and other information private. For example, an application can use OAuth 2.0 to obtain permission from users to store files in their Google Drives. This OAuth 2.0 flow is called the implicit grant flow.


1 Answers

How can I migrate away from GoogleCredential to GoogleCredentials while still using the various APIs such as People API?

In other words: How can I avoid using any of those deprecated classes (GoogleCredential and GoogleApiClient), on Android, while still being able to use the various APIs?

Although you can make GoogleCredentials work directly, it will be better to use a class derived from GoogleCredentials such as UserCredentials which will accommodate token refresh as GoogleCredential does. GoogleCredentials is more of a foundational class.

The following code makes use of UserCredentials. This is mostly what you have presented, but I have changed some credential storing logic for the purpose of the demo. This code has no deprecated methods except startActivityForResult().

serverAuthCode is available from GoogleSignInAccount. Take a look at Moving Past GoogleApiClient on how to remove the dependency on GoogleApiClient. I have update my public gist of RestApiActivity from the Google signin quickstart which shows how to use GoogleOauthHelper as well as GoogleApi.

GoogleOauthHelper.kt

object GoogleOauthHelper {
    @WorkerThread
    fun setUp(context: Context, serverAuthCode: String?): Services {
        val httpTransport: HttpTransport = NetHttpTransport()
        val jsonFactory = GsonFactory.getDefaultInstance()
        // Redirect URL for web based applications. Can be empty too.
        val redirectUrl = "urn:ietf:wg:oauth:2.0:oob"

        // Patch for demo
        val GOOGLE_CLIENT_ID = context.getString(R.string.GOOGLE_CLIENT_ID)
        val GOOGLE_CLIENT_SECRET = context.getString(R.string.GOOGLE_CLIENT_SECRET)

        var accessToken: AccessToken? = null
        var refreshToken =
            getDefaultSecuredSharedPreferences(context).getString(
                SecuredSharedPreferences.KEY_GOOGLE_REFRESH_TOKEN,
                null
            )

        if (refreshToken == null) {
            /*  Did we lose the refresh token, or is this the first time? Refresh tokens are only
                returned the first time after the user grants us permission to use the API. So, if
                this is the first time doing this, we should get a refresh token. If it's not the
                first time, we will not get a refresh token, so we will proceed with the access
                token alone. If the access token expires (in about an hour), we will get an error.
                What we should do is to ask the user to reauthorize the app and go through the
                OAuth flow again to recover a refresh token.

                See https://developers.google.com/identity/protocols/oauth2#expiration regarding
                how a refresh token can become invalid.
            */
            val tokenResponse = GoogleAuthorizationCodeTokenRequest(
                httpTransport, jsonFactory, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET,
                serverAuthCode, redirectUrl
            ).execute()

            refreshToken = tokenResponse.refreshToken
            if (refreshToken != null) {
                getDefaultSecuredSharedPreferences(context).edit()
                    .putString(SecuredSharedPreferences.KEY_GOOGLE_REFRESH_TOKEN, refreshToken)
                    .apply()
            } else {
                Log.d("Applog", "No refresh token. Going with access token alone.")
                val expiresAtMilliseconds =
                    Clock.SYSTEM.currentTimeMillis() + tokenResponse.expiresInSeconds * 1000
                accessToken = AccessToken(tokenResponse.accessToken, Date(expiresAtMilliseconds))
            }
        }

        Log.d("Applog", "Refresh token: $refreshToken")
        // UserCredentials extends GoogleCredentials and permits token refreshing.
        val googleCredentials = UserCredentials.newBuilder().run {
            clientId = GOOGLE_CLIENT_ID
            clientSecret = GOOGLE_CLIENT_SECRET
            setRefreshToken(refreshToken)
            setAccessToken(accessToken)
            build()
        }

        // Save access token on change
        googleCredentials.addChangeListener { oAuth2Credentials ->
            saveAccessToken(oAuth2Credentials.accessToken)
        }

        val requestInitializer: HttpRequestInitializer = HttpCredentialsAdapter(googleCredentials)
        val appPackageName = context.packageName

        val peopleServiceApi = PeopleService.Builder(httpTransport, jsonFactory, requestInitializer)
            .setApplicationName(appPackageName)
            .build()

        return peopleServiceApi.run { Services(people(), otherContacts(), contactGroups()) }
    }

    private fun saveAccessToken(accessToken: AccessToken) {
        // Persist the token securely.
        Log.d("Applog", "Access token has changed: ${accessToken.tokenValue}")
    }

    // Warning insecure!: Patch for demo.
    private fun getDefaultSecuredSharedPreferences(context: Context): SharedPreferences {
        return PreferenceManager.getDefaultSharedPreferences(context)
    }

    // Warning insecure!: Patch for demo.
    object SecuredSharedPreferences {
        const val KEY_GOOGLE_REFRESH_TOKEN = "GOOGLE_REFRESH_TOKEN"
    }

    class Services(
        /**https://developers.google.com/people/api/rest/v1/people*/
        val peopleService: PeopleService.People,
        /**https://developers.google.com/people/api/rest/v1/otherContacts*/
        val otherContactsService: PeopleService.OtherContacts,
        /**https://developers.google.com/people/api/rest/v1/contactGroups*/
        val contactGroups: PeopleService.ContactGroups,
    )
}

I have posted a demo project on GitHub.

like image 98
Cheticamp Avatar answered Oct 10 '22 15:10

Cheticamp