Have refined my approach to avoid touching any core files. Single Sign on now entirely handled via nginx+lua.
(in node-red’s settings.js I parse the configuration.yaml, then sync the NR password with the hass password.
In summary, both NR and hass live behind a nginx firewall (currently exposing port 8888)
As I had to learn nginx+lua to write this, feel free to suggest improvements.
#
# nginx-lua config to provide single front end
# to hass (farm assistant) and nodered
#
# Assumes token based auth and shared access to
# api_password
#
worker_processes 1;
daemon off;
error_log /dev/stderr debug;
events {
worker_connections 255;
}
http {
error_log /dev/stderr debug;
access_log /dev/stdout;
lua_need_request_body on;
access_log /dev/stdout;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream nodered {
server 127.0.0.1:1880;
}
upstream fa {
server 127.0.0.1:8123;
}
server {
listen 8888;
# custom auth as sub request
# change user from admin???
location /_nrauth {
set $rargs "client_id=node-red-editor&grant_type=password&scope=&username=admin&password=";
rewrite_by_lua '
ngx.var.rargs = ngx.var.rargs .. ngx.var.pass
ngx.log(ngx.NOTICE,"Args :" .. ngx.var.rargs)
';
internal;
proxy_method POST;
proxy_set_body $rargs;
proxy_set_header Accept application/json;
proxy_set_header Content-Type application/x-www-form-urlencoded;
proxy_pass http://127.0.0.1:1880/nodered/auth/token;
proxy_redirect http://127.0.0.1:1880/nodered/auth/token /;
proxy_read_timeout 2s;
# May not need or want to set Host. Should default to the above hostname.
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
}
# default nodered handler
location /nodered {
set $token "";
access_by_lua '
if ngx.var.cookie_access_token ~= nil then
ngx.var.token = "Bearer " .. ngx.var.cookie_access_token
ngx.log(ngx.NOTICE,"Node Red added header!")
else
ngx.log(ngx.NOTICE,"Node Red failed to add header!")
ngx.log(ngx.ERR, "Failed to authenticate")
ngx.redirect("http://127.0.0.1:8888/")
end
';
proxy_pass http://nodered;
proxy_http_version 1.1;
proxy_set_header Authorization $token;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location / {
proxy_pass http://fa;
}
# Handle custom nodered login
location /nodered/login {
set $token ='';
set $pass ='';
access_by_lua '
ngx.log(ngx.NOTICE, "Node Red Login attempt!")
if ngx.var.arg_api_password == nil then
ngx.ctx.red = 0
ngx.log(ngx.ERR, "API Password not provided")
return ngx.exit(403)
end
local ck = require("lcookies")
local json = require("cjson")
ngx.log(ngx.NOTICE,"Query string: [" .. ngx.var.arg_api_password .. "]")
ngx.var.pass= ngx.var.arg_api_password;
local res= ngx.location.capture (
"/_nrauth",
{ always_forward_body = true, copy_all_vars = true}
)
ngx.log(ngx.NOTICE,"Result: [" .. res.status .. "]")
if string.sub(res.status,0,3) ~= "200" then
ngx.ctx.red = 0
ngx.log(ngx.ERR, "Failed to authenticate")
return ngx.exit(403)
end
-- ngx.log(ngx.NOTICE,res.body)
local tab = json.decode(res.body);
if tab["access_token"] ~= nil then
ngx.var.token = tab["access_token"];
local expires = tab["expires_in"];
local cooki = "access_token=" .. tab["access_token"] .. "; Path=/"
ck.add_cookie(cooki,expires)
ngx.header["Authorization"] = "Bearer " .. ngx.var.token
ngx.req.set_uri_args({})
else
ngx.log(ngx.ERR, "Failed to authenticate")
return ngx.exit(403)
end
';
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_pass http://127.0.0.1:1880/nodered;
}
# If cookie set to "" (log_out)
# recreate, as cached partial-red does not reload on login
location /api/stream {
access_by_lua '
ngx.log(ngx.NOTICE,"Refresh cookie attempt!")
if ngx.var.request_method == ngx.HTTP_GET and ngx.var.cookie_access_token ~= nil and ngx.var.cookie_access_token == "" then
ngx.log(ngx.NOTICE,"Refresh cookie!")
ngx.log(ngx.NOTICE,"Query string: [" .. ngx.var.arg_api_password .. "]")
ngx.var.pass= ngx.var.arg_api_password;
local res= ngx.location.capture (
"/_nrauth",
{ always_forward_body = true, copy_all_vars = true }
)
if res.status ~= nil and string.sub(res.status,0,3) ~= "200" then
ngx.log(ngx.NOTICE,"Status returned: [" .. string.sub(res.status,0,3) .. "]")
ngx.log(ngx.ERR, "Failed to authenticate")
else
local ck = require("lcookies")
local json = require("cjson")
local tab = json.decode(res.body);
ngx.log(ngx.NOTICE,res.body)
if tab["access_token"] ~= nil then
ngx.log(ngx.NOTICE, "Updated cookie")
ngx.var.token = tab["access_token"];
local expires = tab["expires_in"];
local cooki = "access_token=" .. tab["access_token"] .. "; Path=/"
ck.add_cookie(cooki,expires)
ngx.redirect("http://127.0.0.1:8888/api/stream?" .. ngx.var.query_string)
ngx.exit(ngx.OK)
end
end
end
';
proxy_pass http://fa;
}
# Handle hass logout
# remove cookie
location /api/log_out {
access_by_lua '
ngx.log(ngx.NOTICE,"Logout!")
if ngx.var.cookie_access_token ~= nil then
local ck = require("lcookies")
ngx.log(ngx.NOTICE,"Node Red reset cookie!")
local cooki = "access_token=; Path=/"
ck.add_cookie(cooki,0)
end
';
proxy_pass http://fa;
}
}
}