How to use OAuth2 with node.js - google-bigquery

I would like to use #google-cloud client lib to insert data to BigQuery.
Since I have multiple clients and each has different IAM role I can't use a service account like this:
const bigquery = new BigQuery({
projectId: `myProject`,
keyFilename: '/Users/services/ssh/myProject-111.json'
});
rather I would like to use client-specific oauth2 like this:
const bigquery = new BigQuery({
projectId: `mydata-1470162410749`,
token: clientOauth2Token
});
I get this error
Error in the process: Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project. - Invalid Credentials
This is the full mocha test code I'm using:
import BigQuery from '#google-cloud/bigquery';
import {GoogApi} from "../apiManager" //Private code to get Token from client DB
if (!global._babelPolyfill) {
var a = require("babel-polyfill")
}
describe('Check routing', async () => {
it('Test stack ', async (done) => {
//Fetch client Auth from local Database
let apiManager = new GoogApi({query: {integrationTest: 'on'}}, {}, (err, httpResults) => {});
let clientAuth = await apiManager.getGoogleTokensByUserId(`user#company.con`);
//Replace the 2 value below with real values
const tableName = "myTest";
const dataset = "EVALUEX_DEV";
try {
const bigquery = new BigQuery({
projectId: `myProject`,
token: clientAuth.credentials.access_token
});
await bigquery.createDataset(dataset)
.then(
args => {
console.log(`Create dataset, result is: ${args}`)
})
.catch(err => {
console.log(`Error in the process: ${err.message}`)
})
} catch (err) {
console.log("err", err)
}
})
})
This is how my Token looks like when I Inspect it
{
"domain": null,
"_events": {},
"_eventsCount": 0,
"transporter": {},
"credentials": {
"access_token": "My Token",
"refresh_token": "my Refresh Token"
},
"certificateExpiry": null,
"refreshTokenPromises": [],
"_clientId": "my Client Id",
"_clientSecret": "My client secret",
"redirectUri": "https://s1dg0yjhih.execute-api.us-east-1.amazonaws.com/stage1/goog/saveToken",
"eagerRefreshThresholdMillis": 300000
}

The JSON object you shared is not an OAuth token, it looks like a OAuth2Client object from #google-auth-library.
An actual OAuth token is simply a string, like "ya29.ABC123ABC123_cG123ABCDETCETCETC".
That "token" is actually an OAuth2Client then you can fetch the token with the getAccessToken method. If it is just a plain JSON object, then you can get the credentials.access_token field, is the actual access token.
Note that access tokens expire. getAccessToken will get a new one if necessary, but just getting the access_token from the object will not, and can result in 403s after it expires.

Related

How to get user email using Google Sign In expo Auth Session?

At moment im using this snippet of code to sign in to google, but i cant get user email… anyone know how to do this?
var LoginGoogle = () => {
const [request, response, promptAsync] = Google.useAuthRequest({
androidClientId: 'xxxxxxx.apps.googleusercontent.com',
expoClientId: 'xxxxxxx.apps.googleusercontent.com'
},{
scopes: ["email"]
},{});
React.useEffect(() => {
if (response?.type === 'success') {
const { authentication } = response;
console.log(response);
}
}, [response]);
return (
<GoogleSocialButton disabled={!request} onPress={() => {promptAsync()}} />
)
}
response returns object with links instead of email
I wish this is written in the expo docs. I would like to add a few points from the first answer:
First if you need code snippets on how to fetch user data after getting the access token, you can refer to this github issue: https://github.com/expo/expo/issues/8384
access token can be received by the following code after receiving the response object:
const { authentication: { accessToken } } = response;
then, you can create a function like this:
async function fetchUserInfo(token) {
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
});
return await response.json();
}
and get the user object (which contains the user email, profile, photo, etc) by something like this inside an async function:
const user = await fetchUserInfo(accessToken);
But NOTE for the user object, using https://www.googleapis.com/oauth2/v2/userinfo and https://www.googleapis.com/oauth2/v3/userinfo, will yield slightly different result/object ; in particular for v3, since Google implements the OpenID Connect API, there is no "id" attribute anymore, "id" will be called "sub".
sources:
How to identify a Google OAuth2 user?
https://developers.google.com/assistant/identity/google-sign-in-oauth
https://github.com/expo/expo/issues/8384
Example of a user object in v3:
Object {
"email": "xxxxx#gmail.com",
"email_verified": true,
"family_name": "John Deer",
"given_name": "John",
"hd": "gmail.com",
"locale": "en",
"name": "John Deer",
"picture": "https://lh3.googleusercontent.com/a/asdfjasdklfjaslkf",
"sub": "10998837733652322",
}
Hope this helps someone in the future...!
EDIT: if you need the id_token checkout this one:
expo-auth-session/providers/google Google.useAuthRequest
I am using AuthSession as well in my RN app and I stumbled with this problem. After going through Google API Docs, found out you can pass the access token from the useAuthRequest response to https://www.googleapis.com/oauth2/v3/userinfo?access_token= ACCESS_TOKEN.

Firestore cloud functions apollo graphql authentication

I need help getting my Firebase Apollo/GraphQL Cloud Function to authenticate and receive query requests.
I implemented an Apollo/GraphQL server as a Cloud Function in
Firebase/Firestore using this repository from this post.
I set permissions for the cloud function to
allAuthenticatedUsers and I am using Firebase Phone
Authentication to authenticate.
I used code from this stackoverflow answer to help structure the
authentication portion not included in the initial repository.
The Apollo/GraphQL function works fine (tested with playground) when permissions are set to allUsers. After setting permissions to allAuthenticatedUsers and attempting to send authenticated queries I am receiving the following error response:
Bearer error="invalid_token" error_description="The access token could not be verified"
I believe I am making a mistake with the request sent by the client, and or the handling of the verification and "context" of the ApolloServer. I have confirmed the initial user token is correct. My current theory is that I am sending the wrong header, or messing up the syntax somehow at either the client or server level.
To explain what I believe the appropriate flow of the request should be:
Token generated in client
Query sent from client with token as header
ApolloServer cloud function receives request
Token is verified by Firebase, provides new verified header token
Server accepts query with new verified header token and returns data
If anyone can explain how to send valid authenticated client queries to a Firebase Apollo/GraphQL Cloud Function the help would be greatly appreciated. Code for server and client below.
Server.js (ApolloServer)
/* Assume proper imports */
/* Initialize Firebase Admin SDK */
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "[db-url]",
});
/* Async verification with user token */
const verify = async (idToken) => {
var newToken = idToken.replace("Bearer ", "");
let header = await admin.auth().verifyIdToken(newToken)
.then(function(decodedToken) {
let uid = decodedToken.uid;
// Not sure if I should be using the .uid from above as the token?
// Also, not sure if returning the below object is acceptable, or
// if this is even the correct header to send to firebase from Apollo
return {
"Authorization": `Bearer ${decodedToken}`
}
}).catch(function(error) {
// Handle error
return null
});
return header
}
/* Server */
function gqlServer() {
const app = express();
const apolloServer = new ApolloServer({
typeDefs: schema,
resolvers,
context: async ({ req, res }) => {
const verified = await verify(req.headers.Authorization)
console.log('log verified', verified)
return {
headers: verified ? verified: '',
req,
res,
}
},
// Enable graphiql gui
introspection: true,
playground: true
});
apolloServer.applyMiddleware({app, path: '/', cors: true});
return app;
}
export default gqlServer;
Client.js (ApolloClient)
Client query constructed using these instructions.
/* Assume appropriate imports */
/* React Native firebase auth */
firebase.auth().onAuthStateChanged(async (user) => {
const userToken = await user.getIdToken();
/* Client creation */
const client = new ApolloClient({
uri: '[Firebase Cloud Function URL]',
headers: {
Authorization: userToken ? `Bearer ${userToken}` : ''
},
cache: new InMemoryCache(),
});
/* Query test */
client.query({
query: gql`
{
hello
}
`
}).then(
(result) => console.log('log query result', result)
).catch(
(error) => console.log('query error', error)
)
})
UPDATE 05/03/20
I may have found the source of the error. I won't post an answer until I confirm, but here's the update. Looks like allAuthenticatedUsers is a role specific to Google accounts and not Firebase. See this part of the google docs and this stackoverflow answer.
I will do some testing but the solution may be to change the permissions to allUsers which may still require authentication. If I can get it working I will update with an answer.
I was able to get things working. Working requests required the following changes:
Change cloud function "invoker" role to include allUsers instead of allAuthenticatedUsers. This because the allUsers role makes the function available to http requests (you can still require authentication through sdk verification)
Adjusting the code for the server and client as shown below. Minor change to header string construction.
Server.js (ApolloServer)
/* Assume proper imports */
/* Initialize Firebase Admin SDK */
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "[db-url]",
});
/* Async verification with user token */
const verify = async (idToken) => {
if (idToken) {
var newToken = idToken.replace("Bearer ", "");
// var newToken = idToken
let header = await admin.auth().verifyIdToken(newToken)
.then(function(decodedToken) {
// ...
return {
"Authorization": 'Bearer ' + decodedToken
}
}).catch(function(error) {
// Handle error
return null
});
return header
} else {
throw 'No Access'
}
}
/* Server */
function gqlServer() {
const app = express();
const apolloServer = new ApolloServer({
typeDefs: schema,
resolvers,
context: async ({ req, res }) => {
// headers: req.headers,
const verified = await verify(req.headers.authorization)
console.log('log verified', verified)
return {
headers: verified ? verified: '',
req,
res,
}
},
// Enable graphiql gui
introspection: true,
playground: true
});
apolloServer.applyMiddleware({app, path: '/', cors: true});
return app;
}
export default gqlServer;
Client.js (ApolloClient)
/* Assume appropriate imports */
/* React Native firebase auth */
firebase.auth().onAuthStateChanged(async (user) => {
const userToken = await user.getIdToken();
/* Client creation */
const userToken = await user.getIdToken();
const client = new ApolloClient({
uri: '[Firebase Cloud Function URL]',
headers: {
"Authorization": userToken ? 'Bearer ' + userToken : ''
},
cache: new InMemoryCache(),
});
client.query({
query: gql`
{
hello
}
`
}).then(
(result) => console.log('log query result', result)
).catch(
(error) => console.log('query error', error)
)
})

Oauth2 Google Authentication flow - Next.JS / Express

I am using a React/Next.Js Frontend and am trying to implement authentication with the Oauth2 strategy with Google.
I am very confused by the process.
Currently on the client, I have a Google sign in component that has a Client ID with in it and can retrieve an access token.
<GoogleLogin
clientId="myclientid"
buttonText="Login"
onSuccess={userLogin}
onFailure={userLogin}
cookiePolicy={'single_host_origin'}
/>
I then have a function, which on success sends a post message to my backend with an access token, such as this:
export function googleAuthenticate(accessToken : string) : any{
axios({
method: 'post',
url: "http://localhost:4000/auth/google",
data: {
accessToken: accessToken
}
})
.then(res => {
console.log(res);
})
.catch(err => {
console.log("Failure!");
console.log(err);
})
};
On the backend I am using passport, and the routes look like this:
import express from 'express';
import passport from 'passport';
import Logger from '../logger/index';
const router = express.Router();
export function isAuthenticated(req:express.Request, res:express.Response, next : any) {
return req.isAuthenticated() ?
next() :
res.sendStatus(401);
}
router.get('/fail', (_req:express.Request, res:express.Response) => {
res.json({ loginFailed: true });
});
router.post('/google', passport.authenticate('google', { scope: ['profile']}), (_req:express.Request, _res:express.Response) => {
Logger.info("GET Request at Google Authentication endpoint received.");
});
router.get(
'/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(_req:express.Request, res:express.Response) => {
res.redirect('/graphql');
}
);
export default router;
My passport module looks like this:
module.exports = function(passport : any, GoogleStrategy : any){
passport.use(new GoogleStrategy({
clientID: config.google.client_id,
clientSecret: config.google.client_secret,
callbackURL: config.google.redirect_url
},
function(accessToken : string, profile : Profile, refreshToken : string, cb : any) {
return cb(null, {
id: profile.googleId,
username: profile.email,
image: profile.imageUrl,
firstName: profile.givenName,
surname: profile.familyName,
accessToken: accessToken,
refreshToken: refreshToken
})
}
));
}
Since Next.js is a server side rendered, I am not able to use save a token. I understand I have to use a cookie. But how does this work? I cannot redirect the client browser from the express backend.
Currently I'm just seeing these 2 errors:
OPTIONS https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2localhost:3000%2Fdashboard&scope=profile&client_id=687602672235-l0uocpfchbjp34j1jjlv8tqv7jadb8og.apps.googleusercontent.com 405
Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fbackoffice.dev.myos.co%2Fdashboard&scope=profile&client_id=687602672235-l0uocpfchbjp34j1jjlv8tqv7jadb8og.apps.googleusercontent.com' (redirected from 'http://localhost:4000/auth/google') from origin 'null' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Firstly i think google auth will not work on localhost.
If i understand correctly in your serverside logic you can easily save your token as a cookie and then read them in the client.
Not sure with passport, but you can do something similar to this :
(my app is working with an implementation of this code)
frontend :
<GoogleLogin
clientId="myclientid"
buttonText="Login"
onSuccess={userLogin}
onFailure={userLogin}
cookiePolicy={'single_host_origin'}
/>
userLogin:
async userLogin(response){
var url = '/google-login/'+response.tokenObj.id_token
fetch(url).then(/* i will handle response*/)
}
Then in the backend you can use google-auth-library to login or register.
server.js:
const {OAuth2Client} = require('google-auth-library');
const GOOGLEID = "mygoogleid.apps.googleusercontent.com"
const client = new OAuth2Client(GOOGLEID);
var cookieParser = require('cookie-parser')
async function verify(userToken) {
const ticket = await client.verifyIdToken({
idToken: userToken,
audience: "clientid.apps.googleusercontent.com", // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
const payload = ticket.getPayload();
const userid = payload['sub'];
return payload
// If request specified a G Suite domain:
//const domain = payload['hd'];
}
In server.js a route similar to this :
server.get('/google-login/:token',(req,res) => {
const userToken = req.params.token
var result = verify(userToken).then(function(result){
var userName = result.given_name
var userSurname = result.family_name
var userEmail = result.email
/*
Now user is authenticated i can send to the frontend
user info or user token o save the token to session
*/
}).catch(function(err){
// error handling
})
})
You could use NextAuth.js to handle this for you.
In order to test localhost you should use ngrok to expose your localhost server to the web and configure the given url in google platform

Redirect_uri mismatch in fetch and gapi

working on connecting users to google, and we're trying to get their access and refresh tokens from the google api, and we're getting an issue exchanging the OAuth2 Code for tokens. Both sets of code have the same error.
I initialize the gapi client and fill in the information needed like so:
gapi.load('client:auth2', _ => {
gapi.client.init({
'apiKey': 'omitted for security',
clientId: 'omitted for security',
'scope': 'https://www.googleapis.com/auth/drive',
'discoveryDocs': ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest']
}).then(_ => {
gapi.auth2.getAuthInstance().grantOfflineAccess().then(resp => {
if(resp.code){
gapi.client.request({
path: 'https://www.googleapis.com/oauth2/v4/token',
method: 'post',
params: {code: resp.code},
body: {
code: resp.code,
client_id: opts.clientId,
client_secret: 'omitted for security',
grant_type: 'authorization_code',
redirect_uri: 'omitted for security',
access_type: 'offline'
},
}).then((onfulfill, onreject, context) => {
console.log('fulfilled', onfulfill);
console.log('rejected: ', onreject);
console.log('context', context);
}).catch(err => console.error(err.body));
}
});
});
});
What I'm trying to do in the .then() is to call the token endpoint to exchange the code in the response for a refresh and access token to store in my back end and the user's local storage.
I get this error response from both versions of the code. (better, more reliable code is provided here.)
{ "error": "redirect_uri_mismatch", "error_description": "Bad
Request" }
I also have a backend setup stashed as a last resort that accepts the code from gapi.auth2.getAuthInstance().grantOfflineAccess() calls the token endpoint, and returns the access_token and refresh_token to the client.
This code is similar, but not quite. instead of using the google api library, I used fetch, and it works fine. (Fetch and XHR on the front end have the same issues as the gapi.client.request function....)
const gConfig = require('./basic.json');
const scopes = ['https://www.googleapis.com/auth/drive'];
const { client_id, client_secret, redirect_uris } = gConfig.web;
const authClient = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
app.post('/', (req, res) => {
const { code } = req.body;
console.log('Received Code From Request: ', code);
let data = { code , client_id, client_secret,redirect_uri: redirect_uris[0], grant_type: 'refresh_token'};
let encodedParams = Object.keys(data).map(k => encodeURIComponent(k) + '=' + encodeURIComponent(data[k])).join('&');
fetch(
`https://www.googleapis.com/oauth2/v4/token?code=${code}`,
{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: encodedParams }
).then((res) => {
console.log('called the api with fetch');
console.dir(res.json());
});
authClient.getToken(code, (err, token) => {
if (err) {
console.error(err);
res.status(500).json(err);
}
// console.dir(token);
console.log('TOKEN: =>', token);
res.json(token);
});
});
Is there anyone that's done this on the front end successfully?
You can't get a refresh token in a browser. Your example code would only work on a server. To do oauth at the client you should request "token" instead of "code".

AWS-amplify Including the cognito Authorization header in the request

I have create an AWS mobile hub project including the Cognito and Cloud logic. In my API gateway, I set the Cognito user pool for the Authorizers. I use React native as my client side app. How can I add the Authorization header to my API request.
const request = {
body: {
attr: value
}
};
API.post(apiName, path, request)
.then(response => {
// Add your code here
console.log(response);
})
.catch(error => {
console.log(error);
});
};
By default, the API module of aws-amplify will attempt to sig4 sign requests. This is great if your Authorizer type is AWS_IAM.
This is obviously not what you want when using a Cognito User Pool Authorizer. In this case, you need to pass the id_token in the Authorization header, instead of a sig4 signature.
Today, you can indeed pass an Authorization header to amplify, and it will no longer overwrite it with the sig4 signature.
In your case, you just need to add the headers object to your request object. For example:
async function callApi() {
// You may have saved off the JWT somewhere when the user logged in.
// If not, get the token from aws-amplify:
const user = await Auth.currentAuthenticatedUser();
const token = user.signInUserSession.idToken.jwtToken;
const request = {
body: {
attr: "value"
},
headers: {
Authorization: token
}
};
var response = await API.post(apiName, path, request)
.catch(error => {
console.log(error);
});
document.getElementById('output-container').innerHTML = JSON.stringify(response);
}
Tested using aws-amplify 0.4.1.