Shopify - App must set security headers to protect against clickjacking - reactjs

I'm new to Shopify and I'm trying to help a friend with their website. I'm getting the following errors at the moment.
1. App must set security headers to protect against clickjacking.
Your app does not request installation on the shop immediately after clicking "add app". Apps must ask a shop for access when being installed on a shop for the first time, as well as when they are being reinstalled after having been removed. During install or reinstall we expected OAuth to be initiated at https://cambridgetestshop.myshopify.com/admin/oauth/request_grant but was redirected to https://app-staging.hashgifted.com/. Learn more about authentication in our developer documentation
2. App must verify the authenticity of the request from Shopify.
Your app does not request installation on the shop immediately after clicking "add app". Apps must ask a shop for access when being installed on a shop for the first time, as well as when they are being reinstalled after having been removed. During install or reinstall we expected OAuth to be initiated at https://cambridgetestshop.myshopify.com/admin/oauth/request_grant but was redirected to https://app-staging.hashgifted.com/. Learn more about authentication in our developer documentation
We're using React built in Yarn. I'm not sure about next steps, thanks!

it seems that you're not following the documentation in regarding of authentication and app installation process.
As you're using node I suggest you to take a look at this project https://github.com/Shopify/shopify-app-node
and in particular to the authentication middleware, this is one part
import { Shopify } from "#shopify/shopify-api";
import topLevelAuthRedirect from "../helpers/top-level-auth-redirect.js";
export default function applyAuthMiddleware(app) {
app.get("/auth", async (req, res) => {
if (!req.signedCookies[app.get("top-level-oauth-cookie")]) {
return res.redirect(
`/auth/toplevel?${new URLSearchParams(req.query).toString()}`
);
}
const redirectUrl = await Shopify.Auth.beginAuth(
req,
res,
req.query.shop,
"/auth/callback",
app.get("use-online-tokens")
);
res.redirect(redirectUrl);
});
app.get("/auth/toplevel", (req, res) => {
res.cookie(app.get("top-level-oauth-cookie"), "1", {
signed: true,
httpOnly: true,
sameSite: "strict",
});
res.set("Content-Type", "text/html");
res.send(
topLevelAuthRedirect({
apiKey: Shopify.Context.API_KEY,
hostName: Shopify.Context.HOST_NAME,
host: req.query.host,
query: req.query,
})
);
});
app.get("/auth/callback", async (req, res) => {
try {
const session = await Shopify.Auth.validateAuthCallback(
req,
res,
req.query
);
const host = req.query.host;
app.set(
"active-shopify-shops",
Object.assign(app.get("active-shopify-shops"), {
[session.shop]: session.scope,
})
);
const response = await Shopify.Webhooks.Registry.register({
shop: session.shop,
accessToken: session.accessToken,
topic: "APP_UNINSTALLED",
path: "/webhooks",
});
if (!response["APP_UNINSTALLED"].success) {
console.log(
`Failed to register APP_UNINSTALLED webhook: ${response.result}`
);
}
// Redirect to app with shop parameter upon auth
res.redirect(`/?shop=${session.shop}&host=${host}`);
} catch (e) {
switch (true) {
case e instanceof Shopify.Errors.InvalidOAuthError:
res.status(400);
res.send(e.message);
break;
case e instanceof Shopify.Errors.CookieNotFound:
case e instanceof Shopify.Errors.SessionNotFound:
// This is likely because the OAuth session cookie expired before the merchant approved the request
res.redirect(`/auth?shop=${req.query.shop}`);
break;
default:
res.status(500);
res.send(e.message);
break;
}
}
});
}

Related

CORs issue with Google Authentication/Authorization Using React/Nodejs/Passport

I am having the same issue as issue CORs Error: Google Oauth from React to Express (PassportJs validation). But I am unable to get the solution offered by #Yazmin to work.
I am attempting to create a React, Express/Nodejs, MongoDB stack with Google authentication and authorization. I am currently developing the stack on Windows 10, using Vs Code (React on ‘localhost:3000, Nodejs on localhost:5000 and MongoDB on localhost:27017.
The app’s purpose is to display Urban Sketches(images) on a map using google maps, google photos api and google Gmail api. I may in the future also require similar access to Facebook Groups to access Urban Sketches. But for now I have only included the profile and Email scopes for authorization.
I want to keep all requests for third party resources in the backend, as architecturally I understand this is best practice.
The google authorization process from origin http://localhost:5000 works just fine and returns the expected results. However, when I attempt to do the same from the client - origin Http://localhost:3000 the following error is returned in the developers tools console following the first attempt to access the google auth2 api. Although the scheme and domain are the same the port is different, so the message from the third part (Https://account.google.com) has been rejected by the browser.
Access to fetch at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email%20https%3A%2F%2Fmail.google.com%2F&client_id=' (redirected from 'http://localhost:3000/auth/google') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
No matter what I try the error message is the same.
I think that google is sending the reply to the client (localhost:3000) rather than to the server.
Among other solutions, I attempted to implement Yilmaz’s solution by Quote: “Create setupProxy.js file in client/src. No need to import this anywhere. create-react-app will look for this directory” I had already created my client by running create-react-app previously. So I added setupProxy.js inside my src folder.
Question: I assume I am correct that the new setupProxy.cjs file containing my settings will be included by webpack after I restart the client.
It seems to me that the flow I am getting is not BROWSER ==> EXPRESS ==> GOOGLE-SERVER but BROWSER ==> EXPRESS ==> GOOGLE-SERVER ==>BROWSER where it stops with the cors error as shown above.
To test this theory, I put some console log messages in the client\node_modules\http-proxy-middleware\lib\index.js functions "shouldProxy" and "middleware", but could not detect any activity from the auth/google end point from the google authorization server response (https://accounts.google.com/o/oauth2/v2/auth).
So I think my theory is wrong and I don't know how I will get this working.
Console log messages displayed on VsCode terminal following request to /auth/google endpoint from the React client are as follows...
http-proxy-middleware - 92 HttpProxyMiddleware - shouldProxy
context [Function: context]
req.url /auth/google
req.originalUrl /auth/google
Trace
at shouldProxy (C:\Users\User\github\GiveMeHopev2\client\node_modules\http-proxy-middleware\lib\index.js:96:13)
at middleware (C:\Users\User\github\GiveMeHopev2\client\node_modules\http-proxy-middleware\lib\index.js:49:9)
at handle (C:\Users\User\github\GiveMeHopev2\client\node_modules\webpack-dev-server\lib\Server.js:322:18)
at Layer.handle [as handle_request] (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\layer.js:95:5)
at trim_prefix (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:317:13)
at C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:284:7
at Function.process_params (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:335:12)
at next (C:\Users\User\github\GiveMeHopev2\client\node_modules\express\lib\router\index.js:275:10)
at goNext (C:\Users\User\github\GiveMeHopev2\client\node_modules\webpack-dev-middleware\lib\middleware.js:28:16)
at processRequest (C:\Users\User\github\GiveMeHopev2\client\node_modules\webpack-dev-middleware\lib\middleware.js:92:26)
http-proxy-middleware - 15 HttpProxyMiddleware - prepareProxyRequest
req localhost
The Google callback uri is http://localhost:5000/auth/google/callback
This is a listing of my nodejs server code.
dotenv.config();
// express
const app = express();
// cors
app.use(cors())
// passport config
require ('./config/passport')(passport)
// logging
if( process.env.NODE_ENV! !== 'production') {
app.use(morgan('dev'))
}
const conn = process.env.MONGODB_LOCAL_URL!
/**
* dbConnection and http port initialisation
*/
const dbConnnect = async (conn: string, port: number) => {
try {
let connected = false;
await mongoose.connect(conn, { useNewUrlParser: true, useUnifiedTopology: true })
app.listen(port, () => console.log(`listening on port ${port}`))
return connected;
} catch (error) {
console.log(error)
exit(1)
}
}
const port = process.env.SERVERPORT as unknown as number
dbConnnect(conn, port)
//index 02
// Pre Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }))
const mongoStoreOptions = {
mongoUrl: conn,
collectionName: 'sessions'
}
app.use(
session({
secret: process.env.SESSIONKEY as string,
resave: false,
saveUninitialized: false,
store: MongoStore.create(mongoStoreOptions),
})
)
app.use(passport.initialize())
app.use(passport.session())
// Authentication and Authorisation
const emailScope: string = process.env.GOOGLE_EMAIL_SCOPE as string
//GOOGLE_EMAIL_SCOPE=https://www.googleapis.com/auth/gmail/gmail.compose
const scopes = [
'profile',
emailScope
].join(" ")
app.get('/auth/google', passport.authenticate('google', {
scope: scopes
}));
app.get('/auth/google/callback', passport.authenticate('google', { failureRedirect: '/'}),
(req, res) => {
res.send('Google Login Successful ')
}
)
app.get('/', (req, res) => {
res.send('Hello World');
})
The http-proxy-middleware setupProxy.cjs file. Note the cjs extension. I assume this was because I am using Typescript. It is in the client src folder
const createProxyMiddleware = require('http-proxy-middleware');
module.exports = function (app) {
app.use(createProxyMiddleware('/auth', {target: 'http://localhost:5000'}))
}
And finally the fetch command from the client
async function http(request: RequestInfo): Promise<any> {
try {
const response = await fetch('/auth/google')
const body = await response.json();
return body
} catch (err) { console.log(`Err SignInGoogle`) }
};
And the passport config...
import { PassportStatic} from 'passport';
import {format, addDays} from 'date-fns'
import { IUserDB, IUserWithRefreshToken, ProfileWithJson} from '../interfaces/clientServer'
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const User = require('../models/User')
module.exports = function (passport:PassportStatic) {
const clientID: string = process.env.GOOGLE_CLIENTID as string
const clientSecret: string = process.env.GOOGLE_SECRET as string
const callbackURL: string = process.env.GOOGLE_AUTH_CALLBACK as string
const strategy = new GoogleStrategy(
{
clientID: clientID,
clientSecret: clientSecret,
callbackURL: callbackURL,
proxy: true
},
async (_accesstoken: string, _refreshtoken: string,
profile: ProfileWithJson,
etc
you can't make a fetch call to the /auth/google route!
Here's my solution in javascript...
// step 1:
// handler function should use window.open instead of fetch
const loginHandler = () => window.open("http://[server:port]/auth/google", "_self")
//step 2:
// on the server's redirect route add this successRedirect object with correct url.
// Remember! it's your clients root url!!!
router.get(
'/google/redirect',
passport.authenticate('google',{
successRedirect: "[your CLIENT root url/ example: http://localhost:3000]"
})
)
// step 3:
// create a new server route that will send back the user info when called after the authentication
// is completed. you can use a custom authenticate middleware to make sure that user has indeed
// been authenticated
router.get('/getUser',authenticated, (req, res)=> res.send(req.user))
// here is an example of a custom authenticate express middleware
const authenticated = (req,res,next)=>{
const customError = new Error('you are not logged in');
customError.statusCode = 401;
(!req.user) ? next(customError) : next()
}
// step 4:
// on your client's app.js component make the axios or fetch call to get the user from the
// route that you have just created. This bit could be done many different ways... your call.
const [user, setUser] = useState()
useEffect(() => {
axios.get('http://[server:port]/getUser',{withCredentials : true})
.then(response => response.data && setUser(response.data) )
},[])
Explanation....
step 1 will load your servers auth url on your browser and make the auth request.
step 2 then reload the client url on the browser when the authentication is
complete.
step 3 makes an api endpoint available to collect user info to update the react state
step 4 makes a call to the endpoint, fetches data and updates the users state.

read cookies in front (Reactjs) that comes from nodejs

i need to get cookies(it's a token) which has been defined in a node js Route file to my front, because i need to check infos of this token to show data if it's a user or admin.
THis is some code of the cookies :
// auth with google+
router.get('/auth/google', passport.authenticate('google', {
scope: [
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email'
]
}));
// callback route for google to redirect to
// hand control to passport to use code to grab profile info
router.get('/auth/google/callback*', passport.authenticate('google'), (req, res) => {
if(req.user){
console.log(req.user);
res.cookie('token', req.user);
return res.redirect(config.clientURL);
}
else{
console.log('error');
return res.redirect(config.clientURL);
}
});
// auth with faceboook
router.get('/auth/facebook', passport.authenticate('facebook'));
// callback route for facebook to redirect to
// hand control to passport to use code to grab profile info
router.get('/auth/facebook/callback*', passport.authenticate('facebook'), (req, res) => {
console.log("je suis dans la route callback");
if(req.user){
console.log(req.user);
res.cookie('token', req.user);
return res.redirect(config.clientURL);
}
else{
console.log('error');
return res.redirect(config.clientURL);
}
});
Edit :
i did this :
const auth_head = document.cookie.split('.')[0];
const auth_payload = document.cookie.split('.')[1];
const auth_signature = document.cookie.split('.')[2];
var auth_token = auth_head + "." + auth_payload + "." + auth_signature;
console.log(JSON.parse( auth_head));
console.log(JSON.parse( auth_payload));
console.log(JSON.parse( auth_signature));
but i got this error :
Uncaught (in promise) SyntaxError: Unexpected token o in JSON at position 1
Thank you
As I mentioned in the comments, it's good advice to use httpOnly flag when setting cookies; this means that you need another strategy to return the user data.
Option 1: One easier to implement way could be: After your server redirects the client to let's say /logged-in, you can fetch the user data from let's say /api/userinfo; the response should a json containing the user info; you should use that json to store the information in your client using localStorate.setItem(...). This is the classic and more used way to store your user data in the client.
Example Server (Create an endpoint that returns the logged-in user info):
// Server endpoint that returns user info
router.get('/api/userinfo',
passport.authenticate(your_strategy_here),
(req, res) => {
res.json({ name: req.user.name, role: req.user.role }); // Return just what you need
})
Example Client (Create a component that requests the user info from the new server endpoint):
componentDidMount(){
fetch('/api/userinfo')
.then( res => res.json() )
.then( user => localStorate.setItem('user', user);
}
Option 2: Give Google a URL which is resolved by the client, and then have the client send the request to /auth/facebook/callback; then have the server do res.json(user), instead of the doing the redirect.
Google -> /your-client-app/auth/callback
Client -> /auth/facebook/callback
Option 2 is my advice, however, Option 1 may be more straight forward for your current setup.
Option 3: Disable httpOnly when setting the cookie, there are security concerns when doing this and it's not meant to be done like that in production apps.
res.cookie('token', req.user, { httpOnly: false });
And then on your client, you can use the following data to check the cookies.
const cookieData = document.cookie;
console.log(cookieData)

Amplify - GraphQL request headers are empty

I am attempting to create an app that utilizes Cognito user pools for user auth and then sending api requests to a dynamoDB table through graphQL.
The user auth/signup works correctly, however I receive a 401 error when attempting to query a data table. The message states "Missing authorization header"
I saw in a similar post that the auth token should be auto-populated into the request headers, but that does not occur for me. I also saw that Amplify created a function for custom graphql headers. I attempted this also but still get the same "Missing authorization header" error.
Any suggestions?
aws_appsync_graphqlEndpoint:'',
aws_appsync_region:'',
aws_appsync_authenticationType:'AMAZON_COGNITO_USER_POOLS',
graphql_headers: async () => ({
'My-Custom-Header': cognitoUser
})
}
This is in my config/exports file for amplify ---- Amplify.configure(config)
if (cognitoUser != null) {
cognitoUser.getSession((err, session) => {
if (err) {
console.log(err);
} else if (!session.isValid()) {
console.log("Invalid session.");
} else {
console.log( session.getIdToken().getJwtToken());
}
});
} else {
console.log("User not found.");
}
console.log(cognitoUser)
Amplify.configure(config)
const client = new AWSAppSyncClient({
disableOffline: true,
url: config.aws_appsync_graphqlEndpoint,
region: config.aws_appsync_region,
identityPoolId: config.aws_cognito_identity_pool_id,
userPoolId: config.aws_user_pools_id,
userPoolWebClientId: config.ws_user_pools_web_client_id,
auth: {
type: config.aws_appsync_authenticationType,
jwtoken: async () =>
(await Auth.currentSession()).getIdToken().getJwtToken(),
apiKey: config.aws_appsync_apiKey
}
});```
This is my client settings in my index.js folder
I apologize if I missed something blatant. I am new to backend and am having trouble with getting this to work.
I have only gotten it to work when using API_Key auth.

Error when using Google Application Default Credentials on App Engine

I am trying to make a Node.js app (running Express on App Engine) authenticate with Google API (Server-to-Server) using the Google Application Default Credentials. The app is supposed to use the credentials to talk with Google Analytics, which I have set up by turning on the Analytics API in the Google Developers Console. This is the code I have implemented:
var google = require('googleapis')
var analytics = google.analytics('v3')
app.post('/getAnalyticsData', (req, res) => {
google.auth.getApplicationDefault(function(err, authClient) {
if (err) {
/* Handle error */
}
if (authClient) {
if (authClient.createScopedRequired && authClient.createScopedRequired()) {
authClient = authClient.createScoped(['https://www.googleapis.com/auth/analytics.readonly'])
}
analytics.data.ga.get({
'auth': authClient,
'ids': 'ga:VIEW_ID',
'metrics': 'ga:pageviews,ga:sessions',
'start-date': '2017-01-01',
'end-date': '2017-03-09'
}, function(err, response) {
if (err) {
console.log("Analytics error: ", err)
}
if (response) {
console.log("YAY! Analytics response: ", response)
/* Do something with the response */
}
})
}
})
})
But I am getting this error: 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. Insufficient Permission.
Any idea how to solve this and succeed with the authentication?
I had the same error when trying to use google-auth-library to connect to datastore and was unable to set the correct permissions for the default service account. I found an example in their samples folder that created an auth client using a key file. You can create your own service account with the right permissions and generate a key file on the service account admin page in the cloud console. Hope this helps.
const {auth} = require('google-auth-library');
async function getDnsInfo() {
const client = await auth.getClient({
keyFile: 'path/to/keyFile.json,
scopes: 'https://www.googleapis.com/auth/cloud-platform',
});
const projectId = await auth.getProjectId();
const url = `https://www.googleapis.com/dns/v1/projects/${projectId}`;
const res = await client.request({url});
console.log('DNS Info:');
console.log(res.data);
}

Error: User credentials required in Google Cloud Print API

I'm trying to use Google Cloud Print(GCP) API, but I can't make it works.
Maybe I've understood bad the workflow because is the first time I'm using the google api, please help me to understand how to make it works.
Initial considerations:
I'm trying to implement it in reactJS, but It is indifferent because the logic to make GCP works is independent of the technology. Then you also can help me understand the workflow.
What exactly I want:
To make my first test, I am looking to get all information about my printer.
What I did:
I created a project in: https://console.developers.google.com
Inside the project created, I created a credential:
create credentials -> OAuth client ID
And I chose Application type: Web, and also configure the restrictions to source and redirection to my localhost.
Manually in https://www.google.com/cloudprint, I added my printer, I made a test printing a PDF and was OK.
I created a project in reactJS to get the information of my printer I've added.
Component:
Explanation:
I'm using a component react-google-login to obtain easily the user accessToken: https://github.com/anthonyjgrove/react-google-login
This component only obtains the access token and save it in localStorage, in a variable called googleToken and it draws a button to call a function to obtain the information about the printer.
code:
import React, { Component } from 'react'
import GoogleLogin from 'react-google-login';
import { connect } from 'react-redux'
import { getPrinters } from '../actions/settings'
class Setting extends Component {
responseGoogle(response) {
const accessToken = response.accessToken
localStorage.setItem('googleToken', accessToken)
}
render() {
return (
<div>
<GoogleLogin
clientId="CLIENT_ID_REMOVED_INTENTIONALLY.apps.googleusercontent.com"
buttonText="Login"
onSuccess={this.responseGoogle}
onFailure={this.responseGoogle}
/>
<button
onClick = {() => {
this.props.getPrinters()
}}
>test printer</button>
</div>
)
}
}
const mapStateToProps = state => {
return {
state: state
}
}
const mapDispatchToProps = dispatch => {
return {
getPrinters() {
dispatch(getPrinters())
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Setting)
Action or Function to get information printer:
Explanation:
I'm passing the parameter printerid to get information about that printer.
In authorization, I'm using OAuth ... because in the documentation says that(second paragraph).: https://developers.google.com/cloud-print/docs/appInterfaces
The next two headers I wrote it because I tried solutions as:
Google Cloud Print API: User credentials required
Google Cloud Print User credentials required
code:
import axios from 'axios'
axios.defaults.headers.common['Authorization'] = 'OAuth ' + localStorage.getItem('googleToken')
axios.defaults.headers.common['scope'] = 'https://www.googleapis.com/auth/cloudprint'
axios.defaults.headers.common['X-CloudPrint-Proxy'] = 'printingTest'
const getPrinters = () => {
return () => {
return axios.get('https://www.google.com/cloudprint/printer'
, {
params: {
printeid: 'PRINTER_ID_REMOVED_INTENTIONALLY'
}
}
)
.then(response => {
console.log('response of google cloud print')
console.log(response)
})
}
}
export { getPrinters }
Error:
After all explained before, I got the next error:
User credentials required
Error 403
Note:
I'm using CORS plugin by recommendation of:
Chrome extensions for silent print?
because initially, I had cors error.
Any suggestion or recommendation would be very useful, thanks.
I've resolved my problem, my main problem about User Credential required were because I was using the incorrect access token and It was because I was getting the access token incorrectly.
I'm going to explain my whole solution because there are few examples of codes with this API.
Solutions:
The steps described were Ok until the fourth step where I used the external component react-google-login to trying to get the access token, instead I used googleapis module: Link Github googleapis
Also to avoid CORS problem(and not use CORS chrome plugin) I wrote the requests to Google API in server side.(NODEJS)
I had also a problem in the frontend when I tried to generate a popup to give permission for printer(problems about CORS), my solution was to use this very simple module for authentication: Link Github oauth-open
General scheme:
Explanation:
Knowing I have all data described in my question post(until the third step).
Authentication:
The next step in getting a URL and use it to the user can authenticate.
As I said before I used the module oauth-open in the frontend to generate the popup and only this module need the URL. To get the URL in the backend I used the endpoint /googleurl, where here I used the method generateAuthUrl of the module googleapis to generate the URL.
After that In the frontend, I got the authentication_code(that returned the module oauth-open), I send It to my endpoint /googletoken and here I process the authentication_code to generate access token, refresh token and expiration date with the method getToken of the module googleapis. Finally, these data are stored in the database.
Print:
For print, since the frontend, I send what data I need send to the printer. I used my endpoint /print
In the backend endpoint, my logic was the next:
Recover tokens and expiration date from database, with the expiration date check if the token has expired, and if It has already expired then gets another token and replace the old access token with the new one, replacing also with the new expiration date, to obtain this new data only is necessary call to method refreshAccessToken of module googleapis.Note: the refresh token never expires.
After having the access token updated, use it to send data to the printer with Google route(.../submit)
Code:
All the next codes are in only 1 file
Some data as validation, static variables, error handler, etc, has been removed to better understanding.
Route get URL authentication.
const express = require('express');
const google = require('googleapis');
const router = express.Router();
var OAuth2 = google.auth.OAuth2;
const redirect_url = 'http://localhost:3001/setting'; //Your redirect URL
var oauth2Client = new OAuth2(
'CLIENT ID', //Replace it with your client id
'CLIEND SECRET', //Replace it with your client secret
redirect_url
);
var url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: 'https://www.googleapis.com/auth/cloudprint'
});
router.get('/googleurl', (req, res) => {
return res.status(200).send({
result: { googleURLToken: url }
});
});
To get tokens using the authentication code and save these in the database.
const Setting = require('../models/setting'); // My model(Mongoose)
router.post('/googletoken', (req, res) => {
oauth2Client.getToken(req.body.code, function (err, tokens) {
oauth2Client.credentials = tokens;
// If refresh token exits save it
// because the refresh token it returned only 1 time! IMPORTANT
if (tokens.hasOwnProperty('refresh_token')) {
let setting = new Setting();
setting.refreshTokenGoogle = tokens.refresh_token;
setting.expirationTokenGoogle = tokens.expiry_date;
setting.tokenGoogle = tokens.access_token;
setting.save()
.then((settingCreated) => {
return res.status(200).send({
message: 'OK'
});
})
}
});
});
To print
const axios = require('axios');
const moment = require('moment');
router.post('/print',async (req, res) => {
const tickeProperties = {
'version': '1.0',
'print': {
'vendor_ticket_item': [],
'color': { 'type': 'STANDARD_MONOCHROME' },
'copies': { 'copies': 1 }
}
};
const accessToken = await getTokenGoogleUpdated();
axios.get(
'https://www.google.com/cloudprint/submit',
{
params: {
printerid : printerID, // Replace by your printer ID
title: 'title printer',
ticket: tickeProperties,
content : 'print this text of example!!!',
contentType: 'text/plain'
},
headers: {
'Authorization': 'Bearer ' + accessToken
}
}
)
.then(response => {
return res.status(200).send({
result: response.data
});
})
}
);
async function getTokenGoogleUpdated() {
return await Setting.find({})
.then(async setting => {
const refreshTokenGoogle = setting[0].refreshTokenGoogle;
const expirationTokenGoogle = setting[0].expirationTokenGoogle;
const tokenGoogle = setting[0].tokenGoogle;
const dateToday = new Date();
// 1 minute forward to avoid exact time
const dateTodayPlus1Minute = moment(dateToday).add(1, 'm').toDate();
const dateExpiration = new Date(expirationTokenGoogle);
// Case date expiration, get new token
if (dateExpiration < dateTodayPlus1Minute) {
console.log('Updating access token');
oauth2Client.credentials['refresh_token'] = refreshTokenGoogle;
return await oauth2Client.refreshAccessToken( async function(err, tokens) {
// Save new token and new expiration
setting[0].expirationTokenGoogle = tokens.expiry_date;
setting[0].tokenGoogle = tokens.access_token;
await setting[0].save();
return tokens.access_token;
});
} else {
console.log('Using old access token');
return tokenGoogle;
}
})
.catch(err => {
console.log(err);
});
}
I hope It helps you if you want to use Google Cloud Print to not waste a lot of time as I did.
The important part there is a scope https://www.googleapis.com/auth/cloudprint which is not obvious and took one day for me to figure out.

Resources