Not sure where to request Google Contacts API scopes when using Auth0 + Next.js - next.js

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.

Related

IdentityServer4 Google sign callback doesn't include "IdentityConstants.ExternalScheme" cookie

I have an IdentityServer4 identity provider server. For the most part, I am using the template code from their repo. I am trying to add Google sign-in. I configured the GoogleSignIn in startup and added ClientId/ClientSecret.
When I don't configure the return URIs in the GCP project I get the following error from Google:
"The redirect URI in the request, https://localhost:44333/signin-google, does not match the ones authorized for the OAuth client. To update the authorized redirect URIs..."
When I add the URI
Then as soon as I call Challenge I immediately get a failed callback from Google.
[HttpGet]
public async Task<IActionResult> Callback()
{
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme);
// Here, result.Succeeded is false
// Rest of the method...
}
What could be the problem?
If you have made it to the Callback method, then it sounds like Google auth has completed successfully. However, since the IdentityConstants.ExternalScheme cookie is not present, it sounds like you might have a little misconfiguration.
Once the Google authentication handler has completed, it will sign in using the auth scheme set in its SignInScheme property or the default sign-in scheme. It stores the claims from Google into a local auth method, such as a cookie.
What scheme is the Google authentication handler configured to use? If you're using the quickstarts, it may be using IdentityServerConstants.ExternalCookieAuthenticationScheme rather than ASP.NET Identity's IdentityConstants.ExternalScheme that you are looking for.

Creating Google Calendar events with a GCP Service Account

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

G Suite identity provider for an AWS driven browser based App

I'm aware of how to create a Google authenticated app via with google-signin-client_id 3089273xx-xxxxxxxxxxxx.apps.googleusercontent.com & <script src="https://apis.google.com/js/platform.js" async defer></script>, but the problem here is that, I have not been able to LIMIT the login to just my company's G Suite instance.
The app I have is a "serverless" JS bundle hosted on S3. The logged in Google token is tied to an AWS role that accesses sensitive resources.
So typical solutions to check the email of googleUser.getBasicProfile() or pass a hd parameter don't make any security sense since they can be manipulated with browser dev tools IIUC.
Is there some other Google API I could be using or strategy I could apply? I imagine the solution would come in the form of a special google-signin-client_id for my company's domain which is hosted by G Suite. This is how it's tied to the role at AWS:
I'm aware I could setup duplicate my users in AWS "user pools" and use Cognito, but I am trying to have a "single source of truth" for the company's employees & ease the administration burden.
UPDATE: This answer is insecure as if you simply remove hosted_domain, you can authenticate with any Google login.
After straying upon https://developers.google.com/identity/work/it-apps & using GAPI directly I found I could do a
GAPI.auth2.init({
client_id: CLIENT_ID,
hosted_domain: 'example.com'
})
And then as the documentation advises, you setup Manage API client access
So now only users of #example.com on Gsuite can access this JS app! This took weeks to figure out. So just to conclude, how to authenticate using Google on a AWS powered serverless app:
Setup a client ID via OAuth client ID with your whitelisted origin URLs from https://console.developers.google.com/apis/credentials
In AWS IAM setup a Role with Google as the (web) Identity provider with the client ID
Add your client ID https://admin.google.com/AdminHome?chromeless=1#OGX:ManageOauthClients as documented here https://developers.google.com/identity/work/it-apps to crucially limit your application to your company's domain.
So now we have a statically hosted App limited to only company employees to access sensitive paid AWS APIs.
I tried 3 different options, the first one worked for my scenario:
First Option - Validating Google Id Token on each call on lambda side
I always pass the id_token as a header on the client calls(web and mobile apps).
"acceptableHds" Is the list of allowed domains.
const oauth = new Auth.OAuth2(CLIENT_ID_WEB, CLIENT_SECRET);
oauth.verifyIdToken(token, null, (err, ticket) => {
if (err) {
return reject(err);
}
const payload = ticket.getPayload();
const tokenIsOK = payload &&
payload.aud === CLIENT_ID &&
new Date(payload.exp * 1000) > new Date() &&
acceptableISSs.has(payload.iss) &&
acceptableHds.has(payload.hd)
return tokenIsOK ? resolve(payload.hd) : reject();
});
Second Option - Validating Google Id Token once on lambda side
I started this alternative way but I didn't finished because the first solutions fitted to my needs and the milestones was close(it needs a indentity pool):
1)Send the id_token to the lambda function and validate it on Google API(here is where you can check the domain using the code above)
2)Call the cognitoidentity.getOpenIdTokenForDeveloperIdentity on the lambda side using the id_token coming from the browser
3) On the client, call any of the Cognito or STS functions like assumeWebIdentity, AssumeRole using the tokens returned from getOpenIdToken.
function getCognitoToken(id_token) {
var param = {
IdentityPoolId: 'us-east-1:f7b3d55f-6b63-4097-be8f-3dc22ddec1a4',
Logins: { 'accounts.google.com': id_token }
}
return check_company(id_token).then(function (valid) {
return cognitoidentity.getOpenIdTokenForDeveloperIdentity(param).promise()
})
I couldn't finish the third step. You need use the tokens received on the second step without revealing the 'identity pool id'. If you do that and assure that the role can't list identity pool ids, it will work as intended and It will be secure.
Third Option - SAML provider
You can create a SAML provider and use SAML assertions to validate the user domain.
http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html
I failed miserably trying to do it.
P.S.: Google Admin let you create private apps, limiting to you company domains, but It works only for mobile as far as I know
https://support.google.com/a/answer/2494992?hl=en
Hope it helps someone!

Firebase Google Auth offline access_type in order to get a token refresh

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

Does Firebase support validating a pre-existing facebook access token?

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.

Resources