19

First, let me explain what I am trying to do, as this is a two part question.

I am building a JAX-RS service that internally authenticates with a Google account via OAuth2, so it can access and manipulate a Google calendar. This service will be called by a web site (Joomla), which will allow users that login into the site to register their email address with calendar events so they can get email notifications when the events are near. These users have no knowledge of the underlying Google account.

So my first question, is using Service Account authentication the right thing? Looking at the Google docs, its the only use case that caters for non-human authentication (so server to server authentication).

The second problem is that I have written some code to do this, but I get back a 401/Unauthorized response back the following error back when I call the execute method the Calendar feed. This code was lifted from the Google samples.

I am using v3-rev3-1.5.0-beta of the the Google Calendar API. I have turned on the Calendar API in the account, and created and downloaded a private key, that is used in the code below.

I am calling the code below in Eclipse locally, not from a web server.

So the first call I make is to get back a credential object (Id's obfuscated with X's):

private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
.
.
.
GoogleCredential credential = new GoogleCredential.Builder().setTransport(HTTP_TRANSPORT)
            .setJsonFactory(JSON_FACTORY)
            .setServiceAccountId("284XXXXXXXX@developer.gserviceaccount.com")
            .setServiceAccountScopes(CalendarScopes.CALENDAR)
            .setServiceAccountPrivateKeyFromP12File(new File("D:/3cd8XXXXXXXXXXXXXXXXXXXXXXXXXXXXXd635e-privatekey.p12"))
            .build();

The next step is to then call the Calendar API, like so (no other code called in between):

 Calendar calendar = Calendar.builder(HTTP_TRANSPORT, JSON_FACTORY)
        .setApplicationName("TestApp-Calendar/1.0").setHttpRequestInitializer(credential)
        .build();

 CalendarList feed = calendar.calendarList().list().execute();

The call to list().execute() results in the following error:

com.google.api.client.googleapis.json.GoogleJsonResponseException: 401 Unauthorized { "code" : 401, "errors" : [ { "domain" : "global", "location" : "Authorization", "locationType" : "header", "message" : "Login Required", "reason" : "required" } ], "message" : "Login Required" } at com.google.api.client.googleapis.json.GoogleJsonResponseException.from(GoogleJsonResponseException.java:159) at com.google.api.client.googleapis.json.GoogleJsonResponseException.execute(GoogleJsonResponseException.java:187) at com.google.api.client.googleapis.services.GoogleClient.executeUnparsed(GoogleClient.java:115) at com.google.api.client.http.json.JsonHttpRequest.executeUnparsed(JsonHttpRequest.java:112) at com.google.api.services.calendar.Calendar$CalendarList$List.execute(Calendar.java:510) at uk.org.reigatepriorybowmen.Service.registerEmailForAllCalendarEvents(Service.java:55) at uk.org.reigatepriorybowmen.Service.main(Service.java:74)

The service account is setup as follows:

enter image description here

So, what am I doing wrong?! I have spent hours Googling around looking for solutions so this is my last hope.

Many thanks in advance for your help.

Justin

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449
Justin Phillips
  • 1,358
  • 2
  • 12
  • 26

6 Answers6

6

As for the 401, it seems some oddity with Google's API. I find the first time I run my app in a while, and then at seemingly random intervals hours spread apart, I always get a 401. On the second or third round it actually logs in.

The behavior is even accounted for in Google's Task API sample code here.

Notice this code:

void onRequestCompleted() {
received401 = false;
}

void handleGoogleException(IOException e) {
if (e instanceof GoogleJsonResponseException) {
  GoogleJsonResponseException exception = (GoogleJsonResponseException) e;
  if (exception.getStatusCode() == 401 && !received401) {
    received401 = true;
    accountManager.invalidateAuthToken(credential.getAccessToken());
    credential.setAccessToken(null);
    SharedPreferences.Editor editor2 = settings.edit();
    editor2.remove(PREF_AUTH_TOKEN);
    editor2.commit();
    gotAccount();
    return;
  }
}
Log.e(TAG, e.getMessage(), e);

}

In Google's own code sample they have made provision for a first time 401. It might be some special security issue that needs to be handled in code.

Eugene van der Merwe
  • 4,390
  • 1
  • 35
  • 48
1

I think you will need to specify the user that you are trying to impersonate.

The way you are using ServiceAccount right now is only valid to access data related to your application or to your service account itself. Since you wish to access Google Calendar data you must be a Google Apps domain administrator with privileges to access any of your domain user's data - let me know if I am not correct. Google Calendar data belong to users so you need to specify which user you are trying to impersonate.

To do this using Java, it seems that you need to do use GoogleCredential.Builder#setServiceAccountUser(String)

Please try:

GoogleCredential credential = new GoogleCredential.Builder().setTransport(HTTP_TRANSPORT)
        .setJsonFactory(JSON_FACTORY)
        .setServiceAccountId("284XXXXXXXX@developer.gserviceaccount.com")
        .setServiceAccountScopes(CalendarScopes.CALENDAR)
        .setServiceAccountPrivateKeyFromP12File(new File("/path/to/privatekey.p12"))
        .setServiceAccountUser("user@domain.com")
        .build();

Also, once you have created a Service Account key in your API project (from the APIs Console), you will have to add this project to the list of authorized third party app in the cPanel. More information about this can be found here. The "Client Id" you need to use is the one bound to the Service Account key and looks like <APP_ID>-<OTHER_KEY>.apps.googleusercontent.com

Nicolas Garnier
  • 12,134
  • 2
  • 42
  • 39
  • Hi, thanks, I will give it a whirl and post back if it works. I have abandoned the project since but I still would like to know how to get this working for future reference. – Justin Phillips Jul 19 '12 at 12:18
  • OK I have tried calling the setServiceAccountUser() method and I get the same error. I can't believe that Google have made this so difficult to use! – Justin Phillips Jul 24 '12 at 20:42
  • You also need to whitelist the client ID in your domain;s cPanel. I have added how do that in my answer. I agree 2-Legged OAuth 1 was a bit simpler in my opinion. HOpe we can get something easier than that with OAuth 2.0 one day... – Nicolas Garnier Jul 25 '12 at 18:14
  • @Nivco what cpanel ?? – Majid Laissi Aug 24 '14 at 19:34
  • 1
    It's the Google Apps domain Administration panel. There is a section on giving 3rd party apps API access. The Client ID needs to be whitelisted there. – Nicolas Garnier Aug 25 '14 at 22:58
1

Such behaviour may occur if you did not add api user in admin panel (in your case Documents panel I guess), for instance in google analytics you have to add new user where email address is the same which you got from api (provided by the Google API Service Account, after creating private key). In analytics it looks like this:

example in google analytics

Dawid D
  • 1,057
  • 13
  • 28
  • Hi, thanks, I will give it a whirl and post back if it works. I have abandoned the project since but I still would like to know how to get this working for future reference. – Justin Phillips Jul 19 '12 at 12:17
  • I cant see where the users are set in the API Console. – Justin Phillips Jul 24 '12 at 20:48
  • It is not an api console it is inside google analytics panel. Here is a simple guide try it: [link](http://www.pimcore.org/wiki/display/PIMCORE/Setup+Google+Analytics+Reporting+with+OAuth2+Service+Accounts+(since+1.4.6)) – Dawid D Jul 26 '12 at 23:08
1

I am an Android Mobile App Developer. I am making an application using Google Calendar API (OAuth 2.0). I also faced this same error. But now, I have resolved the error. I am writing the scenario and the solution for it.
The url of getting the Calendar Event for the particular calendar (Mobile App API):

https://www.googleapis.com/calendar/v3/calendars/en.indian%23holiday%40group.v.calendar.google.com/events?access_token=ya29.AHES6ZQyP3iDZM8PF-7ZqZE8oKmWAgUX55sPGPTjGbM5A

In this url, we have to add the calendarId. The format of the url is;

https://www.googleapis.com/calendar/v3/calendars/<calendarId>/events

I was not encoding the calendarId before adding to the url. I encoded the calendarId and then made the Http request. All went fine then.

final String accessToken=dataStore.getAccessToken();

    List<NameValuePair> params = new LinkedList<NameValuePair>();
    params.add(new BasicNameValuePair("access_token", accessToken));
    String paramString = URLEncodedUtils.format(params, "utf-8");

    final String url = String.format(AuthConstants.CALENDAR_EVENTS_URI,URLEncoder.encode(calendarId))+"?"+ paramString;

Now the calendar events are returned in the JSON.

Khushboo
  • 3,095
  • 2
  • 23
  • 24
0

OK, so I have been playing around with this and have got past the 401 error, but I am now hitting a 403. However, I will detail the code I have written that at least answers the 401 problem.

I did not change any of the service account settings or re-generate any of the keys etc, that is all the same.

What has change is some of the code, and I upgraded to v3r20lv1.12.0-beta of the Calendar API.

The code below, as detailed above in the OP, remains the same, i.e.:

private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
.
.
.
GoogleCredential credential = new GoogleCredential.Builder().setTransport(HTTP_TRANSPORT)
        .setJsonFactory(JSON_FACTORY)
        .setServiceAccountId("284XXXXXXXX@developer.gserviceaccount.com")
        .setServiceAccountScopes(CalendarScopes.CALENDAR)
        .setServiceAccountPrivateKeyFromP12File(new File("D:/3cd8XXXXXXXXXXXXXXXXXXXXXXXXXXXXXd635e-privatekey.p12"))
        .build();

The code to get a handle for the Calendar has changed, but this was forced by an API change:

Calendar calendar = new Calendar(HTTP_TRANSPORT, JSON_FACTORY, credential);

At this point, I can iterate through my calendar and see the events, by running the following code (this prints an event summary):

Events events = calendar.events().list(CALENDAR_ID).execute();

for (Event event : events.getItems())
{
    System.out.println(event.getSummary());
}

But, if I try and update an event, I get a 403 Forbidden error. I will raise a separate thread for this, and will link the two threads together.

Thanks to all for your help!

EDIT: here is the new thread.

Community
  • 1
  • 1
Justin Phillips
  • 1,358
  • 2
  • 12
  • 26
0

Did you request permission for the proper scopes? You should always include https://www.googleapis.com/auth/userinfo.profile

Kees de Kooter
  • 7,078
  • 5
  • 38
  • 45