Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OAuth2 authentication #23

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
26 changes: 12 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
# Generic Google API Client for Node-RED
# Generic Google API Client for Node-RED using OAuth2

Node-RED node for Google APIs.
Node-RED node for Google APIs.

## Work in Progress

Changes are coming.

Configuration node name was changed at v.0.1.0: _google conn_ -> _google-conn_
This Node is based on the implementation by [74Labs](https://github.com/74Labs/node-red-contrib-google). It has been updated to use the latest version of the __googleapis__. Further the authorization workflows has been changed to __OAuth2__.

## Features

This node is a wrapper for official Google APIs Node.js Client: [google-api-nodejs-client](https://github.com/google/google-api-nodejs-client).

List of available APIs are delivered online via [Google API Discovery Service](https://developers.google.com/discovery/).

Package contains two nodes. There is configuration node made for maintaining connection to Google API Services (_google-conn_) and regular node providing posibility to call any method of any API exposed via official Google's Node.js Client.
Package contains two nodes. There is configuration node made for maintaining connection to Google API Services (_google-credentials_) and regular node providing posibility to call any method of any API exposed via official Google's Node.js Client.

## How to Install

Run the following command in the root directory of your Node-RED install

```
npm install node-red-contrib-google
npm install node-red-contrib-google-oauth2
```

or for a global installation
```
npm install -g node-red-contrib-google
npm install -g node-red-contrib-google-oauth2
```

## Configuration

1. Generate service account key at [Google API Console](https://console.developers.google.com/apis/credentials/serviceaccountkey).
1. Generate OAuth credentials at [Google API Console](https://console.developers.google.com/apis/credentials/oauthclient).

* Choose Web Application.
* As `Authorized JavaScript origins` enter your Node-RED IP (_e.g. `http://localhost:1880`_)
* As `Authorized redirect URIs` enter your Node-RED IP plus `/google-credentials/auth/callback` (_e.g. `http://localhost:1880/google-credentials/auth/callback`_)

* Choose JSON type and save service key file.
* Paste content of that file into JSON Key field of your _google-conn_ node.

2. Copy the `Client ID` and `Client secret` and paste them into the Config Node
137 changes: 137 additions & 0 deletions google-auth.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<script type="text/javascript">
(function() {
RED.nodes.registerType('google-credentials', {
category: 'config',
defaults: {
displayName: { value: "" },
scopes: { type: "text", value: "", required: true }
},
credentials: {
displayName: { type: "text" },
clientId: { type: "password" },
clientSecret: { type: "password" }
},
label: function() {
return this.displayName || 'Google OAuth2';
},
exportable: false,
oneditprepare: function() {
var id = this.id;
var pathname = document.location.pathname;
if (pathname.slice(-1) != "/") {
pathname += "/";
}

var privateIPRegex = /(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)/;
var callback;
if(privateIPRegex.test(location.hostname)) { // if private IP has been detected
var dummyDomain = "node-red.example.com";
var actualIP = location.hostname;
callback = location.protocol + "//" +
dummyDomain +
(location.port?":"+location.port:"")+
pathname + "google-credentials/auth/callback";
} else {
callback = location.protocol + "//" +
location.hostname +
(location.port?":"+location.port:"")+
pathname + "google-credentials/auth/callback";
}

function updateGoogleAuthButton() {
var v1 = $("#node-config-input-clientId").val();
var v2 = $("#node-config-input-clientSecret").val();
$("#node-config-start-auth").toggleClass("disabled",(v1.length === 0 || v2.length === 0));
}
$("#node-config-input-clientId").on('change keydown paste input',updateGoogleAuthButton);
$("#node-config-input-clientSecret").on('change keydown paste input',updateGoogleAuthButton);

function updateGoogleDisplayName(dn) {
$("#node-config-google-client-keys").hide();
$("#node-config-google").show();
$("#node-config-input-displayName").val(dn);
$("#node-config-google-displayName").html(dn);
}

function pollGoogleCredentials() {
$.getJSON('credentials/google-credentials/'+id,function(data) {
if (data.displayName) {
$("#node-config-dialog-ok").button("enable");
updateGoogleDisplayName(data.displayName);
delete window.googleConfigNodeIntervalId;
} else {
window.googleConfigNodeIntervalId = window.setTimeout(pollGoogleCredentials,2000);
}
});
}

updateGoogleAuthButton();

if (this.displayName) {
updateGoogleDisplayName(this.displayName);
} else {
$("#node-config-google-client-keys").show();
$("#node-config-google").hide();
$("#node-config-dialog-ok").button("disable");
}

$("#node-config-start-auth").mousedown(function() {
var clientId = $("#node-config-input-clientId").val();
var clientSecret = $("#node-config-input-clientSecret").val();
var scopes = $("#node-config-input-scopes").val();
scopes = scopes.replace(/\n/g, "%20");
var url = 'google-credentials/auth?id='+id+'&clientId='+clientId+"&clientSecret="+clientSecret+"&scopes="+scopes+"&callback="+encodeURIComponent(callback);
$(this).attr("href",url);
window.googleConfigNodeIntervalId = window.setTimeout(pollGoogleCredentials,2000);
});
$("#node-config-start-auth").click(function(e) {
var clientId = $("#node-config-input-clientId").val();
var clientSecret = $("#node-config-input-clientSecret").val();
if (clientId === "" || clientSecret === "") {
e.preventDefault();
}
});
},
oneditsave: function() {
if (window.googleConfigNodeIntervalId) {
window.clearTimeout(window.googleConfigNodeIntervalId);
delete window.googleConfigNodeIntervalId;
}
},
oneditcancel: function() {
if (window.googleConfigNodeIntervalId) {
window.clearTimeout(window.googleConfigNodeIntervalId);
delete window.googleConfigNodeIntervalId;
}
}
});
})();
</script>

<script type="text/x-red" data-template-name="google-credentials">
<div id="node-config-google-client-keys">
<div class="form-row">
<label for="node-config-input-clientId"><i class="fa fa-user"></i> Client ID</label>
<input type="password" id="node-config-input-clientId">
</div>
<div class="form-row">
<label for="node-config-input-clientSecret"><i class="fa fa-key"></i> Client Secret</label>
<input type="password" id="node-config-input-clientSecret">
</div>
<div class="form-row">
<label for="node-config-input-scopes">
<i class="fa fa-fw fa-list"></i> Scopes</label>
<textarea id="node-config-input-scopes" rows="10" style="width: 100%"></textarea>
</div>
<div class="form-row">
<label>&nbsp;</label>
<a class="btn" id="node-config-start-auth" href="#" target="_blank">Start Authentication</a>
</div>
</div>
<div id="node-config-google">
<div class="form-row">
<label for="node-config-input-displayName">Name</label>
<input id="node-config-input-displayName">
</div>
</div>
</script>
98 changes: 98 additions & 0 deletions google-auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
module.exports = function(RED) {
"use strict";
const crypto = require("crypto");
const url = require('url');
const { google } = require('googleapis');

function GoogleNode(n) {
RED.nodes.createNode(this,n);
this.displayName = n.displayName;
this.scopes = n.scopes;
}
RED.nodes.registerType("google-credentials",GoogleNode,{
credentials: {
displayName: {type:"text"},
clientId: {type:"text"},
clientSecret: {type:"password"},
accessToken: {type:"password"},
refreshToken: {type:"password"},
expireTime: {type:"password"}
}
});

RED.httpAdmin.get('/google-credentials/auth', function(req, res){
console.log('google-credentials/auth');
if (!req.query.clientId || !req.query.clientSecret ||
!req.query.id || !req.query.callback) {
res.send(400);
return;
}
const node_id = req.query.id;
const callback = req.query.callback;
const credentials = {
clientId: req.query.clientId,
clientSecret: req.query.clientSecret
};
const scopes = req.query.scopes;

const csrfToken = crypto.randomBytes(18).toString('base64').replace(/\//g, '-').replace(/\+/g, '_');
credentials.csrfToken = csrfToken;
credentials.callback = callback;
res.cookie('csrf', csrfToken);
res.redirect(url.format({
protocol: 'https',
hostname: 'accounts.google.com',
pathname: '/o/oauth2/auth',
query: {
access_type: 'offline',
approval_prompt: 'force',
scope: scopes,
response_type: 'code',
client_id: credentials.clientId,
redirect_uri: callback,
state: node_id + ":" + csrfToken,
}
}));
RED.nodes.addCredentials(node_id, credentials);
});

RED.httpAdmin.get('/google-credentials/auth/callback', function(req, res) {
console.log('google-credentials/auth/callback');
if (req.query.error) {
return res.send("google.error.error", {error: req.query.error, description: req.query.error_description});
}
var state = req.query.state.split(':');
var node_id = state[0];
var credentials = RED.nodes.getCredentials(node_id);
if (!credentials || !credentials.clientId || !credentials.clientSecret) {
console.log("credentials not present?");
return res.send("google.error.no-credentials");
}
if (state[1] !== credentials.csrfToken) {
return res.status(401).send("google.error.token-mismatch");
}

const oauth2Client = new google.auth.OAuth2(
credentials.clientId,
credentials.clientSecret,
credentials.callback
);

oauth2Client.getToken(req.query.code)
.then((value) => {
credentials.accessToken = value.tokens.access_token;
credentials.refreshToken = value.tokens.refresh_token;
credentials.expireTime = value.tokens.expiry_date;
credentials.tokenType = value.tokens.token_type;
credentials.displayName = value.tokens.scope.substr(0, 40);

delete credentials.csrfToken;
delete credentials.callback;
RED.nodes.addCredentials(node_id, credentials);
res.send('Authorized');
})
.catch((error) => {
return res.send('Could not receive tokens');
});
});
};
49 changes: 2 additions & 47 deletions google.html
Original file line number Diff line number Diff line change
@@ -1,49 +1,3 @@
<script type="text/javascript">
RED.nodes.registerType('google-conn', {
category: 'config',
defaults: {
name: {
value: "Google",
required: true
},
key: {
value: "{}",
required: true
},
scopes: {
value: "",
required: true
}
},
inputs: 0,
outputs: 0,
label: function() {
return this.name || "Google API";
}
});
</script>

<script type="text/x-red" data-template-name="google-conn">
<div class="form-row">
<label for="node-config-input-name">
<i class="icon-bookmark"></i>
Name</label>
<input type="text" id="node-config-input-name"/>
</div>
<div class="form-row">
<label for="node-config-input-key">
<i class="fa fa-fw fa-key"></i>
JSON Key</label>
<textarea id="node-config-input-key" rows="20" style="width: 100%"></textarea>
</div>
<div class="form-row">
<label for="node-config-input-scopes">
<i class="fa fa-fw fa-list"></i>
Scopes</label>
<textarea id="node-config-input-scopes" rows="10" style="width: 100%"></textarea>
</div>
</script>

<script type="text/javascript">
RED.nodes.registerType('google', {
category: 'function',
Expand All @@ -54,7 +8,8 @@
value: ""
},
google: {
type: "google-conn"
type: "google-credentials",
required: true
},
api: {
type: ""
Expand Down
Loading