mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:57:08 +00:00
Merge branch '304-refresh-authentication-status-for-connected-users' into 'devel'
Resolve "Refresh authentication status for connected users" Closes #304 See merge request wgp/dougal/software!56
This commit is contained in:
@@ -82,6 +82,8 @@ export default {
|
|||||||
if (event.channel == "project" && event.payload?.schema == "public") {
|
if (event.channel == "project" && event.payload?.schema == "public") {
|
||||||
// Projects changed in some way or another
|
// Projects changed in some way or another
|
||||||
await this.refreshProjects();
|
await this.refreshProjects();
|
||||||
|
} else if (event.channel == ".jwt" && event.payload?.token) {
|
||||||
|
await this.setCredentials({token: event.payload?.token});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import vueDebounce from 'vue-debounce'
|
|||||||
import { mapMutations } from 'vuex';
|
import { mapMutations } from 'vuex';
|
||||||
import { markdown, markdownInline } from './lib/markdown';
|
import { markdown, markdownInline } from './lib/markdown';
|
||||||
import { geometryAsString } from './lib/utils';
|
import { geometryAsString } from './lib/utils';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
@@ -32,10 +34,16 @@ new Vue({
|
|||||||
user: null,
|
user: null,
|
||||||
|
|
||||||
wsUrl: "/ws",
|
wsUrl: "/ws",
|
||||||
ws: null
|
ws: null,
|
||||||
|
wsCredentialsCheckInterval: 300*1000, // ms
|
||||||
|
wsCredentialsCheckTimer: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["jwt"])
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
markdown (value) {
|
markdown (value) {
|
||||||
@@ -85,6 +93,17 @@ new Vue({
|
|||||||
this.setServerConnectionState(false);
|
this.setServerConnectionState(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.wsCredentialsCheckTimer) {
|
||||||
|
clearInterval(this.wsCredentialsCheckTimer);
|
||||||
|
this.wsCredentialsCheckTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wsCredentialsCheckTimer = setInterval( () => {
|
||||||
|
this.ws.send(JSON.stringify({
|
||||||
|
jwt: this.jwt
|
||||||
|
}));
|
||||||
|
}, this.wsCredentialsCheckInterval);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
...mapMutations(['setServerEvent', 'setServerConnectionState'])
|
...mapMutations(['setServerEvent', 'setServerConnectionState'])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
async function api ({state, commit, dispatch}, [resource, init = {}, cb]) {
|
async function api ({state, getters, commit, dispatch}, [resource, init = {}, cb]) {
|
||||||
try {
|
try {
|
||||||
commit("queueRequest");
|
commit("queueRequest");
|
||||||
if (init && init.hasOwnProperty("body")) {
|
if (init && init.hasOwnProperty("body")) {
|
||||||
@@ -9,10 +9,17 @@ async function api ({state, commit, dispatch}, [resource, init = {}, cb]) {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (!init.headers) {
|
||||||
|
init.headers = {};
|
||||||
|
}
|
||||||
|
// We also send Authorization: Bearer …
|
||||||
|
if (getters.jwt) {
|
||||||
|
init.headers["Authorization"] = "Bearer "+getters.jwt;
|
||||||
|
}
|
||||||
if (typeof init.body != "string") {
|
if (typeof init.body != "string") {
|
||||||
init.body = JSON.stringify(init.body);
|
init.body = JSON.stringify(init.body);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const url = /^https?:\/\//i.test(resource) ? resource : (state.apiUrl + resource);
|
const url = /^https?:\/\//i.test(resource) ? resource : (state.apiUrl + resource);
|
||||||
const res = await fetch(url, init);
|
const res = await fetch(url, init);
|
||||||
if (typeof cb === 'function') {
|
if (typeof cb === 'function') {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ async function login ({commit, dispatch}, loginRequest) {
|
|||||||
}
|
}
|
||||||
const res = await dispatch('api', [url, init]);
|
const res = await dispatch('api', [url, init]);
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
await dispatch('setCredentials', true);
|
await dispatch('setCredentials', {force: true});
|
||||||
await dispatch('loadUserPreferences');
|
await dispatch('loadUserPreferences');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,12 +34,12 @@ function cookieChanged (cookie) {
|
|||||||
return browserCookie != cookie;
|
return browserCookie != cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCredentials ({state, commit, getters, dispatch}, force = false) {
|
function setCredentials ({state, commit, getters, dispatch}, {force, token} = {}) {
|
||||||
if (cookieChanged(state.cookie) || force) {
|
if (token || force || cookieChanged(state.cookie)) {
|
||||||
try {
|
try {
|
||||||
const cookie = browserCookie();
|
const cookie = browserCookie();
|
||||||
const decoded = cookie ? jwt_decode(cookie.split("=")[1]) : null;
|
const decoded = (token ?? cookie) ? jwt_decode(token ?? cookie.split("=")[1]) : null;
|
||||||
commit('setCookie', cookie);
|
commit('setCookie', (cookie ?? (token && ("JWT="+token))) || undefined);
|
||||||
commit('setUser', decoded);
|
commit('setUser', decoded);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name == "InvalidTokenError") {
|
if (err.name == "InvalidTokenError") {
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ function user (state) {
|
|||||||
return state.user;
|
return state.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function jwt (state) {
|
||||||
|
if (state.cookie?.startsWith("JWT=")) {
|
||||||
|
return state.cookie.substring(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function writeaccess (state) {
|
function writeaccess (state) {
|
||||||
return state.user && ["user", "admin"].includes(state.user.role);
|
return state.user && ["user", "admin"].includes(state.user.role);
|
||||||
}
|
}
|
||||||
@@ -15,4 +21,4 @@ function preferences (state) {
|
|||||||
return state.preferences;
|
return state.preferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { user, writeaccess, adminaccess, preferences };
|
export default { user, jwt, writeaccess, adminaccess, preferences };
|
||||||
|
|||||||
@@ -1,28 +1,21 @@
|
|||||||
const crypto = require('crypto');
|
|
||||||
const cfg = require('../../../lib/config');
|
const cfg = require('../../../lib/config');
|
||||||
const jwt = require('../../../lib/jwt');
|
const jwt = require('../../../lib/jwt');
|
||||||
|
|
||||||
async function login (req, res, next) {
|
async function login (req, res, next) {
|
||||||
if (req.body) {
|
if (req.body) {
|
||||||
const {user, password} = req.body;
|
const {user, password} = req.body;
|
||||||
if (user && password) {
|
const payload = jwt.checkValidCredentials({user, password});
|
||||||
const hash = crypto
|
if (payload) {
|
||||||
.pbkdf2Sync(password, 'Dougal'+user, 10712, 48, 'sha512')
|
|
||||||
.toString('base64');
|
|
||||||
for (const credentials of cfg._("global.users.login.user") || []) {
|
|
||||||
if (credentials.name == user && credentials.hash == hash) {
|
|
||||||
const payload = Object.assign({}, credentials);
|
|
||||||
delete payload.hash;
|
|
||||||
jwt.issue(payload, req, res);
|
jwt.issue(payload, req, res);
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
} else {
|
||||||
}
|
|
||||||
next({status: 401, message: "Unauthorised"});
|
next({status: 401, message: "Unauthorised"});
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
next({status: 400, message: "Bad request"});
|
next({status: 400, message: "Bad request"});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = login;
|
module.exports = login;
|
||||||
|
|||||||
@@ -1,9 +1,37 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
const crypto = require('crypto');
|
||||||
|
const JWT = require('jsonwebtoken');
|
||||||
const cfg = require('./config');
|
const cfg = require('./config');
|
||||||
|
|
||||||
|
|
||||||
|
function checkValidCredentials ({user, password, jwt}) {
|
||||||
|
if (jwt) {
|
||||||
|
try {
|
||||||
|
const decoded = JWT.verify(jwt, cfg.jwt.secret, {maxAge: "1d"});
|
||||||
|
delete decoded.iat;
|
||||||
|
delete decoded.exp;
|
||||||
|
return decoded;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to verify credentials for", jwt);
|
||||||
|
console.warn(err);
|
||||||
|
return; // Invalid JWT
|
||||||
|
}
|
||||||
|
} else if (user && password) {
|
||||||
|
const hash = crypto
|
||||||
|
.pbkdf2Sync(password, 'Dougal'+user, 10712, 48, 'sha512')
|
||||||
|
.toString('base64');
|
||||||
|
for (const credentials of cfg._("global.users.login.user") || []) {
|
||||||
|
if (credentials.name == user && credentials.hash == hash) {
|
||||||
|
const payload = {...credentials};
|
||||||
|
delete payload.hash;
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function issue (payload, req, res) {
|
function issue (payload, req, res) {
|
||||||
|
|
||||||
const token = jwt.sign(payload, cfg.jwt.secret, cfg.jwt.options);
|
const token = JWT.sign(payload, cfg.jwt.secret, cfg.jwt.options);
|
||||||
|
|
||||||
if (req) {
|
if (req) {
|
||||||
req.user = payload;
|
req.user = payload;
|
||||||
@@ -17,5 +45,6 @@ function issue (payload, req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
checkValidCredentials,
|
||||||
issue
|
issue
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const ws = require('ws');
|
|||||||
const URL = require('url');
|
const URL = require('url');
|
||||||
const { listen } = require('../lib/db/notify');
|
const { listen } = require('../lib/db/notify');
|
||||||
const channels = require('../lib/db/channels');
|
const channels = require('../lib/db/channels');
|
||||||
|
const jwt = require('../lib/jwt');
|
||||||
|
|
||||||
function start (server, pingInterval=30000) {
|
function start (server, pingInterval=30000) {
|
||||||
|
|
||||||
@@ -9,7 +10,31 @@ function start (server, pingInterval=30000) {
|
|||||||
wsServer.on('connection', socket => {
|
wsServer.on('connection', socket => {
|
||||||
socket.alive = true;
|
socket.alive = true;
|
||||||
socket.on('pong', function () { this.alive = true; })
|
socket.on('pong', function () { this.alive = true; })
|
||||||
socket.on('message', message => console.log(message));
|
socket.on('message', message => {
|
||||||
|
// console.log("Websocket message:");
|
||||||
|
// console.log(message);
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(message);
|
||||||
|
if (payload?.jwt) {
|
||||||
|
// console.log("Refresh JWT token", payload);
|
||||||
|
const decoded = jwt.checkValidCredentials({jwt: payload.jwt});
|
||||||
|
// console.log("Decoded", decoded);
|
||||||
|
if (decoded) {
|
||||||
|
delete decoded.exp;
|
||||||
|
const token = jwt.issue(decoded);
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
channel: ".jwt",
|
||||||
|
payload: {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Websocket message decoding failed", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on('upgrade', (request, socket, head) => {
|
server.on('upgrade', (request, socket, head) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user