5

We use Google Contacts API with OAuth2:

credential = new GoogleCredential.Builder().setTransport(new NetHttpTransport())
        .setJsonFactory(new JacksonFactory())
        .setClientSecrets(OAuth2ClientId(), OAuth2ClientSecret())
        .addRefreshListener(new CredentialRefreshListener() {...});

myService = new ContactsService("My-App");
myService.setOAuth2Credentials(credential);

and quite regularly we receive '401 Unauthorized' response that the GData library can't handle. AuthenticationException throws NPE when WWW-Authenticate header is missing.

Caused by: java.lang.NullPointerException: No authentication header information
        at com.google.gdata.util.AuthenticationException.initFromAuthHeader(AuthenticationException.java:96) ~[gdata-core-1.0-1.47.1.jar:na]
        at com.google.gdata.util.AuthenticationException.<init>(AuthenticationException.java:67) ~[gdata-core-1.0-1.47.1.jar:na]
        at com.google.gdata.client.http.HttpGDataRequest.handleErrorResponse(HttpGDataRequest.java:608) ~[gdata-core-1.0-1.47.1.jar:na]
        at com.google.gdata.client.http.GoogleGDataRequest.handleErrorResponse(GoogleGDataRequest.java:564) ~[gdata-core-1.0-1.47.1.jar:na]
        at com.google.gdata.client.http.HttpGDataRequest.checkResponse(HttpGDataRequest.java:560) ~[gdata-core-1.0-1.47.1.jar:na]
        at com.google.gdata.client.http.HttpGDataRequest.execute(HttpGDataRequest.java:538) ~[gdata-core-1.0-1.47.1.jar:na]
        at com.google.gdata.client.http.GoogleGDataRequest.execute(GoogleGDataRequest.java:536) ~[gdata-core-1.0-1.47.1.jar:na]
        at com.google.gdata.client.Service.getFeed(Service.java:1135) ~[gdata-core-1.0-1.47.1.jar:1.47.1]
        at com.google.gdata.client.Service.getFeed(Service.java:1077) ~[gdata-core-1.0-1.47.1.jar:1.47.1]
        at com.google.gdata.client.GoogleService.getFeed(GoogleService.java:676) ~[gdata-core-1.0-1.47.1.jar:1.47.1]
        at com.google.gdata.client.Service.query(Service.java:1237) ~[gdata-core-1.0-1.47.1.jar:1.47.1]
        at com.google.gdata.client.Service.query(Service.java:1178) ~[gdata-core-1.0-1.47.1.jar:1.47.1]

We managed to add a wrapper that would try token refreshing on this NPE. It helps but then there are still many cases when refreshing fails:

credential.refreshToken() == false

When we run refreshToken() in the debugger we see that executeRefreshToken() is executed without an exception but tokenResponse==null is returned. As a result refreshToken() returns false and no reason is passed to the listeners

try {
    TokenResponse tokenResponse = executeRefreshToken();
    if (tokenResponse != null) {
      setFromTokenResponse(tokenResponse);
      for (CredentialRefreshListener refreshListener : refreshListeners) {
        refreshListener.onTokenResponse(this, tokenResponse);
      }
      return true;
    }
  } catch (TokenResponseException e) {
    boolean statusCode4xx = 400 <= e.getStatusCode() && e.getStatusCode() < 500;
    // check if it is a normal error response
    if (e.getDetails() != null && statusCode4xx) {
      // We were unable to get a new access token (e.g. it may have been revoked), we must now
      // indicate that our current token is invalid.
      setAccessToken(null);
      setExpiresInSeconds(null);
    }
    for (CredentialRefreshListener refreshListener : refreshListeners) {
      refreshListener.onTokenErrorResponse(this, e.getDetails());
    }
    if (statusCode4xx) {
      throw e;
    }
  }
  return false;

Our tokens are always for multiple scopes: https://www.googleapis.com/auth/userinfo.email https://www.google.com/m8/feeds https://www.googleapis.com/auth/calendar https://mail.google.com/ https://www.googleapis.com/auth/tasks

Update: We've successfully moved to People API and the new API is also used by our Unified API Contacts API https://docs.aurinko.io/article/25-contacts-api

Alexey
  • 556
  • 1
  • 5
  • 18
  • To clarify, sometimes the refresh works but sometimes it doesn't? Once the refresh stops working, does it ever work again? AKA, is it a temporary state or a permanent one? – Eric Koleda Oct 13 '14 at 21:02
  • Ok, we've been experimenting more with this. The behavior seems a bit different for Contacts vs. Calendar&Tasks. It looks like for Calendar&Tasks credential.refreshToken() may fail without a reason but if you wait and try again it may actually refresh the token. For Contacts it's the NPE "No authentication header information" but then again if you try again in some cases it does end up refreshing the token and in other cases it just always fails. – Alexey Oct 14 '14 at 21:48
  • I'm working on replicating the problem. I certainly see the NPE being thrown, but still working on getting the refresh to fail. – Eric Koleda Oct 16 '14 at 15:07
  • Eric I updated the question. I you want I could share with you a set of access/refresh tokens for which this happens quite regularly. I wonder if token refreshing is rate limited by Google because if we try more times to refresh we usually succeed eventually. – Alexey Oct 17 '14 at 17:36
  • This may be a separate issue but I've also noticed that for Gmail/IMAP XOAUTH a newly refreshed token often is rejected by Gmail/IMAP. It feels like the refreshed token is not propagated fast enough to Gmail servers and they don't know about it yet when we try to use it. Again, we put some wrappers to wait a bit before using the token and that seems to help. So in general our token refreshing procedures now are wrapped into our code that handles NPE, retries and then waits and then retries again. Not very elegant. – Alexey Oct 17 '14 at 17:45
  • executeRefreshToken method has this comment "successful response from the token server or null if it is not possible to refresh the access token". In our case 'null' is returned often but additional attempts would succeed. Do you know why it would be "not possible to refresh token"? – Alexey Oct 17 '14 at 17:59
  • How often are you refreshing the token? Are there multiple threads each using the refresh token to generate access tokens? That could have an impact. – Eric Koleda Oct 17 '14 at 19:58
  • I think the token is refreshed hourly. Yes, there could be multiple threads refreshing it. We actually store 1-4 copies of the same multi-scope token and then 1-4 threads (calendar, contacts, email, tasks) are using its own copy and could be refreshing the token when it expires. – Alexey Oct 18 '14 at 20:47
  • I refactored our code to use one shared GoogleCredential instead of 1-4 (one per each service thread). This way is probably more correct and should prevent parallel attempts to refresh the token (there is locking/synchronization in that method). Will have to monitor to see if this helps. – Alexey Oct 19 '14 at 07:08
  • With Gmail/XOAUTH2 it's some other issue. The token is refreshed fine but the following IMAPS connection still fails: [ALERT] Invalid credentials (Failure) at com.sun.mail.imap.IMAPStore.protocolConnect(IMAPStore.java:715) ~[javax.mail-1.5.2.jar:1.5.2] at com.sun.mail.gimap.GmailStore.protocolConnect(GmailStore.java:92) ~[gimap-1.5.2.jar:1.5.2] – Alexey Oct 20 '14 at 06:03
  • Let's tackle the Gmail/XOAUTH2 in another thread. See answer below for workaround. – Eric Koleda Oct 20 '14 at 13:31

1 Answers1

7

There is currently a bug in the Contacts API where certain HTTP User-Agent strings, cause the 401 response to return as an HTML page instead of an XML response, and missing the WWW-Authenticate header that the AuthenticationException class is relying on. GContacts-Java is one of these special user agent strings. A workaround is to change the user agent of your client after you create it:

ContactsService service = new ContactsService(applicationName);
service.getRequestFactory().setHeader("User-Agent", applicationName);

This should eliminate the NPE, and allow the client library to automatically detect expired tokens and refresh them automatically.

Eric Koleda
  • 12,420
  • 1
  • 33
  • 51
  • Thank you Eric. I'll give it a try. And will open another question for the Gmail token issue. – Alexey Oct 21 '14 at 23:19
  • Eric, it seemed like it fixed the issue for the Contacts API (GData) but now I also see that we get these new errors: GData insufficientPermissions Insufficient Permission Any ideas why? Is it another sign of an expired token that is not handled automatically? – Alexey Oct 27 '14 at 06:14
  • 1
    Here you go http://stackoverflow.com/questions/26616591/gdata-insufficientpermissions – Alexey Oct 28 '14 at 19:10
  • 1
    Eric, the NullPointerException "No authentication header information" issue seems to be back for Google Contacts API. We have not seen it for a long while after implementing your workaround but we see this error again in all our logs. – Alexey Oct 21 '15 at 15:48
  • com.google.gdata.util.AuthenticationException.initFromAuthHeader(AuthenticationException.java:96) ~[gdata-core-1.0-1.47.1.jar:na] – Alexey Oct 27 '15 at 17:36
  • @Alexey, we are encountering exactly what you were describing, and it's suddenly is happening after months of properly working. The linked question was deleted. Did you find a workaround or was there some other problem? Any insight is appreciated. – Jeff Evans Nov 02 '15 at 19:08
  • The workaround is to refresh your token but this is annoying to catch NPE and handle it as a hack. Too bad Google keeps breaking their own gdata java library. – Alexey Nov 03 '15 at 06:39
  • I've reached back out to the engineering team for more information. – Eric Koleda Nov 03 '15 at 15:30
  • 1
    same here. it fails on every call now. can anyone knowing how to handle this can place a GIST with workaround somewhere? – musketyr Dec 02 '15 at 14:28
  • The problem started 2015-10-07 19:51:59,558 for my service http://gsyn.ch I really don't know why Google didn't fix that problem. There should be more software out there using their Contacts API. Here's my work-around to be called before the service is used: Long expiresIn = credential.getExpiresInSeconds(); // check if token will expire in a minute if (credential.getAccessToken() == null || expiresIn != null && expiresIn <= 60) { boolean succ = credential.refreshToken(); log.info("Refresh status {}", succ); } service.setOAuth2Credentials(credential); – Bytemaster Dec 10 '15 at 15:27
  • 1
    It's 2017 and the issue has not fixed yet. – Alexey Mar 08 '17 at 21:30
  • So, does it mean can't simply use this service? http://stackoverflow.com/questions/43050496/cant-get-the-list-of-accounts-using-google-contacts-api-oauth-2-0/43050984#43050984 – Greg Mar 28 '17 at 08:25