server/auth: add token authentication

* Users are only authenticated against their password on login,
  and to retrieve a token
* Passwords are wiped from the GUI frontend and cookies
  after login and token retrieval
* Tokens are revoked at the end of the session/logout
* If the user chooses the "remember me" option,
  the token is stored in the cookie
* Tokens correctly delete themselves on logout
* Tokens can expire at user-specified date
* Tokens have their last usage time
* Tokens can have user defined descriptions
* Users can manage login tokens in their account settings
This commit is contained in:
ReAnzu
2018-02-25 04:44:02 -06:00
committed by rr-
parent e35e709927
commit 2a69f0193f
36 changed files with 1609 additions and 40 deletions

View File

@ -15,6 +15,7 @@ class Api extends events.EventTarget {
this.user = null;
this.userName = null;
this.userPassword = null;
this.token = null;
this.cache = {};
this.allRanks = [
'anonymous',
@ -87,11 +88,76 @@ class Api extends events.EventTarget {
loginFromCookies() {
const auth = cookies.getJSON('auth');
return auth && auth.user && auth.password ?
this.login(auth.user, auth.password, true) :
return auth && auth.user && auth.token ?
this.loginWithToken(auth.user, auth.token, true) :
Promise.resolve();
}
loginWithToken(userName, token, doRemember) {
this.cache = {};
return new Promise((resolve, reject) => {
this.userName = userName;
this.token = token;
this.get('/user/' + userName + '?bump-login=true')
.then(response => {
const options = {};
if (doRemember) {
options.expires = 365;
}
cookies.set(
'auth',
{'user': userName, 'token': token},
options);
this.user = response;
resolve();
this.dispatchEvent(new CustomEvent('login'));
}, error => {
reject(error);
this.logout();
});
});
}
createToken(userName, options) {
let userTokenRequest = {
enabled: true,
note: 'Web Login Token'
};
if (typeof options.expires !== 'undefined') {
userTokenRequest.expirationTime = new Date().addDays(options.expires).toISOString()
}
return new Promise((resolve, reject) => {
this.post('/user-token/' + userName, userTokenRequest)
.then(response => {
cookies.set(
'auth',
{'user': userName, 'token': response.token},
options);
this.userName = userName;
this.token = response.token;
this.userPassword = null;
}, error => {
reject(error);
});
});
}
deleteToken(userName, userToken) {
return new Promise((resolve, reject) => {
this.delete('/user-token/' + userName + '/' + userToken, {})
.then(response => {
const options = {};
cookies.set(
'auth',
{'user': userName, 'token': null},
options);
resolve();
}, error => {
reject(error);
});
});
}
login(userName, userPassword, doRemember) {
this.cache = {};
return new Promise((resolve, reject) => {
@ -103,10 +169,7 @@ class Api extends events.EventTarget {
if (doRemember) {
options.expires = 365;
}
cookies.set(
'auth',
{'user': userName, 'password': userPassword},
options);
this.createToken(this.userName, options);
this.user = response;
resolve();
this.dispatchEvent(new CustomEvent('login'));
@ -118,9 +181,20 @@ class Api extends events.EventTarget {
}
logout() {
let self = this;
this.deleteToken(this.userName, this.token)
.then(response => {
self._logout();
}, error => {
self._logout();
});
}
_logout() {
this.user = null;
this.userName = null;
this.userPassword = null;
this.token = null;
this.dispatchEvent(new CustomEvent('logout'));
}
@ -137,6 +211,10 @@ class Api extends events.EventTarget {
}
}
isCurrentAuthToken(userToken) {
return userToken.token === this.token;
}
_getFullUrl(url) {
const fullUrl =
(config.apiUrl + '/' + url).replace(/([^:])\/+/g, '$1/');
@ -258,7 +336,11 @@ class Api extends events.EventTarget {
}
try {
if (this.userName && this.userPassword) {
if (this.userName && this.token) {
req.auth = null;
req.set('Authorization', 'Token '
+ new Buffer(this.userName + ":" + this.token).toString('base64'))
} else if (this.userName && this.userPassword) {
req.auth(
this.userName,
encodeURIComponent(this.userPassword)