Google API to list users in directory - javascript

I am developing a user page to allow managers to create users by selecting a new user from a drop down list. I will like to populate the drop down with company users from Google directory. So this user page will only be accessible after login.
As I read, Google Admin SDK accesses private user data and needs access token to work. I will like to use this Google directory API users.list method to retrieve users from Google directory. I have a look at the example from quick start for javascript. The issue is this script requires the user to login again, and this would confuse the admin users.
On the login page, I use the google the HTML sign-in button to render Login page, returning the JWT token to our webapp's login endpoint. The returned token credential contains login user's email profile but no access token.
Could someone please advise how to modify below on so that I can retrieve users on the user page.
async function initializeGapiClient() { await gapi.client.init({
apiKey: API_KEY,
discoveryDocs: [DISCOVERY_DOC],
});
await listUsers();
}
async function listUsers() {
let response; try {
const request = {
'customer': 'my_customer',
'maxResults': 10,
'orderBy': 'email',
};
response = await gapi.client.directory.users.list(request);
} catch (err) {
document.getElementById('content').innerText = err.message;
return;
}
const users = response.result.users;
}

The main issue you are having is that you are using JavaScript. Client side JavaScript does not have the ability to store login information for future use. Which means that every time your users return they will need to login and authorize your application again.
I would suggest that you switch to a server sided language which will allow your users to login and for you to store a refresh token for them enabling you to request a new access token when ever you need to. node.js for example would work as well.
Remember though only users who have admin access on your workspace account are going to be able to do this the user will need to have access
Node.js Quickstart
The following is a quick example for node.js, its for an installed app but will show you how things need to be put together for refreshing your access token.
// npm install googleapis#105 #google-cloud/local-auth#2.1.0 --save
// npm install googleapis
const fs = require('fs');
const path = require('path');
const process = require('process');
const {authenticate} = require('#google-cloud/local-auth');
const {google} = require('googleapis');
// If modifying these scopes, delete token.json.
const SCOPES = ['https://www.googleapis.com/auth/admin.directory.user.readonly'];
// Token File Name
const TOKEN_FILE = 'token.json'
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = path.join(process.cwd(), TOKEN_FILE);
const CREDENTIALS_PATH = 'C:\\Development\\FreeLance\\GoogleSamples\\Credentials\\Workspace-Installed-TestEverything.json';
// the workspace customer id found in admin on Google Workspace
const WORKSPACE_CUSTOMER_ID = '[REDACTED]'
/**
* Reads previously authorized credentials from the save file.
*
* #return {Promise<OAuth2Client|null>}
*/
async function loadSavedCredentialsIfExist() {
try {
const content = fs.readFileSync(TOKEN_PATH,{encoding:'utf8', flag:'r'});
const credentials = JSON.parse(content);
return google.auth.fromJSON(credentials);
} catch (err) {
return null;
}
}
/**
* Serializes credentials to a file compatible with GoogleAUth.fromJSON.
*
* #param {OAuth2Client} client
* #return {Promise<void>}
*/
async function saveCredentials(client) {
const content = fs.readFileSync(CREDENTIALS_PATH, {encoding:'utf8', flag:'r'});
const keys = JSON.parse(content);
const key = keys.installed || keys.web;
const payload = JSON.stringify({
type: 'authorized_user',
client_id: key.client_id,
client_secret: key.client_secret,
refresh_token: client.credentials.refresh_token,
});
await fs.writeFileSync(TOKEN_PATH, payload);
}
/**
* Load or request or authorization to call APIs.
*
*/
async function authorize() {
let client = await loadSavedCredentialsIfExist();
if (client) {
return client;
}
client = await authenticate({
scopes: SCOPES,
keyfilePath: CREDENTIALS_PATH,
});
if (client.credentials) {
await saveCredentials(client);
}
return client;
}
/**
* Lists the names and IDs of up to ten users on Google Workspace.
* #param {OAuth2Client} authClient An authorized OAuth2 client.
*/
async function listUsers(authClient) {
const service = google.admin({version: 'directory_v1', auth: authClient});
const res = await service.users.list({
customer: WORKSPACE_CUSTOMER_ID,
pageSize: 10,
fields: 'nextPageToken, users(id, name)',
});
const users = res.data.users;
if (users.length === 0) {
console.log('No users found.');
return;
}
console.log('users:');
users.map((user) => {
console.log(`${user.name.fullName} (${user.id})`);
});
}
authorize().then(listUsers).catch(console.error);
Google workspace
In google workspace under the user, You can check if they have admin access.

Related

How do i connect my authenticated service account Google People API to read contacts on my personal user account?

I have my service account JSON credentials in my environment. I am able to use the Google People API while connected to a service account. But I can't make requests to impersonate my personal user account which owns the platform.
I throws error when i add my user Gmail as subject account.
require("dotenv").config();
const { google } = require("googleapis");
let scopes = ["https://www.googleapis.com/auth/contacts"];
const init = async () => {
console.log("Auth Started");
let auth = await google.auth.getClient({
scopes,
});
// auth.subject = "username#gmail.com";
const { people } = google.people({
version: "v1",
auth,
});
let res = await people.connections.list({
resourceName: "people/me",
personFields: "names",
});
console.log(res);
};
init();

Req.params and req.user missing after hitting stripe checkout flow

I'm trying to integrate Stripe subscriptions billing in my app with a tiered pricing model and based on my understanding, I need to do two things:
Allow my new users to also create a stripe customer account (via the integration)
Monitor Stripe webhook 'events' to provision access while customer subscription payments are active
My userflow is as follows:
create profile in my app (saved to database) -> redirected to stripe checkout portal for billing info (saved to stripe database) -> attempt to save stripe customerId to my database so I can monitor subscription status
However, I can't figure out how to save the customerId info in my app because req.user and req.params are empty as the users are sent back to my app from the stripe billing portal
Controller function
module.exports.stripeWebhook = async (req, res) => {
let data;
const webhookSecret = stripeWebhookSecret;
if (webhookSecret) {
let event;
let signature = req.headers["stripe-signature"];
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
webhookSecret
);
} catch (err) {
console.log(`⚠️ Webhook signature verification failed.`);
return res.sendStatus(400);
}
data = event.data;
eventType = event.type;
} else {
// retrieve the event data directly from the request body.
data = req.body.data;
eventType = req.body.type;
}
switch (eventType) {
case 'payment_intent.succeeded': {
console.log('PaymentIntent was successful!');
break;
}
case 'checkout.session.completed':
// Payment is successful and the subscription is created.
// You should provision the subscription and save the customer ID to your database.
console.log(data.object.customer); <- works
const user = await User.findById(req.user.id); <- comes back empty so my next two lines of code don't work
user.stripeId.push(data.object.customer);
await user.save();
break;
default:
}
res.sendStatus(200);
};
App.js
app.use(bodyParser.raw({type: "application/json"}));
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true }));
I included the app.js code because the bodyparser.raw has an impact on how the body comes through in my controller function.
I was counting on the req.user or req.params to find the user in my database but it's not working. How do I save the customerId to my database like the stripe comments suggest?
You should create Stripe customer account before creating checkout session for the customer.
Check if customer already have stripe_customer account (Stripe customer account). If yes, use that one. If not, create one for him and save it in database.
Set stripe_customer for the checkout session, so customer will be automatically authenticated in the Stripe checkout
You can optionally put user's _id in the metadata of the Stripe checkout session, so you access that data later in the webhook.
Note: You should create one stripe_customer account for each currency. So one user can have multiple stripe_customer accounts, one for each currency.
router.post('/create-checkout-session', authenticate.verifyUser, async (req, res) => {
const { currency } = req.body;
...
// If user does not have stripe customer for order currency, create a new one.
if (!(req.user.stripe_customer && req.user.stripe_customer[currency])) {
const new_stripe_customer = await stripe.customers.create({
email: req.user.email,
metadata: {
user_id: req.user._id.toString(),
},
});
let update = {};
update[`stripe_customer.${currency}`] = new_stripe_customer.id;
await Users.findByIdAndUpdate(req.user._id, update);
if (!req.user.stripe_customer) {
req.user.stripe_customer = {};
req.user.stripe_customer[currency] = new_stripe_customer.id;
} else {
req.user.stripe_customer[currency] = new_stripe_customer.id;
}
}
...
// Set `stripe_customer` for checkout session.
const session = await stripe.checkout.sessions.create({
customer: req.user.stripe_customer[currency],
...
});
...
}

AWS - Get user token from an existing Cognito User Pool with username and password on an existing React web app

I have a simple React web app created from create-react-app.
I also have an existing user pool (set up by a third party) on Amazon Cognito. I have been given a username and password for authentication. Once authenticated, Cognito provides a JWT token.
What I want to achieve is to authenticate the user and get a JWT access_token within the componentDidMount method of the App component; then use the token to call other APIs to retrieve some data and then show the data on the App component.
The following info is known:
region
USER_POOL_ID
APP_CLIENT_ID
USER_ACCOUNT
USER_PASSWORD
AWS provides the authenticate_and_get_token function for Python developers. Is there an equivalent for JavaScript and React developers? Any sample code is appreciated.
Don't know about the authenticate_and_get_token you mentioned, couln't find this function in Boto3 docs.
But I do know different function to retrieve a token called adminInitiateAuth. This is function available for JS.
you can implement this using Lambda, like this:
const AWS = require('aws-sdk');
const USER_POOL_ID = "us-east-1_*******";
const APP_CLIENT_ID = "***********";
const USER_ACCOUNT = "***********";
const USER_PASSWORD = "***********";
const cognitoClient = new AWS.CognitoIdentityServiceProvider({
apiVersion: "2016-04-19",
region: 'us-east-1'
});
exports.handler = async (event) => {
try {
const userData = await userSignIn(USER_ACCOUNT, USER_PASSWORD);
return userData // refer to userData.AuthenticationResult.IdToken;
} catch(e){
throw e;
}
};
const userSignIn = async (username, password) => {
try {
var params = {
AuthFlow: 'ADMIN_NO_SRP_AUTH',
ClientId: APP_CLIENT_ID ,
UserPoolId: USER_POOL_ID,
AuthParameters: {
USERNAME: username,
PASSWORD: password
}
};
return await cognitoClient.adminInitiateAuth(params).promise();
} catch (err) {
return err.message ? err : {message : err};
}
};
Please make sure you have the specific permission to invoke this method, like "Action": "cognito-idp:AdminInitiateAuth", in the lambda permissions, or in IAM role.

After deploying a Javascript Microsoft Botframework template via Azure the provided URL returns an error

After following all the steps from start to finish provided by Microsoft (Tutorial here: https://learn.microsoft.com/en-us/azure/bot-service/javascript/bot-builder-javascript-quickstart?view=azure-bot-service-4.0 ) I've setup the continuous deployment via GIT which works fine.
I've been able to run the code on my localhost and Bot Framework Emulator without any issues. I've also been able to run the bot using the web chat channel iframe provided on the Azure platform.
( https:// webchat.botframework.com/embed/{your-bot}?s={secret})
Lastly I'm also able to run the bot using the "test in web chat" option via Azure.
However, when I try to use the URL provided by azure to test my bot I get the following:
https://{your-domain}.azurewebsites.net/api/messages
{"code":"MethodNotAllowed","message":"GET is not allowed"}
And from https://{your-domain}.azurewebsites.net
{"code":"ResourceNotFound","message":"/ does not exist"}
I've scoured the internet trying to find a solution, all the solutions that I found are using the old version of the framework and point towards a server.get method not being present in index.js.
If there's any more information that I can provide please let me know.
Here's the code from index.js
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// index.js is used to setup and configure your bot
// Import required packages
const path = require('path');
const restify = require('restify');
// Import required bot services. See https://aka.ms/bot-services to learn more about the different parts of a bot.
const { BotFrameworkAdapter, ConversationState, InputHints, MemoryStorage, UserState } = require('botbuilder');
const { FlightBookingRecognizer } = require('./dialogs/flightBookingRecognizer');
// This bot's main dialog.
const { DialogAndWelcomeBot } = require('./bots/dialogAndWelcomeBot');
const { MainDialog } = require('./dialogs/mainDialog');
// the bot's booking dialog
const { BookingDialog } = require('./dialogs/bookingDialog');
const BOOKING_DIALOG = 'bookingDialog';
// Note: Ensure you have a .env file and include LuisAppId, LuisAPIKey and LuisAPIHostName.
const ENV_FILE = path.join(__dirname, '.env');
require('dotenv').config({ path: ENV_FILE });
// Create adapter.
// See https://aka.ms/about-bot-adapter to learn more about adapters.
const adapter = new BotFrameworkAdapter({
appId: process.env.MicrosoftAppId,
appPassword: process.env.MicrosoftAppPassword
});
// Catch-all for errors.
adapter.onTurnError = async (context, error) => {
// This check writes out errors to console log
// NOTE: In production environment, you should consider logging this to Azure
// application insights.
console.error(`\n [onTurnError]: ${ error }`);
// Send a message to the user
const onTurnErrorMessage = `Sorry, it looks like something went wrong!`;
await context.sendActivity(onTurnErrorMessage, onTurnErrorMessage, InputHints.ExpectingInput);
// Clear out state
await conversationState.delete(context);
};
// Define a state store for your bot. See https://aka.ms/about-bot-state to learn more about using MemoryStorage.
// A bot requires a state store to persist the dialog and user state between messages.
// For local development, in-memory storage is used.
// CAUTION: The Memory Storage used here is for local bot debugging only. When the bot
// is restarted, anything stored in memory will be gone.
const memoryStorage = new MemoryStorage();
const conversationState = new ConversationState(memoryStorage);
const userState = new UserState(memoryStorage);
// If configured, pass in the FlightBookingRecognizer. (Defining it externally allows it to be mocked for tests)
const { LuisAppId, LuisAPIKey, LuisAPIHostName } = process.env;
const luisConfig = { applicationId: LuisAppId, endpointKey: LuisAPIKey, endpoint: `https://${ LuisAPIHostName }` };
const luisRecognizer = new FlightBookingRecognizer(luisConfig);
// Create the main dialog.
const bookingDialog = new BookingDialog(BOOKING_DIALOG);
const dialog = new MainDialog(luisRecognizer, bookingDialog);
const bot = new DialogAndWelcomeBot(conversationState, userState, dialog);
// Create HTTP server
const server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function() {
console.log(`\n${ server.name } listening to ${ server.url }`);
console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`);
console.log(`\nTo test your bot, see: https://aka.ms/debug-with-emulator`);
});
// Listen for incoming activities and route them to your bot main dialog.
server.post('/api/messages', (req, res) => {
// Route received a request to adapter for processing
adapter.processActivity(req, res, async (turnContext) => {
// route to bot activity handler.
await bot.run(turnContext);
});
});
The Bot Framework /api/messages endpoint is configured to accept POST requests. Navigating to https://{your-domain}.azurewebsites.net/api/messages is a GET request, which is why are getting the "Resource not found" response. The "Resource not found" response actually tells you that your bot is running. If the web app wasn't running or the initial deployment failed, you would see a "The resource you are looking for has been removed, had its name changed, or is temporarily unavailable" message. As long as Test in Web Chat works, you are good to go!
If you want to add a static HTML page to your server, there are a few changes I would recommend adding to your bot.
Direct Line Token Endpoint
First, you should add another endpoint to your bot to generate a Direct Line Token. Here is a basic implementation. For more details I recommend you take a look at the Direct Line Authentication documentation.
// DirectLine Token
server.post('/directline/token', async (req, res) => {
const id = (req.body && req.body.id)? `dl_${req.body.id}`: `dl_default_user`
const options = {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.directLineSecret}`,
'Content-Type': 'application/json',
},
url: 'https://directline.botframework.com/v3/directline/tokens/generate',
data: {
user: { id }
}
};
try {
const { data } = await axios(options);
res.send(data);
} catch ({ message }) {
res.status(403);
res.send({ message });
}
});
Serve Static Files
Next, create a public file in your root directory and add a new endpoint to your bot with the Restify serveStatic plugin. Take a look at the Restify Plugin documentation for more details.
server.get('/*', restify.plugins.serveStatic({
directory: path.join(__dirname, 'public'),
default: 'index.html'
}));
index.html
Finally, in your public directory, create an index.html and add the following code. It is configured to request a token from your /directline/token endpoint and render an instance of Web Chat.
<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Web Chat: Full-featured bundle</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.botframework.com/botframework-webchat/latest/webchat.js"></script>
<style>
html, body { height: 100% }
body { margin: 0 }
#webchat {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="webchat" role="main"></div>
<script>
(async function () {
const res = await fetch('/directline/token', { method: 'POST' });
const { token } = await res.json();
window.WebChat.renderWebChat({
directLine: window.WebChat.createDirectLine({ token })
}, document.getElementById('webchat'));
document.querySelector('#webchat > *').focus();
})().catch(err => console.error(err));
</script>
</body>
</html>
Hope this helps!

Using a separate Google account for backend processes in MeteorJS

I'm creating an application for YouTube that utilizes some of the Analytics APIs for Content Owners. The APIs require a user with sufficient permissions to be logged in, who can then retrieve reports for all the users of our application.
Currently our application can get YouTube User IDs, which is fine, but we need a separate account (other than the current user) to make requests to the API using the logged in user's ID.
How can I implement such a setup? I know it would involve using offline authentication and periodically refreshing the access tokens, but I'm not quite sure how to do it.
I've done a Google Analytics dashboard that refreshes the token in an interval. The admin chooses the GA Profile and it plots things. I needed to use a bunch of stuff to do that:
Npm Integration - So easy to use. Just take a look how to make methods calls sync.
google-api-nodejs-client [alpha] - Integrate it with the Npm above. It refreshes the token for you automatically when you Make Authenticated Requests
If you don't want to use google-apis-nodejs-client to refresh your token, you can use this code I've made to refresh the token by yourself:
var googleAccount = Accounts.loginServiceConfiguration.findOne({service: 'google'});
CLIENT_ID = googleAccount.clientId;
CLIENT_SECRET = googleAccount.secret;
REDIRECT_URL = '/_oauth/google?close';
var googleapis = Meteor.require('googleapis'),
OAuth2Client = googleapis.OAuth2Client,
client = getClient();
function getClient () {
var client = Meteor.sync(function (done) {
googleapis.discover('analytics', 'v3').execute(function (err, client) {
done(err, client);
});
});
if (client.err)
throw new Meteor.Error(400, 'Client not received');
return client.result;
}
function getOAuth2Client (user) {
var accessToken = user.services.google.accessToken,
refreshToken = user.services.google.refreshToken,
oauth2Client = new OAuth2Client(CLIENT_ID, CLIENT_SECRET, REDIRECT_URL);
if (user.services.google.expiresAt < +(new Date())) {
var res = Meteor.http.call("POST", "https://accounts.google.com/o/oauth2/token",
{params: {
grant_type : 'refresh_token',
refresh_token : refreshToken,
client_id : CLIENT_ID,
client_secret : CLIENT_SECRET
}, headers: {
"content-type": "application/x-www-form-urlencoded"
}});
accessToken = res.data.access_token;
Meteor.users.update({_id: user._id}, {$set: {
'services.google.accessToken': accessToken,
'services.google.expiresAt': +(new Date()) + (1000 * res.data.expires_in)
}});
}
oauth2Client.credentials = {
access_token: accessToken,
refresh_token: refreshToken
};
return oauth2Client;
}
Meteor.methods({
'getAccounts': function () {
var user = Meteor.users.findOne({_id: this.userId}),
oauth2Client = getOAuth2Client(user),
accounts = getAccounts(oauth2Client, client);
return accounts;
}
});

Categories