Reputation: 1021
I'm trying to make a simple desktop email reader (w/ electron) that shows a user's emails on little cards that can be flipped through. Nothing much, that being why I don't want to setup a whole remote server system for it. I followed the instructions in Google's NodeJS Quickstart guide to get started. That includes saving my Google API credentials to a file in the app. Upon login, a token is saved to disk. If no such token exists, it will open up a login page in the browser that redirects to 127.0.0.1:3000/authorize
(the express app running there saves the token). It works and doesn't require a remote server which is what I want.
My question is, is it safe to distribute the credentials.json
file (contains client_id, client_secret, project_id) with my app? What are the potential security issues? If this is not suitable, what is the least complicated alternative to make my app distributable safely?
I looked at Google's docs and found this.
The process results in a client ID and, in some cases, a client secret, which you embed in the source code of your application. (In this context, the client secret is obviously not treated as a secret.)
So the client_secret isn't secret in this case, right? What about the rest of credentials.json
? Can someone impersonate my app and do bad things using that information?
Here's the code (it works) that does the first-time login:
function getNewToken(oAuth2Client, callback, method, args) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
require('electron').shell.openExternal(authUrl);
// mini server for authorization
const express = require('express')()
//express part
express.get('/authorize', function (req, res) {
oAuth2Client.getToken(req.query.code, (err, token) => {
if (err) return console.error('Error retrieving access token', err);
oAuth2Client.setCredentials(token);
// Store the token to disk for later program executions
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) return console.error(err);
console.log('Token stored to', TOKEN_PATH);
});
method(oAuth2Client, callback, args);
});
res.sendFile(path.join(__dirname, 'authorize.html'));
});
express.listen(3000, () => {
console.log(`Example app listening at http://localhost:3000`)
})
}
And the function I use to access the API if a token is saved:
function ApiCall(method, callback, args) {
fs.readFile('credentials.json', (err, content) => {
if (err) return console.log('Error loading client secret file:', err);
// Authorize a client with credentials, then call the Gmail API.
var credentials = JSON.parse(content);
var {client_secret, client_id, redirect_uris} = credentials.web;
var oAuth2Client = new google.auth.OAuth2(
client_id, client_secret, redirect_uris[0]);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, (err, token) => {
if (err) return getNewToken(oAuth2Client, callback, method, args);
oAuth2Client.setCredentials(JSON.parse(token));
method(oAuth2Client, callback, args);
});
});
}
Upvotes: 3
Views: 1439
Reputation: 1021
Was just looking at the google oauth github repo for another project, and came across this:
If you're authenticating with OAuth2 from an installed application (like Electron), you may not want to embed your client_secret inside of the application sources. To work around this restriction, you can choose the iOS application type when creating your OAuth2 credentials in the Google Developers console:
https://github.com/googleapis/google-auth-library-nodejs#oauth2-with-installed-apps-electron
Not sure how that prevents another app from using your client ID in the same fashion, but it's their suggestion.
Upvotes: 0
Reputation: 3949
It's not safe to distribute client secret in this manner as it can be easily exfiltrated out of your app by an attacker. Client id is safe to distribute, on its own it's not enough for an attack. You don't need project id for authorization.
The flow you want to implement is known as authorization code grant type in oAuth. You'll also want to implement a PKCE extension. Taken together, authorization code + PKCE is a recommended approach for using oAuth in a native app running on an end-user's device.
Google documents authorization code without PKCE as "Server Flow" (somewhat of a misnomer) using OpenId Connect. The latter is a superset of oAuth, everything above still applies:
- Create an anti-forgery state token
- Send an authentication request to Google
- Confirm the anti-forgery state token
- Exchange code for access token and ID token
- Obtain user information from the ID token
- Authenticate the user
You may not need steps 5-6 in your application.
You can try rolling steps 1-4 in JS with google-api-nodejs-client or you can give oidc-client-js a shot.
Upvotes: 3