Java Google Calendar api "access_denied" on Service Account - java

I'm trying to connect to a calendar using the Java Google Calendar api. The java application uses a service account.
I've the following code:
java.io.File licenseFile = new java.io.File("39790cb51b361f51cab6940d165c6cda4dc60177-privatekey.p12");
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(HTTP_TRANSPORT)
.setJsonFactory(JSON_FACTORY)
.setServiceAccountId("xxx#developer.gserviceaccount.com")
.setServiceAccountUser(EMAIL_ADRESS)
.setServiceAccountScopes(CalendarScopes.CALENDAR)
.setServiceAccountPrivateKeyFromP12File(licenseFile)
.build();
client = new com.google.api.services.calendar.Calendar.Builder(
HTTP_TRANSPORT, JSON_FACTORY, credential)
.setApplicationName("Google Calendar Sync").build();
Calendar calendar = client.calendars().get(EMAIL_ADRESS).execute();
On the last line I get an IOException with the message:
ex = (com.google.api.client.auth.oauth2.TokenResponseException)
com.google.api.client.auth.oauth2.TokenResponseException: 400 Bad
Request { "error" : "access_denied" }
I dubble checked the values for the GoogleCredential object and they are correct.
I've also added https://www.google.com/calendar/feeds/, http://www.google.com/calendar/feeds/ in my domain console with the application id as client to authorize third party application access
Am I forgetting a step?

The api isn't finished yet. More specifically the service account part.
The calendar owner needs to give permission to the application to read/write the calendar in it's calendar settings. It's found in the sharing settings of the calendar, there you can add e-mail adresses of accounts and give them permission on your calendar.
So in this case I had to add: xxx#developer.gserviceaccount.com to the permission/share list of the calendars the application needed access to.
I also deleted another post that didn't full work because of the issue above. I'll undelete it since it contains some code fragments that may help other people in the future. But beaware of the permission issues and service accounts not supporting Google Calendar

I do:
List find = client.events().list(EMAIL_ADRESS);
DateTime timeMin = new DateTime(DateUtil.stringToDate("01/01/2013"));
DateTime timeMax = new DateTime(DateUtil.stringToDate("01/02/2013"));
find.setTimeMin(timeMin);
find.setTimeMax(timeMax);
try{
Events events = find.execute();
int i =0;
while (true) {
System.out.println("Page: "+(++i)+": "+events.getItems().size());
for (Event event : events.getItems()) {
System.out.println(event.getSummary());
}
String pageToken = events.getNextPageToken();
if (pageToken != null && !pageToken.isEmpty()) {
events = client.events().list(EMAIL_ADRESS).setPageToken(pageToken).execute();
} else {
break;
}
}
}catch(GoogleJsonResponseException e){
System.out.println(e.getMessage());
// e.printStackTrace();
}

I got it to work
I don't know why but I deleted the line
.setServiceAccountUser(EMAIL_ADRESS)
Also I added an extra url in the domain scope:
https://apps-apis.google.com/a/feeds/calendar/resource/#readonly
and deleted a link that wasn't working.
Finally I changed the applicationName in my client declaration to the same name as in the api console
client = new com.google.api.services.calendar.Calendar.Builder(
HTTP_TRANSPORT, JSON_FACTORY, credential)
.setApplicationName("HolidaySyncs").build();
After these steps it started to work.
Also note for future reference after I did this I had the following error:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "notFound",
"message": "Not Found"
}
],
"code": 404,
"message": "Not Found"
}
}
I solved this by changing for example:
Event result = client.events().insert(<EMAIL ADRESS>, event).execute();
to
Event result = client.events().insert("primary", event).execute();
First I tought there was something wrong with the google servers, but apparently it goes wrong when you try to link to a calendar ID. So linking to "primary" which is the primary calendar of an account works. But according to the documentation it should also work when you refer to a specific calendar ID, where the email address is the primary calendar. Probably a bug?
UPDATE: after these code correction I still had issues. Read the accepted answer for more information.

Related

Error while creating google calendar event with a service account

I have a requirement of creating a google calendar event on a calendar and add other users as attendees to that event. The objective is to send calendar events to the application users without taking their consent ( O-Auth ).
After reading Google's documentation, I found out that I need a service account. So I created a project and a service account from one of the email addresses of our G-Suite, noreply#xxxxx.com and enabled calendar API for the same.
I created and downloaded a key pair ( JSON ) whose content is,
{
"type": "service_account",
"project_id": "*****",
"private_key_id": "bdbbcd**************49f77d599f2",
"private_key": "**"
"client_email": "******#*****.iam.gserviceaccount.com",
"client_id": "11083******576856",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/****dev%40*****kdev.iam.gserviceaccount.com"
}
And as per the documentation, I proceded to write authentication flow code,
public static GoogleCredential doOauth( String credsPath ) throws IOException
{
GoogleCredential credential = GoogleCredential.fromStream(new FileInputStream(credsPath))
.createScoped(Collections.singleton(CalendarScopes.CALENDAR));
System.out.println(credential);
return credential;
}
The credential object has most of the details from the key file. But, the fields, serviceAccountUser, accessToken, refreshToken, clientAuthentication and requestInitializer have null value. ( I am guessing something wrong here )
Now, using the credentialObject, I continued to write the code as per the documentation to create the event.
GoogleCredential credential = doOauth(CREDENTIALS_FILE_PATH);
Event event = new Event().setSummary("Google I/O 2015").setLocation("800 Howard St., San Francisco, CA 94103")
.setDescription("A chance to hear more about Google's developer products.");
final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
DateTime startDateTime = new DateTime("2020-12-28T09:00:00-07:00");
EventDateTime start = new EventDateTime().setDateTime(startDateTime).setTimeZone("America/Los_Angeles");
event.setStart(start);
DateTime endDateTime = new DateTime("2020-12-28T17:00:00-07:00");
EventDateTime end = new EventDateTime().setDateTime(endDateTime).setTimeZone("America/Los_Angeles");
event.setEnd(end);
String[] recurrence = new String[] { "RRULE:FREQ=DAILY;COUNT=2" };
event.setRecurrence(Arrays.asList(recurrence));
EventAttendee[] attendees = new EventAttendee[] { new EventAttendee().setEmail("myemailaddress#gmail.com.com") };
event.setAttendees(Arrays.asList(attendees));
EventReminder[] reminderOverrides = new EventReminder[] {
new EventReminder().setMethod("email").setMinutes(24 * 60),
new EventReminder().setMethod("popup").setMinutes(10), };
Event.Reminders reminders = new Event.Reminders().setUseDefault(false)
.setOverrides(Arrays.asList(reminderOverrides));
event.setReminders(reminders);
String calendarId = "primary";
Calendar service = new Calendar.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
.setApplicationName("testapp").build();
event = service.events().insert(calendarId, event).execute();
System.out.printf("Event created: %s\n", event.getHtmlLink());
But, this resulted in the error,
{
"code" : 403,
"errors" : [ {
"domain" : "calendar",
"message" : "Service accounts cannot invite attendees without Domain-Wide Delegation of Authority.",
"reason" : "forbiddenForServiceAccounts"
} ],
"message" : "Service accounts cannot invite attendees without Domain-Wide Delegation of Authority."
}
After spending time on Domain-Wide Delegation, I understood that this is needed if we have to send the event as other user from our g-suite, which is not needed for my problem. But, to debug, I went ahead and provided Domain-Wide Delegation and re ran the program. The same error came again.
So, I removed the invitees/attendees from the event object and re ran the application. This time the program ran without any error, but the event link generated, on click says, Could not find the requested event.
I do not see any examples of using the service account via java client libraries on the google developer link.
Can you please let me know what is going wrong here and the official/working documentation on how exactly to create a google calendar event from my project add other ( non g suite as well ) users to add as attendees, so that I do not have to get consent from other users to add events to their own calendar?
Thank You.
Fist off I want to say that this is something new, if you are seeing a lot of questions, and tutorials that did not state you needed to do this its because service accounts used to be able to send invites, this is something google locked down about a year ago.
This should work but i have not tested it as i no longer have access to a Gsuite account. You need to have the gsuite admin set up domain wide delegation to anther user. Then the service account needs to impersonate that user so it will appear as that user is the one sending the invites.
The only example I have for how that is added is in .net
.net example
var gsuiteUser = "se#clawskeyboard.com";
var serviceAccountCredentialInitializer = new ServiceAccountCredential.Initializer(serviceAccount)
{
User = gsuiteUser,
Scopes = new[] { GmailService.Scope.GmailSend, GmailService.Scope.GmailLabels }
}.FromCertificate(certificate);
Java example guess
I am not a java dev but the .net client library and the Java client library are very close in how they were developed. I would guess that you are looking for a method called setServiceAccountUser
GoogleCredential credential = new GoogleCredential.Builder().setTransport(HTTP_TRANSPORT)
.setJsonFactory(JSON_FACTORY)
.setServiceAccountId(SERVICE_ACCOUNT_EMAIL)
.setServiceAccountScopes(CalendarScopes.CALENDAR)
.setServiceAccountPrivateKeyFromP12File(credsPath))
.setServiceAccountUser(gsuiteUser)
.build();
OAuth2 for Service Accounts
You should be able to impersonate the account user by setting the delegated variable to the user's email in the GoogleCredential object.
GoogleCredential credential = GoogleCredential.fromStream(new FileInputStream("credentials.json"))
.createScoped(<SCOPES>)
.createDelegated("user#example.com");

Gmail api scope & format mismatch

I'm trying to write tiny gmail client for android as training.
I took gmail api guide sample from https://developers.google.com/gmail/api/quickstart/android modified it a little to get messages with headers & body by threads. I set scopes to GmailScopes.Gmail_modify and edited main request function as this:
private List<String> getDataFromApi() throws IOException {
// Get the labels in the user's account.
String user = "me";
List<String> labels = new ArrayList<String>();
ListLabelsResponse listResponse =
mService.users().labels().list(user).execute();
ListThreadsResponse listThreads = null;
try {
listThreads = mService.users().threads().list(user).execute();
} catch (IOException ioex){
Log.e(LOG_TAG, "First: " + ioex.toString());
}
for (Thread thread : listThreads.getThreads()) {
try {
thread = mService.users().threads().get(user, thread.getId()).setFormat("full").execute();
} catch (IOException ioex){
Log.e(LOG_TAG, "Second: " + ioex.toString());
}
for(Message message : thread.getMessages()){
labels.add(message.getId());
}
}
return labels;
}
But I always get
Second: GoogleJsonResponseException: 403 Forbidden {
"code" : 403,
"errors" : [ {
"domain" : "global",
"message" : "Metadata scope doesn't allow format FULL",
"reason" : "forbidden"
} ],
"message" : "Metadata scope doesn't allow format FULL"
}
I tried different scopes configurations but seems like service scope is always set to GmailScopes.GMAIL_METADATA
This is exactly my problem these day when playing with Google APIs Explorer. And here is what I did to solve it:
Go to https://security.google.com/settings/security/permissions
Choose the app you are playing with, mine is Google APIs Explorer
Click Remove > OK
Next time, just request exactly which permissions you need.
Hope it help :)
you should remove the "metadata" scope.
check app permissions to make sure you have only these 3 scopes:
https://mail.google.com/
gmail.modify
readonly
, or else remove the permissions and add them again.
After getting permissions to device Contacts you have to approve chosen copes. So first time I approved metadata scope. Next times when I needed to approve readonly scope, there was no window to do it. So you need to delete scopes permissions from google account and reinstall app.
got the same error when a service account which i was using had only these scopes ( Security > API Controls > Domain-wide Delegation ):
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/gmail.metadata
and addition of
https://mail.google.com/
solved the issue

Subject must match Issuer claim in the client assertion

I want to integrate office365 service management API for collecting events from it.I want to use client credential way to use service to service call but i am getting following error,
{
"error":"invalid_client",
"error_description":"AADSTS50048: Subject must match Issuer claim in the client assertion.
\r\nTrace ID: 1ad7acd8-3945-4fe0-a313-07638eb76e42\r\nCorrelation ID: a6c3a3c9-b737-4bfc-894f-3086c3ce8dfa\r\nTimestamp: 2016-06-09 07:20:15Z",
"error_codes":[50048
],
"timestamp":"2016-06-09 07:20:15Z",
"trace_id":"1ad7acd8-3945-4fe0-a313-07638eb76e42",
"correlation_id":"a6c3a3c9-b737-4bfc-894f-3086c3ce8dfa"
}
i use following doc to integration,
For getting client assersion,
https://msdn.microsoft.com/en-us/library/azure/dn645543.aspx I am getting this. But for Access token,
https://msdn.microsoft.com/en-us/library/office/dn707383.aspx I not getting this as a response getting above error.
Somebody help me please :)
How did you get the client_assertion? The link you provide doesn’t describe how to get the ‘client_assertion’. It acquire the token with the app’s id and secret which is doesn’t support for the Office 365 Management API. You can refer the blog to about the ‘client_assertion’.
And here is an C# code sample which use the ADAL to get the access token for the client credentials flow:
string clientId = "{clientId}";
string certThumbprint = "‎{copy from mmc}";
certThumbprint = certThumbprint.Replace("\u200e", string.Empty).Replace("\u200f", string.Empty).Replace(" ", string.Empty);
string apiResourceId = "https://manage.office.com";
X509Certificate2 cert = null;
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
string authority = "https://login.windows.net/{yourTentant}";
var authContext = new AuthenticationContext(authority);
try
{
store.Open(OpenFlags.ReadOnly);
cert = store.Certificates.Find(X509FindType.FindByThumbprint, certThumbprint, false)[0];
}
finally
{
store.Close();
}
var certCred = new ClientAssertionCertificate(clientId, cert);
AuthenticationResult result = null;
try
{
result = await authContext.AcquireTokenAsync(apiResourceId, certCred);
}
catch (Exception ex)
{
}

Yahoo Oauth inconsistent "Invalid Signature"

H!I am having a very hard time with Yahoo Oauth right now.
So here's the problem, I am using scribe 3.1.5 and followed Yahoo's documentations(apparently they use Oauth1.0), I've been able to get the request token, then trade for the access token with the verifier. The problem emerges when I try to get user's GUID from URL http://social.yahooapis.com/v1/me/guid?format=json with the access token.
now, what's interesting is that, yahoo would sometimes give me the GUID back, and sometimes give me a "invalid signature" error. Sometimes I get 5 invalid signatures in a row, sometimes I get 15 successful calls in a row, most of the time it is like 40% invalid signatures and 60% success. What is even weirder is that sometimes I get a success when fetching GUID, but when i try to fetch user's profile IMMEDIATELY after the success with the identical access token and GUID, it gives me an invalid sigature...(wtf)
so here's the code I use:
Redirecting User:
Token requestToken = yahooService.getRequestToken();
getSession().setAttribute("yahooRequestToken", requestToken);
String authenticationUrl = yahooService.getAuthorizationUrl(requestToken);
redirect(authenticationUrl);
Getting callback:
#GET #Path("/oauthcallback/yahoo")
public Response yahooCallback(#QueryParam("oauth_token") String oAuthToken, #QueryParam("oauth_verifier") String oAuthVerifier) {
Token requestToken = (Token)getSession().getAttribute("yahooRequestToken");
Token accessToken = yahooService.getAccessToken(requestToken, oAuthVerifier);
UserProfile user = userService.findUserById(getUserId());
try{
//TODO occasioanlly yahoo returns invalid_signature, this is inconsistent and I have no idea why
String guid = yahooService.getGuid(accessToken);
String email = yahooService.getUserEmail(guid, accessToken);
.....
YahooService::Getting Access Token:
[the service object is protected final OAuthService service; in parent class]
#Override
public Token getAccessToken(Token requestToken, String oAuthVerifier) {
Verifier verifier = new Verifier(oAuthVerifier);
return service.getAccessToken(requestToken, verifier);
}
YahooService::Getting GUID:
#Override
public String getGuid(Token accessToken){
OAuthRequest requestA = new OAuthRequest(Verb.GET, GET_YAHOO);
service.signRequest(accessToken, requestA);
Response responseA = requestA.send();
JsonParser parser = new JsonParser();
//sometimes the response body is a invalid signature error message
JsonObject json = (JsonObject)parser.parse(responseA.getBody());
return json.getAsJsonObject("guid").get("value").getAsString();
}
YahooService::Getting User Email:
#Override
public String getUserEmail(String guid, Token accessToken) {
String profileCallUrl = GET_YAHOO_PROFILE.replaceAll("GUID", guid);
OAuthRequest requestB = new OAuthRequest(Verb.GET, profileCallUrl);
service.signRequest(accessToken, requestB);
requestB.addHeader("realm", "yahooapis.com");
Response responseB = requestB.send();
JsonParser parser = new JsonParser();
//sometimes the response body is a invalid signature error message
JsonObject jsonProfile = (JsonObject)parser.parse(responseB.getBody());
...processing code, error free
}
I know YahooAPI class in Scribe 3.1.5 in maven distribution is like 2 years old, but I doubt it would lead to such inconsistent behavior. Scribe's built in support for Google and Live oauth is basically useless, unfortunately, unlike Google or Hotmail which both have awesome doc so that I could basically figure out everything myself, Yahoo's doc stops at getting the access token, I can not find useful explanation on why I would get an invalid signature SOMETIMES with my access token
Please help! Thanks in advance
Its looks like Yahoo issue, I have same error message since few days :
http://developer.yahoo.com/forum/OAuth-General-Discussion-YDN-SDKs/signature-invalid-when-making-calls-to-the/1385735171123-8a38d8cf-815b-43ac-9d77-5bd2f2f60796
There is no need to ask for GUID to yahoo as yahoo returns GUID of the currently logged in user at the time of giving you the access token so if you have a access token you also have a GUID in the response.
Refer this

How to program availability check and to create new meeting to Outlook?

I am creating a Java web app to manage meetings between a set of students and teachers. All of them already use Outlook to manage their email and personal calendar.
I would like to know if it's even possible to build the schedule feature of my web app using Exchange, Office365 or Sharepoint Team Calendar via REST service in order to check the availability and create a meeting for a student and one of the teachers available:
SharePoint 2013 REST service
So far, the most promising mechanism I have found is Microsoft Sharepoint Server's calendar, which collaborative features makes possible to create a meeting and check availability for a list of users. The downside is that it does not support one to one meetings but for the entire team (as far I have found).
My second option would be to require everyone in the group (students and teachers of the department) to make public their personal calendar so the web app be able to check the availability of both student and teacher and send a meeting request. The obvious problem is the privacy/security concern derived from this approach.
My last option (and by far the less favourite because it feels like re-inventing the wheel) is to build a proprietary calendar within the web app and send iCal requests to each person. The obvious problem with this approach is synchronisation between the two separated calendars.
In addition, this feature must be a pretty common need so there should be tons of blogs explaining how to take advantage of Exchange/Sharepoint/Office365 to implement it (other platforms are not considered since my employer's infrastructure is based on Microsoft). However, whether it is so obvious that nobody talks about it or I have not searched in the right place. Any advice to point me in the right direction?
Exchange natively shows user calendar availability exposed in EWS (Exchange Web Services), your network administrator must configure Exchange server enabling EWS.
But guess what... Office 365 (as I know) have EWS services enabled, due exchange is part of office 365 offer.
As EWS are normal Web services you should create a "service stub" or proxy in whatever you use in java to create services references mapping wsdl files.
Exchanged EWS is my preferred solution.
Hope this helps.
This is the reference page, this link show how to use the service references from C# to make the right API calls.
http://msdn.microsoft.com/en-us/library/exchange/aa494212(v=exchg.140).aspx
static void GetUserAvailability(ExchangeServiceBinding esb)
{
// Identify the time to compare free/busy information.
Duration duration = new Duration();
duration.StartTime = DateTime.Now;
duration.EndTime = DateTime.Now.AddHours(4);
// Identify the options for comparing free/busy information.
FreeBusyViewOptionsType fbViewOptions = new FreeBusyViewOptionsType();
fbViewOptions.TimeWindow = duration;
fbViewOptions.RequestedView = FreeBusyViewType.MergedOnly;
fbViewOptions.RequestedViewSpecified = true;
fbViewOptions.MergedFreeBusyIntervalInMinutes = 35;
fbViewOptions.MergedFreeBusyIntervalInMinutesSpecified = true;
MailboxData[] mailboxes = new MailboxData[1];
mailboxes[0] = new MailboxData();
// Identify the user mailbox to review for free/busy data.
EmailAddress emailAddress = new EmailAddress();
emailAddress.Address = "tplate#contoso.com";
emailAddress.Name = String.Empty;
mailboxes[0].Email = emailAddress;
mailboxes[0].ExcludeConflicts = false;
// Make the request.
GetUserAvailabilityRequestType request = new GetUserAvailabilityRequestType();
// Set the time zone of the request.
request.TimeZone = new SerializableTimeZone();
request.TimeZone.Bias = 480;
request.TimeZone.StandardTime = new SerializableTimeZoneTime();
request.TimeZone.StandardTime.Bias = 0;
request.TimeZone.StandardTime.DayOfWeek = DayOfWeekType.Sunday.ToString();
request.TimeZone.StandardTime.DayOrder = 1;
request.TimeZone.StandardTime.Month = 11;
request.TimeZone.StandardTime.Time = "02:00:00";
request.TimeZone.DaylightTime = new SerializableTimeZoneTime();
request.TimeZone.DaylightTime.Bias = -60;
request.TimeZone.DaylightTime.DayOfWeek = DayOfWeekType.Sunday.ToString();
request.TimeZone.DaylightTime.DayOrder = 2;
request.TimeZone.DaylightTime.Month = 3;
request.TimeZone.DaylightTime.Time = "02:00:00";
// Add the mailboxes to the request.
request.MailboxDataArray = mailboxes;
// Add the view options to the request.
request.FreeBusyViewOptions = fbViewOptions;
try
{
// Send the request and get the response.
GetUserAvailabilityResponseType response = esb.GetUserAvailability(request);
// Access free/busy information.
if (response.FreeBusyResponseArray.Length < 1)
{
throw new Exception("No free/busy response data available.");
}
else
{
foreach (FreeBusyResponseType fbrt in response.FreeBusyResponseArray)
{
if (fbrt.ResponseMessage.ResponseClass == ResponseClassType.Error)
{
Console.WriteLine(string.Format("Error: {0}", fbrt.ResponseMessage.MessageText));
}
else
{
// Show the free/busy stream.
FreeBusyView fbv = fbrt.FreeBusyView;
Console.WriteLine(string.Format("Merged free/busy data: {0}", fbv.MergedFreeBusy));
}
}
}
}
catch (Exception e)
{
// Perform error processing.
Console.WriteLine(e.Message);
Console.ReadLine();
}
}

Categories