My app is displaying list of events from users gmail "primary" calendar (and creates/deletes events). App works fine with one of my gmail accounts, when I try to sign in with other gmail accounts I get an error when fetching events:
GET 403, and in response => Request had insufficient authentication scopes.
My code for loading calendar:
window.gapi.load("client:auth2", () => {
console.log("Client loaded!!");
window.gapi.client.init({
apiKey: "MY_API_KEY",
clientId:
"MY_CLIENT_ID",
discoveryDocs: [
"https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",
],
scope: "https://www.googleapis.com/auth/calendar.events",
});
window.gapi.client.load("calendar", "v3", () => {
console.log("Calendar loaded!!");
// List events after calendar is loaded
listUpcomingEvents("week");
});
});
And in my listUpcomingEvents() function I fetch events with pure js(no backend):
window.gapi.client.calendar.events
.list({
calendarId: "primary",
timeMin: start.toISOString(),
timeMax: end,
showDeleted: false,
singleEvents: true,
maxResults: 100,
orderBy: "startTime",
})
.then(function (response) {
var events = response.result.items;
console.log("Events:", events);
// Set events
setEvents([...events]);
setEventsFilter(events_filter);
});
I have tried setting scopes in my Google Cloud Platform (oauth consent screen), switched to other scopes or add more than scope but nothing works. I have even tried to make calendar public and bunch of other things i found on stackoverflow.
Request had insufficient authentication scopes.
Means that the user has authenticated with one scope but the method you are using requires another.
The method Events.list requires one of the following scopes
Now the code you have posted shows scope: "https://www.googleapis.com/auth/calendar.events", which is one of the scopes that should give you access to list events.
The issue is that when you ran that code the consent screen popped up and the user authorized your application. You then changed the scope to https://www.googleapis.com/auth/calendar.events and ran your code again. But the new consent screen didn't popup because the user is currently authenticated already.
You need to fore your app to request authorization of the user again, at that time the new scope will show up and if the user consents to it you will be able to use that method.
The easiest way to do this is to go to the users google account and revoke the access that was granted to your application. Google account
Or as your application is JavaScript you should be able to wait an hour and your access should expire and the user will be asked to consent again.
Related
I am currently developing a react web app that will use Microsoft's MSAL package to authenticate users to ensure only users within our tenant may access the Api.
I've built a http function app called TARGET_APP with a python function that accesses our data and returns it. I registered it to our Azure AD enterprise applications.
Now according to the documentation for proper "On Behalf Of" calls to work I am to register another app to represent my react client app, called CALLER_APP I registered this as well, and set up the scopes I need which include email, user.read, and the TARGET_APP's exposed Api.
Example of my CALLER_APP permissions here:
However when attempting to authorize with the CALLER_APP from the client, via MSAL with the scopes in the image, I get a prompt saying "Admin consent required"
Snippet from my authentication flow (handleLogin is the initiating function called) :
const msalConfig = {
auth: {
authority: "https://login.microsoftonline.com/MY_TENANT/",
clientId: "CALLER_APP_CLIENT_ID",
redirectUri,
postLogoutRedirectUri: redirectUri
},
cache: {
cacheLocation: "localStorage"
}
}
// NOTE I have subbed out my actual caller scope with "CALLER_APP_SCOPE" for this post
const loginRequest = {
scopes: ["CALLER_APP_SCOPE", "user.read", "email"]
};
async function handleLogin(instance) {
const loginUrl = await getLoginUrl(instance, loginRequest);
const loginResult = await launchWebAuthFlow(instance, loginUrl);
// Acquire token
const { accessToken } = await acquireToken(instance, loginRequest);
console.log(accessToken)
}
/**
* Generates a login url
*/
async function getLoginUrl(instance, request) {
return new Promise((resolve, reject) => {
instance.loginRedirect({
...request,
onRedirectNavigate: (url) => {
resolve(url);
return false;
}
}).catch(reject);
});
}
/**
* Generates a login url
*/
async function launchWebAuthFlow(instance, url) {
return new Promise((resolve, reject) => {
chrome.identity.launchWebAuthFlow({
interactive: true,
url
}, (responseUrl) => {
// Response urls includes a hash (login, acquire token calls)
if (responseUrl.includes("#")) {
instance.handleRedirectPromise(`#${responseUrl.split("#")[1]}`)
.then(resolve)
.catch(reject)
} else {
// Logout calls
resolve();
}
})
})
}
/**
* Attempts to silent acquire an access token, falling back to interactive.
*/
async function acquireToken(instance, request) {
return instance.acquireTokenSilent(request).then((response) => {
console.log(response.accessToken);
}).catch(async (error) => {
console.error(error);
storage.set({'loggedState': false});
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Most of this code is taken directly from the documentation,
Calling the handleLogin function initiates the authentication flow successfully, however when I log in with my Microsoft credentials I receive the "App needs permission to access resources in your organisation that only an admin can grant" popup.
I double checked my scopes and ensured none require admin consent, as well as I have gone into the enterprise apps user consent and permissions settings and enabled user consent on low impact scopes as you can see here:
Enterprise Setting:
The "3 permissions classified as low impact" are the 3 scopes described above (email, user.read, allow-caller)
However,
If I go to the "Expose an API" blade instead for the CALLER_APP and make a scope there, and use that scope in the MSAL call instead, authentication goes through fully, I get a bearer token, and I am able to use the API for what I need.
This method is not mentioned in the documentation, nor any of the readings I've looked into though.
I was wondering if I could get help in understand why I shouldn't use "Expose an API" for my case, as well as why it requires admin consent?
Usually the permissions in the API permissions are selected where
user.read , email are graph permissions and when you mention
User.Read while calling msal it indirectly means
https://graph.microsoft.com/User.read which is the basic permission
to sign in user to read users profile and mail.
But the scope for calling your web api is created by you and it has different AppId or say App ID URI for different applications and its scope needs to be defined uniquely for that App to access that.
So actual scopes for that app to access the Api are exposed in expose an api blade which is the scope of the App to access.
NOTE:Actual full value/string of the Scope is the concatenation of your web API's Application ID URI and Scope name of scope. The
App ID URI acts as the prefix for the scopes you'll reference in your
API's code, and it must be globally unique.
For example,
if your web API's application ID URI is https://contoso.com/ and the
scope name is Employees.Read.All, the full scope is:
https://contoso.com/Employees.Read.All or
api://<application-client-id>/allow-caller in your case.
And coming to the point that it is asking admin consent is , when
there is no scope that actually means full string scope
api:///allow-caller , only mentioning
allow-caller is totally different scope and this new scope may
require consent from admin as it is not exposed for that particular
API.
Also you can add a client application in expose an api blade in case you don’t want to see the admin consent as the "authorized client applications" is used when you basically want to preauthorize users without admin consent being required to access that api ,If not it will prompt users for consent if needed.
Please check the below image:
References:
quickstart-configure-app-expose-web-api(github)
azure-expose an Api vs Api-permissions(stackOverflow)
I already have a project to query google calendar apis.
I wanted to go further by querying google mail apis.
In my project I have activated mail API
I have added discoveryDocs in my javascript app like this
[
"https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",
"https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"
]
And scopes like this
"https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events https://mail.google.com/"
Initialization of my client is done like this:
initClient(): void {
gapi.client.init({
apiKey: 'ma_api_key',
clientId: 'my_client_id.apps.googleusercontent.com',
discoveryDocs: [
"https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest",
"https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"
],
scope: 'https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events https://mail.google.com/',
}).then(() => {
console.log('ok')
});
}
I still can get my calendars and events but I can not get my labels with this code:
getMails(): void {
console.log(gapi);
gapi.client.gmail.users.labels.list({
'userId': 'me'
}).then(function(response) {
var labels = response.result.labels;
console.log(labels);
});
}
What am I missing please ?
Thanks
Request had insufficient authentication scopes javascript
Means that the user who you have authentication with has authorized your application to use some scopes but they are not the scopes, but you are trying to use a method which requires addental scopes then the user has authorized your application for.
This error normally occurs when you authorize your application once then change the scopes and run it again, if your application still has a session var or cookies from the previous authorization request then your application will run without requesting access of the user and adding the additional scopes.
You need to revoke the access token or force the application show the consent screen again
On my website, I am asking for google calendar access. I can edit the user calendar but, I don't want to ask for user permission, again and again, so once the user authorized and give access to google calendar, I can edit it anytime until the user revokes the access. Should I implement it on the frontend or the backend and how? I checked few answers where they mention we can use a service account but, it is not clear how can I edit or read the individual user's calendar events and how can I remove it once the user revokes access. This question was deleted because code was missing so adding code below.
I tried this so once user login I get access token and I am using it
window.gapi.load("client:auth2", () => {
window.gapi.client.setApiKey("api_key");
window.gapi.client.load("https://content.googleapis.com/discovery/v1/apis/calendar/v3/rest")
.then(() => {
window.gapi.auth.setToken({ access_token: access_token })
window.gapi.client.calendar.events.insert({
"calendarId": "id",
'resource': event
}).then((res) => {
console.log("calendar data res "+JSON.stringify(res))
}).catch(err => console.log("error getting calendar data "+JSON.stringify(err)))
}).catch(err => console.error("Error loading GAPI client for API", err) )
})
but once access token expires how can I get a new access token( I don't want to show login popup to the user again and again. I want to know how can I do it using refresh token on client-side).
You can't get a refresh token on the client-side without exposing your secret key to the public.
You can create an endpoint that accepts oAuth code and return the token, save the refresh token for later. You set up a corn job that checks for expired token and refreshes them.
Every time the user accesses your app, you grab a fresh token from the server and proceed to work normally.
As per Google guidelines. You do POST to https://oauth2.googleapis.com/token. Assuming your server-side stack is in Node.js, you do something like this using an HTTP client like Axios:
const Axios = require('axios');
const Qs = require('querystring');
const GOOGLE_CLIENT_ID = 'abc';
const GOOGLE_CLIENT_SECRET = '123';
let refreshToken = getFromDataBase(); // should be stored in database
Axios.post('https://oauth2.googleapis.com/token', Qs.stringify({
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
refresh_token: refreshToken,
grant_type: 'refresh_token'
}), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
})
.then(({ data }) => console.log(data.access_token)) // new token that expires in ~1 hour
.catch(console.log)
Firstly, do you (a) want to update the calendar when the user is not logged in, for example in response to an external event? ... OR ... do you (b) only want to update the calendar from within a browser session?
If (a), then you need to ask the user for offline access which will give you a Refresh Token , which you can securely store on a server and use whenever you need to. (Forget all about Service Accounts).
If (b), then you need the following pieces of information :-
When the access token expires, request access again, but add the flag prompt=none. This will give you a fresh Access Token without the user seeing any UX.
Do this in a hidden iframe so that it is happening in the background and is invisible to the user. Your iframe will therefore always have an up to date Access Token which it can share with your app via localStorage or postMessage.
My app makes use of Firebase to log users in, and they have access to a calendar, which is especially created as a single common calendar to add and remove reservations.
The app has, so to speak, its own gmail address for this purpose. The app is accessed via the google API (gapi client).
In order for the users to be able to interact with the calendar, I have found out that I can only use the service account of the calendar / app.
So far I managed to:
create a service key for the service account,
share the calendar with the service account "dummy" user,
use that service key (certificate) to create a JWT token (using jsrsasign library),
make a POST request to get an access token for gapi,
initialise the gapi auth and client, and have access to the calendar via gapi
Now when I get to the point of retrieving the google Calendar events, I do get a successful response, but the array of events is empty, even though there are test events available in the shared calendar.
The response looks like this:
{
"kind": "calendar#events",
"etag": "\"pqef3g4h5j6j0g\"",
"summary": "my_app_email#appspot.gserviceaccount.com",
"updated": "2019-01-15T21:14:05.029Z",
"timeZone": "UTC",
"accessRole": "owner",
"defaultReminders": [],
"items": []
}
There are a few topics on Stackoverflow regarding this, but none of them have helpful information, or they are for Pythin / PHP.
I am hoping someone can give advice with this for Javascript...
I resolved this... The problem was in the gapi request, when fetching the events.
I was using the wrong calendarId. I had its value set to the default 'primary', but the actual calendarId to be used, can be found under the Google Calendar Settings >> Integrate Calendar. In my settings, the calendarId was the associated account's email address.
So the gapi request looks like this:
const fetchTimeLimit = new Date(2019, 0, 1).toISOString();
let events = await gapi.client.calendar.events.list({
calendarId: 'calendar_email#gmail.com',
timeMin: fetchTimeLimit,
showDeleted: false,
singleEvents: true,
maxResults: 300,
orderBy: 'startTime'
})
.then(response => { ........etc
So I have this code and it is working properly ONLY when I am logged in w/ my google account. When I'm not logged in I have to log in in order to continue my create event function. Is there any way to make it so that anyone can add event to my public calendar? I've searched many places but I can't seem to find any answers. :P
var CLIENT_ID = 'MY CLIENT_ID GOES HERE';
var SCOPES = ["https://www.googleapis.com/auth/calendar"];
checkAuth();
function checkAuth() {
gapi.auth.authorize(
{
'client_id': CLIENT_ID,
'scope': SCOPES.join(' '),
'immediate': true
}, handleAuthResult);
}
function handleAuthResult(authResult) {
if (authResult && !authResult.error) {
console.log(authResult);
loadCalendarApi();
} else {
gapi.auth.authorize(
{client_id: CLIENT_ID, scope: SCOPES, immediate: false},
handleAuthResult);
return false;
}
}
function loadCalendarApi() {
gapi.client.load('calendar', 'v3', createEvent);
}
function createEvent(){
var resource = {
"summary": eventName,
"location": location,
"description": rid,
"start": {
"dateTime": startNew
},
"end": {
"dateTime": endNew
},
};
var request = gapi.client.calendar.events.insert({
'calendarId': 'MY CALENDAR ID GOES HERE#group.calendar.google.com',
'resource': resource
});
request.execute(function(resp) {
console.log(resp);
callback();
});
}
Notice some of the codes where left out. But everything is working properly.
You can try creating a service account:
A service account's credentials include a generated email address that is unique and at least one public/private key pair.
Your application now has the authority to make API calls as users in your domain (to "impersonate" users). When you prepare to make authorized API calls, you specify the user to impersonate.
You can follow this step from thread:
You need to go to the Google Developer's console and mark your account as a 'service account'. This will differentiate it from a web application. The important difference is that nobody will be prompted to log in to their account before the events are added since the account will belong to your application, not an end user. For more information see this article, starting on page 5.
You need to create a public/private key pair. From the developer's console, click on Credentials. Under you service account, click on 'Generate new P12 key'. You'll need to store this somewhere. That file location becomes the $key_file_location variable string in the code below.
Also from the developer's console, you need to enable the Calendar API.
In Google Calendar that you want to add events to, under settings, click Calendar Settings then on 'Share this Calendar' at the top. Under 'Share with specific people' in the 'Person' field, paste in the email address from the service account credentials. Change the permission settings to 'Make changes to events'. Don't forget to save the change.
There are also some links in the thread that would help you on implementing service account to your website.
I hope this helps.