What is the correct google authentication mechanism in a standalone Python script?

I have a code that I am using to extract the email address from Gmail contacts into a text file. This is a simple Python script that runs on a cron job and is based on the Python gdata library (currently v2.0.18).

As at the beginning of this month, this no longer works due to Google's depreciation of the ClientLogin protocol . The resulting error looks like this:

{'status': 401, 'body': '<?xml version="1.0" encoding="UTF-8"?>\n<errors xmlns="http://schemas.google.com/g/2005">\n <error>\n  <domain>GData</domain>\n  <code>required</code>\n  <location type="header">Authorization</location>\n  <internalReason>Login Required</internalReason>\n </error>\n</errors>\n', 'reason': 'Unauthorized'}

      

I knew this was happening and handled in other places (like AppEngine apps), but forgot that I would have to transform this script. Now that I'm here, I found that I have no idea how I should get this job done.

All the links I've found, like here on the Google Apps Developers blog or here and here on StackOverflow, assume that the solution is to use OAuth2Token. However, this requires a Client ID and Client Secret from the Google API Console that is bound to the application. I have no application. I just want to authenticate from my script.

Can anyone suggest the correct way to do this in a standalone script? Or am I out of luck and there is no mechanism for this?

These are the guts of the existing contact code:

from gdata.contacts.service import ContactsService, ContactsQuery

user = "myuser@gmail.com"
password = "mypassword"

addresses = set()
client = ContactsService(additional_headers={"GData-Version":"2"})
client.ssl = True
client.ClientLogin(user, password)
groups = client.GetGroupsFeed()
for group in groups.entry:
   if group.content.text == "System Group: My Contacts":
      query = ContactsQuery()
      query.max_results = 9999   # large enough that we'll get "everything"
      query.group = group.id.text
      contacts = client.GetContactsFeed(query.ToUri())
      for contact in contacts.entry:
         for email in contact.email:
            addresses.add(email.address.lower())
      break
return addresses

      

Ideally, I want to replace with client.ClientLogin()

another mechanism that saves the rest of the code using gdata. Alternatively, if this is not possible with gdata, I am open to converting to another library that offers similar functionality.

+3


source to share


2 answers


Can anyone suggest a suitable way to do this in a standalone script? Or am I out of luck and there is no mechanism for this?

There is no mechanism like the one you are using. You will need to create a Cloud Developer project and use OAuth2 and rewrite your script.



To make this as reliable as possible, you can switch to the new Contacts API . With this API, you can use OAuth2 device flow , which might be easier for your use.

Not the answer you hoped to hear, I know, but I think this is the only answer.

+1


source


In the end it turned out to be easier to just hack the shell script using curl, mess with the gdata library. As expected, I was able to do most of the verification process manually, outside of the script, behind the OAuth2 device flow instructions .

After completing the verification process, I had 4 required credentials: client id, client secret, access token, and refresh token. According to google documentation, the access token will eventually expire. You can get a new access token by asking the token manager to renew the token. When you do this, you apparently get a new access token, but not a new token update.

I store the client id and secret and the refresh token CREDENTIALS

in a JSON file. Since the access token changes over time, it is stored in a file ACCESS

, also in JSON format.



Below are the important parts of the script:

#!/bin/ksh

CLIENT_ID=$(cat ${CREDENTIALS} | jq -r ".client_id")
CLIENT_SECRET=$(cat ${CREDENTIALS} | jq -r ".client_secret")
REFRESH_TOKEN=$(cat ${CREDENTIALS} | jq -r ".refresh_token")
ACCESS_TOKEN=$(cat ${ACCESS} | jq -r ".access_token")

CONTACTS_URL="https://www.google.com/m8/feeds/contacts/default/full?access_token=${ACCESS_TOKEN}&max-results=5000&v=3.0"
ERROR=$(curl --show-error --silent --fail "${CONTACTS_URL}" -o ${CONTACTS_XML} 2>&1)
RESULT=$?
if [[ ${RESULT} -eq 0 ]]
then
   cat ${CONTACTS_XML} | grep 'gd:email' | sed 's/^.*address="//g' | sed 's/".*$//g' | tr '[:upper:]' '[:lower:]' | sort | uniq
elif [[ ${RESULT} -eq 22 ]]
then
   echo "${ERROR}" | grep -q "401"
   if [[ $? -eq 0 ]]
   then
      TOKEN_URL="https://www.googleapis.com/oauth2/v3/token"
      REFRESH_PARAMS="client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token"
      ERROR=$(curl --show-error --silent --fail --data "${REFRESH_PARAMS}" ${TOKEN_URL} -o ${REFRESH_JSON})
      RESULT=$?
      if [[ ${RESULT} -eq 0 ]]
      then
         ACCESS_TOKEN=$(cat ${REFRESH_JSON} | jq -r ".access_token")
         jq -n --arg access_token "${ACCESS_TOKEN}" '{"access_token": $access_token, }' > ${ACCESS}

         CONTACTS_URL="https://www.google.com/m8/feeds/contacts/default/full?access_token=${ACCESS_TOKEN}&max-results=5000&v=3.0"
         ERROR=$(curl --show-error --silent --fail "${CONTACTS_URL}" -o ${CONTACTS_XML} 2>&1)
         RESULT=$?
         if [[ ${RESULT} -eq 0 ]]
         then
            cat ${CONTACTS_XML} | grep 'gd:email' | sed 's/^.*address="//g' | sed 's/".*$//g' | tr '[:upper:]' '[:lower:]' | sort | uniq
         else
            print "Unexpected error: ${ERROR}" >&2
            exit 1
         fi
      else
         print "Unexpected error: ${ERROR}" >&2
         exit 1
      fi
   else
      print "Unexpected error: ${ERROR}" >&2
      exit 1
   fi
else
   print "Unexpected error: ${ERROR}" >&2
   exit 1
fi

      

It's not the prettiest thing in the world, but I was looking for something quick-dirty and it works.

+3


source







All Articles