mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:17:08 +00:00
Fix user authentication.
* Use X-JWT header for sending authentication info both from server to client and from client to server. * Send token in body of login response. * Also use Set-Cookie: JWT=… so that calls that are not issued directly by Dougal (e.g. Deck.gl layers with a URL `data` property) work without having to jump through hoops. Closes #321
This commit is contained in:
@@ -4,7 +4,7 @@ import router from './router'
|
||||
import store from './store'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import vueDebounce from 'vue-debounce'
|
||||
import { mapMutations } from 'vuex';
|
||||
import { mapMutations, mapActions } from 'vuex';
|
||||
import { markdown, markdownInline } from './lib/markdown';
|
||||
import { geometryAsString } from './lib/utils';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -67,7 +67,14 @@ new Vue({
|
||||
this.snack = true;
|
||||
},
|
||||
|
||||
sendJwt () {
|
||||
if (this.jwt) {
|
||||
this.ws.send(JSON.stringify({ jwt: this.jwt }));
|
||||
}
|
||||
},
|
||||
|
||||
initWs () {
|
||||
|
||||
if (this.ws) {
|
||||
console.log("WebSocket initWs already called");
|
||||
return;
|
||||
@@ -77,11 +84,12 @@ new Vue({
|
||||
|
||||
this.ws.addEventListener("message", (ev) => {
|
||||
const msg = JSON.parse(ev.data);
|
||||
this.setServerEvent(msg);
|
||||
this.processServerEvent(msg);
|
||||
});
|
||||
|
||||
this.ws.addEventListener("open", (ev) => {
|
||||
console.log("WebSocket connection open", ev);
|
||||
this.sendJwt()
|
||||
this.setServerConnectionState(true);
|
||||
});
|
||||
|
||||
@@ -107,14 +115,13 @@ new Vue({
|
||||
}
|
||||
|
||||
this.wsCredentialsCheckTimer = setInterval( () => {
|
||||
this.ws.send(JSON.stringify({
|
||||
jwt: this.jwt
|
||||
}));
|
||||
this.sendJwt();
|
||||
}, this.wsCredentialsCheckInterval);
|
||||
|
||||
},
|
||||
|
||||
...mapMutations(['setServerEvent', 'setServerConnectionState'])
|
||||
...mapMutations(['setServerConnectionState']),
|
||||
...mapActions(['processServerEvent'])
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ async function api ({state, getters, commit, dispatch}, [resource, init = {}, cb
|
||||
}
|
||||
// We also send Authorization: Bearer …
|
||||
if (getters.jwt) {
|
||||
init.credentials = "include";
|
||||
init.headers["Authorization"] = "Bearer "+getters.jwt;
|
||||
}
|
||||
if (typeof init.body != "string") {
|
||||
@@ -81,7 +82,9 @@ async function api ({state, getters, commit, dispatch}, [resource, init = {}, cb
|
||||
if (res.ok) {
|
||||
|
||||
if (!isCached) {
|
||||
await dispatch('setCredentials');
|
||||
if (res.headers.has("x-jwt")) {
|
||||
await dispatch('setCredentials', { response: res });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import jwt_decode from 'jwt-decode';
|
||||
import { User } from '@/lib/user';
|
||||
|
||||
async function login ({commit, dispatch}, loginRequest) {
|
||||
async function login ({ commit, dispatch }, loginRequest) {
|
||||
const url = "/login";
|
||||
const init = {
|
||||
method: "POST",
|
||||
@@ -9,96 +9,111 @@ async function login ({commit, dispatch}, loginRequest) {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: loginRequest
|
||||
};
|
||||
|
||||
const callback = async (err, res) => {
|
||||
if (!err && res) {
|
||||
const { token } = (await res.json());
|
||||
await dispatch('setCredentials', {token});
|
||||
}
|
||||
}
|
||||
const res = await dispatch('api', [url, init]);
|
||||
if (res && res.ok) {
|
||||
await dispatch('setCredentials', {force: true});
|
||||
await dispatch('loadUserPreferences');
|
||||
}
|
||||
|
||||
await dispatch('api', [url, init, callback]);
|
||||
await dispatch('loadUserPreferences');
|
||||
}
|
||||
|
||||
async function logout ({commit, dispatch}) {
|
||||
commit('setCookie', null);
|
||||
async function logout ({ commit, dispatch }) {
|
||||
commit('setToken', null);
|
||||
commit('setUser', null);
|
||||
// Should delete JWT cookie
|
||||
await dispatch('api', ["/logout"]);
|
||||
|
||||
// Clear preferences
|
||||
commit('setPreferences', {});
|
||||
}
|
||||
|
||||
function browserCookie (state) {
|
||||
return document.cookie.split(/; */).find(i => /^JWT=.+/.test(i));
|
||||
function setCookie(context, {name, value, expiry, path}) {
|
||||
if (!name) name = "JWT";
|
||||
if (!path) path = "/";
|
||||
if (!value) value = "";
|
||||
|
||||
if (expiry) {
|
||||
document.cookie = `${name}=${value}; expiry=${(new Date(expiry)).toUTCString()}; path=${path}`;
|
||||
} else {
|
||||
document.cookie = `${name}=${value}; path=${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
function cookieChanged (cookie) {
|
||||
return browserCookie != cookie;
|
||||
}
|
||||
function setCredentials({ state, commit, getters, dispatch, rootState }, { force, token, response } = {}) {
|
||||
try {
|
||||
let tokenValue = token;
|
||||
|
||||
function setCredentials ({state, commit, getters, dispatch, rootState}, {force, token} = {}) {
|
||||
if (token || force || cookieChanged(state.cookie)) {
|
||||
try {
|
||||
const cookie = browserCookie();
|
||||
const decoded = (token ?? cookie) ? jwt_decode(token ?? cookie.split("=")[1]) : null;
|
||||
commit('setCookie', (cookie ?? (token && ("JWT="+token))) || undefined);
|
||||
if (!tokenValue && response?.headers?.get('x-jwt')) {
|
||||
tokenValue = response.headers.get('x-jwt');
|
||||
}
|
||||
|
||||
if (!tokenValue) {
|
||||
console.log('No JWT found in token or response');
|
||||
return;
|
||||
}
|
||||
|
||||
if (force || tokenValue !== getters.jwt) {
|
||||
const decoded = jwt_decode(tokenValue);
|
||||
commit('setToken', tokenValue);
|
||||
commit('setUser', decoded ? new User(decoded, rootState.api.api) : null);
|
||||
} catch (err) {
|
||||
if (err.name == "InvalidTokenError") {
|
||||
console.warn("Failed to decode", browserCookie());
|
||||
|
||||
if (tokenValue && decoded) {
|
||||
if (decoded?.exp) {
|
||||
dispatch('setCookie', {value: tokenValue, expiry: decoded.exp*1000});
|
||||
} else {
|
||||
dispatch('setCookie', {value: tokenValue});
|
||||
}
|
||||
} else {
|
||||
console.error("setCredentials", err);
|
||||
// Clear the cookie
|
||||
dispatch('setCookie', {value: "", expiry: 0});
|
||||
}
|
||||
|
||||
console.log('Credentials refreshed at', new Date().toISOString());
|
||||
} else {
|
||||
console.log('JWT unchanged, skipping update');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('setCredentials error:', err.message, 'token:', token, 'response:', response?.headers?.get('x-jwt'));
|
||||
if (err.name === 'InvalidTokenError') {
|
||||
commit('setToken', null);
|
||||
commit('setUser', null);
|
||||
}
|
||||
}
|
||||
dispatch('loadUserPreferences');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user preferences to localStorage and store.
|
||||
*
|
||||
* User preferences are identified by a key that gets
|
||||
* prefixed with the user ID. The value can
|
||||
* be anything that JSON.stringify can parse.
|
||||
*/
|
||||
function saveUserPreference ({state, commit}, [key, value]) {
|
||||
function saveUserPreference({ state, commit }, [key, value]) {
|
||||
const k = `${state.user?.id}.${key}`;
|
||||
|
||||
if (value !== undefined) {
|
||||
localStorage.setItem(k, JSON.stringify(value));
|
||||
|
||||
const preferences = state.preferences;
|
||||
preferences[key] = value;
|
||||
const preferences = { ...state.preferences, [key]: value };
|
||||
commit('setPreferences', preferences);
|
||||
} else {
|
||||
localStorage.removeItem(k);
|
||||
|
||||
const preferences = state.preferences;
|
||||
const preferences = { ...state.preferences };
|
||||
delete preferences[key];
|
||||
commit('setPreferences', preferences);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUserPreferences ({state, commit}) {
|
||||
// Get all keys which are of interest to us
|
||||
async function loadUserPreferences({ state, commit }) {
|
||||
const prefix = `${state.user?.id}`;
|
||||
const keys = Object.keys(localStorage).filter( k => k.startsWith(prefix) );
|
||||
|
||||
// Build the preferences object
|
||||
const keys = Object.keys(localStorage).filter(k => k.startsWith(prefix));
|
||||
const preferences = {};
|
||||
keys.map(str => {
|
||||
keys.forEach(str => {
|
||||
const value = JSON.parse(localStorage.getItem(str));
|
||||
const key = str.split(".").slice(2).join(".");
|
||||
preferences[key] = value;
|
||||
});
|
||||
|
||||
// Commit it
|
||||
commit('setPreferences', preferences);
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
login,
|
||||
logout,
|
||||
setCookie,
|
||||
setCredentials,
|
||||
saveUserPreference,
|
||||
loadUserPreferences
|
||||
|
||||
@@ -4,8 +4,12 @@ function user (state) {
|
||||
}
|
||||
|
||||
function jwt (state) {
|
||||
if (state.cookie?.startsWith("JWT=")) {
|
||||
return state.cookie.substring(4);
|
||||
return state.token;
|
||||
}
|
||||
|
||||
function cookie (state) {
|
||||
if (state.token) {
|
||||
return "JWT="+token;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
function setCookie (state, cookie) {
|
||||
state.cookie = cookie;
|
||||
function setToken (state, token) {
|
||||
state.token = token;
|
||||
localStorage?.setItem("jwt", token);
|
||||
}
|
||||
|
||||
function setUser (state, user) {
|
||||
@@ -11,4 +12,4 @@ function setPreferences (state, preferences) {
|
||||
state.preferences = preferences;
|
||||
}
|
||||
|
||||
export default { setCookie, setUser, setPreferences };
|
||||
export default { setToken, setUser, setPreferences };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const state = () => ({
|
||||
cookie: null,
|
||||
token: localStorage?.getItem("jwt") ?? null,
|
||||
user: null,
|
||||
preferences: {}
|
||||
});
|
||||
|
||||
@@ -81,6 +81,13 @@ export default {
|
||||
await this.logout();
|
||||
await this.login(this.credentials);
|
||||
|
||||
if (this.user) {
|
||||
console.log("Login successful");
|
||||
// Should trigger auto-refresh over ws as well as authenticating the
|
||||
// user over ws.
|
||||
this.$root.sendJwt();
|
||||
}
|
||||
|
||||
if (this.user && !this.user.autologin) {
|
||||
this.$router.replace("/");
|
||||
} else {
|
||||
|
||||
@@ -6,8 +6,10 @@ async function login (req, res, next) {
|
||||
const {user, password} = req.body;
|
||||
const payload = await jwt.checkValidCredentials({user, password});
|
||||
if (payload) {
|
||||
jwt.issue(payload, req, res);
|
||||
res.status(204).send();
|
||||
const token = jwt.issue(payload, req, res);
|
||||
res.set("X-JWT", token);
|
||||
res.set("Set-Cookie", `JWT=${token}`); // For good measure
|
||||
res.status(200).send({token});
|
||||
next();
|
||||
return;
|
||||
} else {
|
||||
|
||||
@@ -43,7 +43,8 @@ function issue (payload, req, res) {
|
||||
}
|
||||
|
||||
if (res) {
|
||||
res.cookie("JWT", token, {maxAge: cfg.jwt.options.expiresIn*1000 || 0});
|
||||
res.set("X-JWT", token);
|
||||
res.set("Set-Cookie", `JWT=${token}`); // For good measure
|
||||
}
|
||||
|
||||
return token;
|
||||
@@ -51,5 +52,6 @@ function issue (payload, req, res) {
|
||||
|
||||
module.exports = {
|
||||
checkValidCredentials,
|
||||
issue
|
||||
issue,
|
||||
decode: JWT.decode
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user