node-red-contrib-google-smarthome
Version:
Lets you control Node-Red via Google Assistant or the Google Home App
271 lines (247 loc) • 12.6 kB
JavaScript
/**
* node-red-contrib-google-smarthome
* Copyright (C) 2024 Michael Jacobsen and others.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
const path = require('path');
const util = require('util');
const fs = require('fs');
/******************************************************************************************************************
* HttpAuth
*
*/
class HttpAuth {
/**
* Constructor
*
* @param {GoogleSmartHome} smarthome
*/
constructor(smarthome) {
this._smarthome = smarthome;
}
//
//
//
httpAuthRegister(httpRoot, appHttp) {
let me = this;
let use_decode = false;
if (typeof appHttp === 'undefined') {
appHttp = this._smarthome.app;
use_decode = true;
}
/**
* expecting something like the following:
*
* GET https://myservice.example.com/auth? \
* client_id=GOOGLE_CLIENT_ID
* - The Google client ID you registered with Google.
* &redirect_uri=REDIRECT_URI
* - The URL to which to send the response to this request
* &state=STATE_STRING
* - A bookkeeping value that is passed back to Google unchanged
* in the result
* &response_type=code
* - The string code
*/
appHttp.get(me._smarthome.Path_join(httpRoot, 'oauth'), function(req, res) {
me._smarthome.debug('HttpAuth:httpAuthRegister(GET /oauth) query ' + JSON.stringify(req.query));
if (req.query.response_type !== 'code') {
me._smarthome.error('HttpAuth:httpAuthRegister(GET /oauth): response_type ' + req.query.response_type + ' must equal "code"');
return res.status(500)
.send('response_type ' + req.query.response_type + ' must equal "code"');
}
if (!me._smarthome.auth.isValidClient(req.query.client_id)) {
me._smarthome.error('HttpAuth:httpAuthRegister(GET /oauth): client_id ' + req.query.client_id + ' invalid');
return res.status(500).send('client_id ' + req.query.client_id + ' invalid');
}
const useGoogleClientAuth = me._smarthome.auth.useGoogleClientAuth();
const googleClientId = useGoogleClientAuth ? me._smarthome.auth.getGoogleClientId() : '';
// User is not logged in. Show login page.
me._smarthome.debug('HttpAuth:httpAuthRegister(GET /oauth) User is not logged in, showing login page');
fs.readFile(path.join(__dirname, 'frontend/login.html'), 'utf8', function (err, data) {
if (err) {
res.end();
throw(err);
}
res
.set("Content-Security-Policy", "default-src 'self' 'unsafe-inline' *.google.com")
.send(data.replace(/GOOGLE_CLIENT_ID/g, googleClientId).replace(/USE_GOOGLE_LOGIN/g, '' + useGoogleClientAuth));
});
});
//
//
//
appHttp.post(me._smarthome.Path_join(httpRoot, 'oauth'), function(req, res) {
me._smarthome.debug('HttpAuth:httpAuthRegister(POST /oauth): body = ' + JSON.stringify(req.body));
const my_uri = req.protocol + '://' + req.get('Host') + me._smarthome.Path_join(httpRoot, 'oauth');
// client_id in POST date is not decoded automatically by bodyParser and needs to be decoded manually
let client_id = use_decode ? decodeURIComponent(req.body.client_id) : req.body.client_id;
if (!me._smarthome.auth.isValidClient(client_id)) {
me._smarthome.error('HttpAuth:httpAuthRegister(POST /oauth): client_id ' + client_id + ' invalid');
return res.status(500).send('client_id ' + client_id + ' invalid');
}
if (!me._smarthome.auth.isValidRedirectUri(req.body.redirect_uri || '', me._smarthome._debug ? my_uri : '')) {
me._smarthome.error('HttpAuth:httpAuthRegister(POST /oauth): redirect_uri ' + req.body.redirect_uri + ' invalid');
return res.status(500).send('redirect_uri ' + req.body.redirect_uri + ' invalid');
}
if (me._smarthome.auth.useGoogleClientAuth()) {
me._smarthome.debug('HttpAuth:httpAuthRegister(POST /oauth): Google login');
if (req.body.id_token) {
const {OAuth2Client} = require('google-auth-library');
const googleClientId = me._smarthome.auth.getGoogleClientId();
const client = new OAuth2Client(googleClientId);
client
.verifyIdToken({
idToken: req.body.id_token,
audience: googleClientId,
})
.then(function(ticket) {
const payload = ticket.getPayload();
// const userid = payload['sub'];
const email = payload['email'];
const isValidUser = me._smarthome.auth.isGoogleClientEmailValid(email);
me._smarthome.debug('HttpAuth:httpAuthRegister(POST /oauth): email ' + email + " valid: " + isValidUser);
me.handleUserAuth(req, res, email, '', isValidUser, httpRoot);
})
.catch(function(err) {
me._smarthome.error('HttpAuth:httpAuthRegister(POST /oauth): verifyIdToken error ' + err);
me.handleUserAuth(req, res, 'google', '', false, httpRoot);
});
} else {
me.handleUserAuth(req, res, 'google', '', false, httpRoot);
}
} else {
me._smarthome.debug('HttpAuth:httpAuthRegister(POST /oauth): Local login');
let isValidUser = me._smarthome.auth.isValidUser(req.body.username, req.body.password);
me.handleUserAuth(req, res, req.body.username, req.body.password, isValidUser, httpRoot);
}
});
/**
* client_id=GOOGLE_CLIENT_ID
* &client_secret=GOOGLE_CLIENT_SECRET
* &response_type=token
* &grant_type=authorization_code
* &code=AUTHORIZATION_CODE
*
* OR
*
*
* client_id=GOOGLE_CLIENT_ID
* &client_secret=GOOGLE_CLIENT_SECRET
* &response_type=token
* &grant_type=refresh_token
* &refresh_token=REFRESH_TOKEN
*/
appHttp.all(me._smarthome.Path_join(httpRoot, 'token'), function(req, res) {
me._smarthome.debug('HttpAuth:httpAuthRegister(/token): query = ' + JSON.stringify(req.query));
me._smarthome.debug('HttpAuth:httpAuthRegister(/token): body = ' + JSON.stringify(req.body));
const my_uri = req.protocol + '://' + req.get('Host') + me._smarthome.Path_join(httpRoot, 'oauth');
let clientId = req.query.client_id ? req.query.client_id : req.body.client_id;
let clientSecret = req.query.client_secret ? req.query.client_secret : req.body.client_secret;
let grantType = req.query.grant_type ? req.query.grant_type : req.body.grant_type;
if (!me._smarthome.auth.isValidClient(clientId, clientSecret)) {
me._smarthome.error('HttpAuth:httpAuthRegister(/token): invalid client id or secret');
return res.status(400).send('invalid client id or secret');
}
if (grantType === 'authorization_code') {
return me.handleAuthCode(req, res, me._smarthome._debug ? my_uri : '');
} else if (grantType === 'refresh_token') {
return me.handleRefreshToken(req, res);
} else {
me._smarthome.error('HttpAuth:httpAuthRegister(/token): grant_type ' + grantType + ' is not supported');
let error_result = {"error": "invalid_grant"};
return res.status(400).send(error_result);
}
});
}
/******************************************************************************************************************
* private methods
*
*/
/**
* @returns {{}}
* {
* }
*/
handleUserAuth(req, res, username, password, isValidUser, httpRoot) {
if (!isValidUser) {
let redirectUrl = util.format('%s?client_id=%s&redirect_uri=%s&state=%s&response_type=code&error=invalid_user',
this._smarthome.Path_join(httpRoot, 'oauth'), req.body.client_id, encodeURIComponent(req.body.redirect_uri), req.body.state);
this._smarthome._mgmtNode.error('HttpAuth:handleUserAuth(): invalid user');
this._smarthome.debug('HttpAuth:handleUserAuth(): invalid user, redirecting to login form at ' + redirectUrl);
return res.redirect(redirectUrl);
}
this._smarthome.debug('HttpAuth:handleUserAuth(): login successful');
let authCode = this._smarthome.auth.generateAuthCode(username);
if (authCode) {
let redirectUrl = util.format('%s?code=%s&state=%s', req.body.redirect_uri, authCode, req.body.state)
this._smarthome.debug('HttpAuth:handleUserAuth(): authCode generated successfully (authCode = ' + authCode + ')');
this._smarthome.debug('HttpAuth:handleUserAuth(): redirecting to Google at ' + redirectUrl);
return res.redirect(redirectUrl);
} else {
let redirectUrl = util.format('%s?client_id=%s&redirect_uri=%s&state=%s&response_type=code',
this._smarthome.Path_join(httpRoot, 'oauth'), req.body.client_id, encodeURIComponent(req.body.redirect_uri), req.body.state);
this._smarthome._mgmtNode.error('HttpAuth:handleUserAuth(): generating authCode failed');
this._smarthome.debug('HttpAuth:handleUserAuth(): generating authCode failed, redirecting to Google at ' + redirectUrl);
return res.redirect(redirectUrl);
}
}
/**
* @returns {{}}
* {
* token_type: "bearer",
* access_token: "ACCESS_TOKEN",
* refresh_token: "REFRESH_TOKEN"
* expires_in: "EXPIRATION_SECONDS",
* }
*/
handleAuthCode(req, res, my_uri) {
let code = req.query.code ? req.query.code : req.body.code;
let redirect_uri = req.query.redirect_uri ? req.query.redirect_uri : req.body.redirect_uri;
try {
let token = this._smarthome.auth.exchangeAuthCode(code, redirect_uri, my_uri);
this._smarthome.debug('HttpAuth:handleAuthCode(): respond success; token = ' + JSON.stringify(token));
return res.status(200).json(token);
}
catch (err) {
this._smarthome._mgmtNode.error('HttpAuth:handleAuthCode(): ' + err);
let error_result = {"error": "invalid_grant"};
return res.status(400).send(error_result);
}
}
/**
* @returns {{}}
* {
* token_type: "bearer",
* access_token: "ACCESS_TOKEN",
* expires_in: "EXPIRATION_SECONDS",
* }
*/
handleRefreshToken(req, res) {
let refreshToken = req.query.refresh_token ? req.query.refresh_token : req.body.refresh_token;
try {
let token = this._smarthome.auth.refreshAccessToken(refreshToken);
this._smarthome.debug('HttpAuth:handleRefreshToken(): respond success; token = ' + JSON.stringify(token));
return res.status(200).json(token);
}
catch (err) {
this._smarthome._mgmtNode.error('HttpAuth:handleRefreshToken(): ' + err);
let error_result = {"error": "invalid_grant"};
return res.status(400).send(error_result);
}
}
}
module.exports = HttpAuth;