I would like to rig things so that my GCP service account can invite users to calendar events. So for example my-service-account#myproject.iam.gserviceaccount.com would invite user#myCorporation.com to an event. It seems to me that this should be possible simply by giving my-service-account#myproject.iam.gserviceaccount.com permission to use the Calendar API, without having user#myCorporation.com grant any additional permissions.
I tried to implement this example, but replaced the compute scope and the compute API calls with the calendar scope and calendar API calls. My code is returning the error
Insufficient Permission: Request had insufficient authentication scopes.
I've poked around on the internet a bunch, and I cannot tell if the problem is that I did something wrong or if the problem is that Google does not support what I'm trying to do.
Here is my code:
const {google} = require('googleapis');
const compute = google.compute('v1');
const {GoogleAuth} = require('google-auth-library');
async function main() {
const auth = new GoogleAuth({
scopes: ['https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/compute']
});
//keeping the compute stuff in as a sanity check
//to ensure that the problem is with calendar, not something more general
const authClient = await auth.getClient();
const project = await auth.getProjectId();
const res = await compute.zones.list({project, auth: authClient});
console.log(res.data);
createEvent(auth);
}
/**
* Lists the next 10 events on the user's primary calendar.
* #param {google.auth.OAuth2} auth An authorized OAuth2 client.
*/
function createEvent(auth) {
const calendar = google.calendar({version: 'v3', auth});
calendar.events.insert({
calendarId: 'primary',
event: {
"description": "my test event",
"start": {
"date": "2020-05-20",
},
attendees: [{email: "myGuest#mailinator.com"}]
}
}
);
}
main().catch(console.error);
Answer:
You need to enable the APIs and provide scopes in three places: in your auth code, in the GCP console, and the Google Admin console.
More Information:
As I explained in the comments, the code you have provided should run without issue. The Insufficient Permission: Request had insufficient authentication scopes. error is a result of the service account not being given access to the required scopes somewhere on Google's side.
Make sure you have completed the following steps:
Provided the scopes in the application as an auth object (which you have already done):
const auth = new GoogleAuth({
scopes: ['https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/compute']
});
Enabled the Calendar API in the GCP console for your Service Account GCP Project.
Provided the required scopes for your service account in the OAuth consent screen settings in GCP.
Added the required scopes to the service account in the Google Admin console. This is done by following the Security > Advanced Settings > Manage API client access UI elements, and assigning all scopes the service account needs to the service account client ID.
Note: This final step must be done by a domain admin and can not be done by anyone who is not.
In this case, you will need to contact your domain admin to give your project API access.
References:
Google API Console
Google Admin Console
Related Answers:
Google Calendar API. Adding an event to someone calendar throws error “Error 401: invalid_client” just when authenticating
Related
I'm using Firebase v8 with the GoogleAuthProvider.
Firebase documentation provides the following code to authenticate the user.
firebase.auth().signInWithPopup(provider).then((result) => {
/** #type {firebase.auth.OAuthCredential} */
var credential = result.credential;
// This gives you a Google Access Token. You can use it to access the Google API.
var token = credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
})
Questions
Google's Using OAuth 2.0 to Access Google APIs article recommends incremental authorization (it's not Firebase, but the recommendation is clear)
It is generally a best practice to request scopes incrementally, at
the time access is required, rather than up front. For example, an app
that wants to support saving an event to a calendar should not request
Google Calendar access until the user presses the "Add to Calendar"
button.
AFAICT, there is no way to achieve incremental authorization with Firebase without re-authenticating the user. While scopes can be added to GoogleAuthProvider using addScope, a subsequent call to signInWithPopup is required (i.e. the user is re-authenticated). Is there any way to prompt only for authorization (e.g. Drive access) without re-authenticating?
Assuming the access token is short lived, can the Google ID token be used to obtain a new access token? Is re-authenticating the user the only way to obtain a new access token?
Is there a way to determine whether the access token has expired?
I am trying to get my app verified by Google for both of Google Contacts readonly scopes so users can import their contacts into my app.
https://www.googleapis.com/auth/contacts.readonly
https://www.googleapis.com/auth/contacts.other.readonly
I am using Auth0 for handling users in my Next.js app. The package I am using is #auth0/nextjs-auth0. I have initialized auth0 as the documentation suggests.
// pages/api/auth/[...auth0].ts
import {handleAuth} from '#auth0/nextjs-auth0';
export default handleAuth();
In the Google Social connection in Auth0, I have checked the Contacts box and when I login with Google, I see the expected request to allow access to my contacts.
Using the googleapis package, I then call the google.people.connections.list method (which to my understanding is enabled by the contacts.readonly scope) with the Google user's access token and receive back a list of people from my main contacts list, NOT the "other" contacts list.
// This is the abstract Google API service config where the user's access token is set
export interface GoogleServiceConfig {
accessToken: string
}
abstract class AbstractGoogleService {
protected _config: GoogleServiceConfig
constructor(config: GoogleServiceConfig) {
this._config = config
}
}
export default AbstractGoogleService
// This is the Google Contacts Helper service that extends the above Abstract Service
export default class GoogleContactsService extends AbstractGoogleService {
// Search for contacts in user's google account and return them as options
async search(options?: any) {
const service = google.people({ version: 'v1', headers: {
// the accessToken is the one fetched from the Google IDP profile provided by the Auth0 management API
authorization: `Bearer ${this._config.accessToken}`
}})
return service.people.connections.list({
resourceName: 'people/me',
pageSize: 100,
personFields: 'names,emailAddresses'
})
}
}
However, when I submitted my request for app scope verification to Google, they said I was only requesting the https://www.googleapis.com/auth/contacts.other.readonly scope and needed to revise either my scope verification request or my code.
I'm very confused as to where I'm supposed to request the two different Google Contact scopes as it seems that checking the Contacts box in the Auth0 Social Google configuration does request the contacts.readonly scope.
I tried putting the two Google Scopes in my handleAuth() call in [...auth0].ts as shown here https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#access-an-external-api-from-an-api-route but received an error when logging in of...
access_denied (Service not found: https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/contacts.other.readonly)
This is my first time integrating with Auth0 and Google APIs so any help would be much appreciated as I am going in circles now and having troubling making heads or tails.
Thanks!
I was finally able to get approval from Google and wanted to share for the rest of the Googlers out there.
To get approval when checking the Contacts box in Auth0:
I needed to request this scope from Google:
https://www.googleapis.com/auth/contacts
This then let me get verified by Google and call the People API.
I'm trying to use file.getSignedUrl() to get the download URL from Firebase Storage via Google Cloud Functions (Nodejs). I'm getting this error in the Cloud Functions console:
{ SigningError: A Forbidden error was returned while attempting to retrieve an access token for the Compute Engine built-in service account. This may be because the Compute Engine instance does not have the correct permission scopes specified. Permission iam.serviceAccounts.signBlob is required to perform this operation on service account projects/myapp-cd94d/serviceAccounts/myapp-cd94d#appspot.gserviceaccount.com.
at SigningError (/user_code/node_modules/#google-cloud/storage/build/src/file.js:58:9)
at authClient.sign.then.catch.err (/user_code/node_modules/#google-cloud/storage/build/src/file.js:1019:22)
at process._tickDomainCallback (internal/process/next_tick.js:135:7) name: 'SigningError' }
I copied the code from the Add the Firebase Admin SDK to Your Server documentation. I have my serviceAccountKey.json in my functions folder. firebase deploy isn't given me the error
Error parsing triggers: Cannot find module 'serviceAccountKey.json'
so I must have the right path to my serviceAccountKey.json. I even generated a new private key, that didn't fix the problem. I have firebase-admin 6.1.0 and firebase-tools 6.1.0. Here's the relevant parts of my code:
const admin = require('firebase-admin');
var serviceAccount = require("./myapp-cd94d-firebase-adminsdk-1234x-sEcReT.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://myapp-cd94d.firebaseio.com"
});
...
const config = {
action: 'read',
expires: '03-17-2025'
};
file.getSignedUrl(config).then(function(data) {
const url = data[0];
console.log(url);
})
.catch(function(error) {
console.error(error);
})
I saw that Doug Stevenson's answer has different code but it appears to be equivalent to the code in the documentation.
The answer has to do with Cloud Identity and Access Management. First, go to your Google Cloud Platform IAM & admin page. You'll see various service accounts. Look for the service account that looks like myapp-cd99d#appspot.gserviceaccount.com. It should say App Engine default service account in the Name column. (If an error message referenced a different service account, find that service account.)
In the Role column, you may or not see some roles. If you're getting a SigningError message, the Role column is missing the role Service Account Token Creator. Check the checkbox to the left of myapp-cd99d#appspot.gserviceaccount.com to select the service account, and then click the pencil to the right to edit it. In the next screen, click +ADD ANOTHER ROLE. Scroll down to Service Accounts, select Service Account Token Creator, and save. Now you should see Service Account Token Creator in the Roles column for App Engine default service account. Now you have permission to create signed tokens.
Next, repeat these steps and add a role for Storage Object Creator. This will allow you to run getSignedURL().
You could save alternatively assign Service Account Admin and Storage Admin, which include the Service Account Token Creator and Storage Object Creator roles respectively, plus other roles.
Now, if you instead got a SingingError message, it might be because you're warbling Bruce Springsteen's "Glory Days" out of tune. :-)
In my case I had enable Identity and Access Management (IAM), the url is the following one:
https://console.developers.google.com/apis/api/iam.googleapis.com/overview?project="YOUR
PROJECT NAME"
We are using firebase with google authentication. We chose Google because our application makes Google API calls. We authorize these api calls with the access_token included in authorization payload that is returned from firebase. However, we are having trouble figuring out how to refresh the access_token after it expires. According to Google, we should assume the access_token may expire for various reasons.
Therefore, (as I understand it) we need a way to refresh this token without forcing the user to reauthorize. Ideally, I could request the offline access_type when requesting the firebase auth...but I dont see how to do that (short of triggering firebase.authWithOAuthPopup(...) again, which we absolutely do not want to do as the users session is obviously still valid.
Is it possible to get an offline access_type Google oauth token through Firebase so that Google will return a refresh_token (https://developers.google.com/accounts/docs/OAuth2WebServer#formingtheurl)? With a refresh_token, I think I can grab a new access_token for api calls.
I was trying this but its definitely not supported:
this.firebase.authWithOAuthPopup("google", this.authenticateGoogle.bind(this), {
access_type: 'offline', <-- not passed to Google
scope: 'https://www.googleapis.com/auth/userinfo.profile, https://www.googleapis.com/auth/devstorage.read_write'
});
All calls to https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=abcd show the access_type as online.
Thanks
A solution that minimizes server side implementation requirements.
TL:DR; Use the Google Sign-In for Websites library to generate the auth credentials. Login Firebase using the auth credentials, and post the offline access exchange code to your server.
Client Side
Client side I have implemented Google Sign-In for Websites by including the following :
<script src="https://apis.google.com/js/platform.js?onload=loadAuth2" async defer></script>
<script>
function loadAuth2 () {
gapi.load('auth2', function() {
gapi.auth2.init({
client_id: 'your firebase Web client ID',
cookie_policy: 'single_host_origin',
scope: 'profile ...'
});
});
}
</script>
Note: Scope should be a space delimited list of the access scopes you require.
Assuming Firebase is loaded my login click handler is :
<script>
function login() {
const auth = gapi.auth2.getAuthInstance();
auth.then(() => {
auth.grantOfflineAccess({
'redirect_uri': 'postmessage',
'prompt': 'concent',
'approval_prompt': 'force',
}).then(offlineAccessExchangeCode => {
// send offline access exchange code to server ...
const authResp = auth.currentUser.get().getAuthResponse();
const credential = firebase.auth.GoogleAuthProvider.credential(authResp.id_token);
return firebase.auth().signInWithCredential(credential);
}).then(user => {
// do the thing kid!
});
});
}
</script>
Calling auth.grantOfflineAccess with 'redirect_uri': 'postmessage' causes the Google auth2 library to communicate the authentication credentials back to your web app via window.postMessage. See here for the auth2 library reference.
Elsewhere in my application I am listening for Firebase auth state to change.
firebase.auth().onAuthStateChanged(user => {
if (user) {
// navigate to logged in state
} else {
// navigate to login page
}
});
Server Side
I POST the offlineAccessExchangeCode (which looks like {"code": "..."}) to my server to exchange for a creds for the currently authenticated user, which includes a refresh token. Though client side you can access firebase.auth().currentUser.refreshToken this token was not working for me (maybe someone can tell me I was mistaken here :D)
My server side code in Python follows. Please note that the Google SDKs are auto-generated for most Google services, so the following code should translate easily into to any language they support.
from oauth2client import client
// ...
// assuming flask
#app.route("/google/auth/exchange", methods=['POST'])
def google_auth_exchange():
auth_code = request.get_json()['code']
credentials = client.credentials_from_clientsecrets_and_code(
'config/client_secret.json', ['profile', '...'], auth_code)
print(credentials.refresh_token)
And that's pretty much it. I would assume that you have a server or some server side code if you require offline access so hopefully implementing a route isn't too far from an ideal solution.
Sequencing
Note : The GCLID Resolver is a project I am currently working on that required this.
SOLVED for now. According to Rob DiMarco from Firebase: "Unfortunately, it is not currently possible to get a Google OAuth refresh token via Firebase, though it's something we're aware of and hope to fix."
Use a different OAuth 2.0 library in your client code that is able to send an authorization request with the access_type=offline. There's nothing that is firebase specific in the OAuth 2.0 interaction with Google that gets you an access token and a refresh token, so you could rely on separate code for that part. Of course you'll need to provide scope(s) specifically for Firebase (I believe at least "https://www.googleapis.com/auth/freebase") but that's not a problem for any OAuth 2.0 client library.
Solved: Google OAuth Refresh Tokens not returning Valid Access Tokens
You have to handle authentication on a server, then return an idtoken to the client and sign in with firebase after being authenticated on the server. That way you can get refresh tokens on the backend, store them on the user on your database (from the server) and use that refresh token to reauthenticate.
2023 Update: This is now possible! If you follow the instructions here:
https://firebase.google.com/docs/auth/extend-with-blocking-functions#accessing_a_users_identity_provider_oauth_credentials
To create a blocking function, you can get a refresh token. See example code below:
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (context.credential &&
context.credential.providerId === 'google.com') {
const refreshToken = context.credential.refreshToken;
const uid = user.uid;
// These will only be returned if refresh tokens credentials are included
// (enabled by Cloud console).
// TODO: Store or use your refreshToken here!
}
});
Just make sure you register the blocking function after you've deployed it and make sure you select refreshToken :)
Credit: https://stackoverflow.com/a/74989323
Suppose, for the sake of argument, that I already have a facebook access token for a user of my application. In that case, I don't really need to go through Firebase's whole auth.login("facebook") process, I really just want a trusted server to make sure this is a real access token (e.g. by making a GET request to "https://graph.facebook.com/me" with it) and then to set the Firebase user ID appropriately. Can Firebase do this?
Firebase Simple Login was recently updated to support logging in with an existing Facebook access token.
This means that you can integrate directly with then native Facebook JS SDK in your application, and then pass that Facebook access token to Firebase Simple Login (skipping a second pop-up) via:
var ref = new Firebase(...);
var auth = new FirebaseSimpleLogin(ref, function(error, user) { ... });
auth.login('facebook', { access_token: '<ACCESS_TOKEN>' });
See the access_token option on https://www.firebase.com/docs/security/simple-login-facebook.html for more information.