From 313084683984145b5a149fb5be007a8a8c7349cd Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 29 Apr 2020 14:10:21 +0300 Subject: [PATCH 01/15] Extract auth server functionality into separate module. Make it use JWTs --- server/src/auth-utils.js | 39 ++++++++++++++++++ server/src/auth.js | 88 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 server/src/auth-utils.js create mode 100644 server/src/auth.js diff --git a/server/src/auth-utils.js b/server/src/auth-utils.js new file mode 100644 index 0000000000..64349f3fed --- /dev/null +++ b/server/src/auth-utils.js @@ -0,0 +1,39 @@ +const jwt = require('jsonwebtoken'); +const expiresIn = process.env.JWT_TOKEN_VALIDITY || '7 days'; +const issuer = process.env.JWT_TOKEN_ISSUER || 'Tangerine Devs'; +const jwtTokenSecret = + process.env.JWT_TOKEN_SECRET || + 'tstststsgsvdsjheytetew4567823e76tfvbendi876tfvewbndfitw562yhwbdnfjytf2wvedtrrfwvbhytfwhystrwfgtwfgwhygwvbtwtgvwftwghbb'; + +const createLoginJWT = ({ username }) => { + const signingOptions = { + issuer, + subject: username, + expiresIn, + }; + return jwt.sign({ username }, jwtTokenSecret, signingOptions); +}; + +const verifyJWT = (token) => { + try { + const jwtPayload = jwt.verify(token, jwtTokenSecret, { issuer }); + return !!jwtPayload; + } catch (error) { + return false; + } +}; + +const decodeJWT = (token) => { + try { + const jwtPayload = jwt.verify(token, jwtTokenSecret, { issuer }); + return jwtPayload; + } catch (error) { + return undefined; + } +}; + +module.exports = { + createLoginJWT, + verifyJWT, + decodeJWT, +}; diff --git a/server/src/auth.js b/server/src/auth.js new file mode 100644 index 0000000000..7e446d5000 --- /dev/null +++ b/server/src/auth.js @@ -0,0 +1,88 @@ +const log = require('tangy-log').log; +const bcrypt = require('bcryptjs'); +const DB = require('./db.js'); +const USERS_DB = new DB('users'); +const { createLoginJWT } = require('./auth-utils'); +const login = async (req, res) => { + const { username, password } = req.body; + try { + if (await areCredentialsValid(username, password)) { + const token = createLoginJWT({ username }); + log.info(`${username} login success`); + return res.status(200).send({data: { token }}); + } else { + log.info(`${username} login fail`); + return res.status(401).send({ data: 'Invalid Credentials' }); + } + } catch (error) { + log.info(`${username} login failure`); + return res.status(401).send({ data: 'Could not login user' }); + } +}; + +const findUserByUsername = async (username) => { + const result = await USERS_DB.find({ selector: { username } }); + return result.docs[0]; +}; + +const areCredentialsValid = async (username, password) => { + try { + let isValid = false; + if ( + username == process.env.T_USER1 && + password == process.env.T_USER1_PASSWORD + ) { + isValid = true; + return isValid; + } else { + if (await doesUserExist(username)) { + const data = await findUserByUsername(username); + const hashedPassword = data.password; + isValid = await bcrypt.compare(password, hashedPassword); + return isValid; + } else { + return isValid; + } + } + } catch (error) { + return false; + } +}; + +const doesUserExist = async (username) => { + try { + if (await isSuperAdmin(username)) { + return true; + } else { + await USERS_DB.createIndex({ index: { fields: ['username'] } }); + const data = await findUserByUsername(username); + return data && data.username && data.username.length > 0; + } + } catch (error) { + console.error(error); + return true; // In case of error assume user exists. Helps avoid same username used multiple times + } +}; +const isSuperAdmin = async (username) => { + return username === process.env.T_USER1; +}; + +const hashPassword = async (password) => { + try { + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + return hashedPassword; + } catch (error) { + console.error(error); + } +}; + +module.exports = { + areCredentialsValid, + doesUserExist, + findUserByUsername, + hashPassword, + isSuperAdmin, + login, + USERS_DB, +}; From d7d16977ca39a9a39637f280cd51ce2838487d66 Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 29 Apr 2020 14:25:01 +0300 Subject: [PATCH 02/15] Remove unnecessary require --- server/src/express-app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/express-app.js b/server/src/express-app.js index b67f7cef62..8aa1670167 100644 --- a/server/src/express-app.js +++ b/server/src/express-app.js @@ -25,7 +25,6 @@ const chalk = require('chalk'); const pretty = require('pretty') const flatten = require('flat') const json2csv = require('json2csv') -const bcrypt = require('bcryptjs'); const _ = require('underscore') const log = require('tangy-log').log const clog = require('tangy-log').clog From 7c739765f584a6d205c87fb09da4f6def87b2895 Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 29 Apr 2020 14:26:22 +0300 Subject: [PATCH 03/15] Refactor code into modules and remove from main express file. --- server/src/express-app.js | 108 ++------------------------------------ 1 file changed, 3 insertions(+), 105 deletions(-) diff --git a/server/src/express-app.js b/server/src/express-app.js index 8aa1670167..24db7cc8b6 100644 --- a/server/src/express-app.js +++ b/server/src/express-app.js @@ -33,13 +33,12 @@ const multer = require('multer') const upload = multer({ dest: '/tmp-uploads/' }) // Place a groupName in this array and between runs of the reporting worker it will be added to the worker's state. var newGroupQueue = [] -const DB = require('./db.js') -const USERS_DB = new DB('users'); let crypto = require('crypto'); const junk = require('junk'); const cors = require('cors') const sep = path.sep; const tangyModules = require('./modules/index.js')() +const {doesUserExist, findUserByUsername, isSuperAdmin, hashPassword, USERS_DB, login} = require('./auth') log.info('heartbeat') setInterval(() => log.info('heartbeat'), 5*60*1000) @@ -83,91 +82,21 @@ app.use(cors({ })); app.options('*', cors()) // include before other routes -/* - * Auth - */ -var passport = require('passport') - , LocalStrategy = require('passport-local').Strategy; - -// This determines wether or not a login is valid. -passport.use(new LocalStrategy( - async function (username, password, done) { - if (await areCredentialsValid(username, password)) { - log.info(`${username} login success`) - return done(null, { - name: username - }); - } else { - log.info(`${username} login fail`) - return done(null, false, { message: 'Incorrect username or password' }) - } - } -)); - -async function findUserByUsername(username) { - const result = await USERS_DB.find({ selector: { username } }); - return result.docs[0]; -} - -async function areCredentialsValid(username, password) { - try { - let isValid = false; - if (username == process.env.T_USER1 && password == process.env.T_USER1_PASSWORD) { - isValid = true; - return isValid; - } else { - if (await doesUserExist(username)) { - const data = await findUserByUsername(username); - const hashedPassword = data.password; - isValid = await bcrypt.compare(password, hashedPassword); - return isValid; - } - else { return isValid; } - } - } catch (error) { - return false; - } -} - -// This decides what identifying piece of information to put in a cookie for the session. -passport.serializeUser(function (user, done) { - done(null, user.name); -}); - -// This transforms the id in the session cookie to pass to req.user object. -passport.deserializeUser(function (id, done) { - done(null, { name: id }); -}); - -// Use sessions. -app.use(session({ - secret: "cats", - resave: false, - saveUninitialized: new PouchSession(new DB('sessions')) -})); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json({ limit: '1gb' })) app.use(bodyParser.text({ limit: '1gb' })) app.use(compression()) -app.use(passport.initialize()); -app.use(passport.session()); - // Middleware to protect routes. var isAuthenticated = require('./middleware/is-authenticated.js') var hasUploadToken = require('./middleware/has-upload-token.js') var isAuthenticatedOrHasUploadToken = require('./middleware/is-authenticated-or-has-upload-token.js') // Login service. -app.post('/login', - passport.authenticate('local', { failureRedirect: '/login' }), - function (req, res) { - res.send({ name: req.user.name, statusCode: 200, statusMessage: 'ok' }); - } -); +app.post('/login', login); app.get('/login/validate/:userName', function (req, res) { - if (req.user && req.params.userName === req.user.name) { + if (req.user && (req.params.userName === req.user.name)) { res.send({ valid: true }); } else { res.send({ valid: false }); @@ -309,23 +238,6 @@ app.get('/users/userExists/:username', isAuthenticated, async (req, res) => { } }); - -async function doesUserExist(username) { - try { - if (await isSuperAdmin(username)) { - return true; - } else { - await USERS_DB.createIndex({ index: { fields: ['username'] } }); - const data = await findUserByUsername(username); - return data && data.username && data.username.length > 0; - } - - } catch (error) { - console.error(error); - return true; // In case of error assume user exists. Helps avoid same username used multiple times - } -} - app.post('/users/register-user', isAuthenticated, async (req, res) => { try { if (!(await doesUserExist(req.body.username))) { @@ -375,16 +287,6 @@ app.get('/users/isAdminUser/:username', isAuthenticated, async (req, res) => { } }); -async function hashPassword(password) { - try { - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(password, salt); - return hashedPassword; - } catch (error) { - console.error(error); - } -} - app.post('/editor/file/save', isAuthenticated, async function (req, res) { const filePath = req.body.filePath const groupId = req.body.groupId @@ -457,10 +359,6 @@ async function getGroupsByUser(username) { return groups; } } -async function isSuperAdmin(username) { - return username === process.env.T_USER1; -} - // If is not admin, return false else return the list of the groups to which user isAdmin async function isAdminUser(username) { try { From 021866af30dfd54773a3bd0d47f28a7e7a111161 Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 29 Apr 2020 14:27:20 +0300 Subject: [PATCH 04/15] Refactor isauthenticated middleware to use JTWs to verify authentication ststus --- server/src/middleware/is-authenticated.js | 30 ++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/server/src/middleware/is-authenticated.js b/server/src/middleware/is-authenticated.js index e7675f065a..e18e0d0238 100644 --- a/server/src/middleware/is-authenticated.js +++ b/server/src/middleware/is-authenticated.js @@ -1,13 +1,21 @@ -const log = require('tangy-log').log - +const log = require('tangy-log').log; +const { verifyJWT, decodeJWT } = require('../auth-utils'); module.exports = function (req, res, next) { - // Uncomment next two lines when you want to turn off authentication during development. - // req.user = {}; req.user.name = 'user1'; - // return next(); - if (req.isAuthenticated()) { - return next(); + const token = req.headers.authorization; + const errorMessage = `Permission denied at ${req.url}`; + if (token && verifyJWT(token)) { + const { username } = decodeJWT(token); + if (!username) { + log.warn(errorMessage); + res.status(401).send(errorMessage); + } else { + req.user= {} + req.user.name = username; + res.locals.username = username; + next(); + } + } else { + log.warn(errorMessage); + res.status(401).send(errorMessage); } - let errorMessage = `Permission denied at ${req.url}`; - log.warn(errorMessage) - res.status(401).send(errorMessage) -} +}; From c1c6384ac9a21c0a9179dd71726a63965d5413ce Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 29 Apr 2020 14:29:04 +0300 Subject: [PATCH 05/15] Add jsonwebtoken library to handle JWTs --- server/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/server/package.json b/server/package.json index 44182d9442..61f033e3aa 100644 --- a/server/package.json +++ b/server/package.json @@ -59,6 +59,7 @@ "flat": "^4.0.0", "fs-extra": "^4.0.3", "json2csv": "^3.11.5", + "jsonwebtoken": "^8.5.1", "junk": "^2.1.0", "lodash": "^4.17.10", "multer": "^1.4.1", From 3553535df2b773060e6b538ca9b39dfc80698b76 Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 29 Apr 2020 14:29:49 +0300 Subject: [PATCH 06/15] Add rxjs-compat libray to enable use of Observable of in Angular --- editor/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/editor/package.json b/editor/package.json index b440254886..63b0cc766e 100644 --- a/editor/package.json +++ b/editor/package.json @@ -32,12 +32,14 @@ "file-list-component": "1.3.2", "hammerjs": "^2.0.8", "just-snake-case": "^1.1.0", + "jwt-decode": "^2.2.0", "material-design-icons-iconfont": "^3.0.3", "moment": "^2.23.0", "qrcode-generator-es6": "^1.1.4", "rangen": "^1.0.0", "redux": "^4.0.1", "rxjs": "~6.5.4", + "rxjs-compat": "^6.5.5", "tangy-form": "4.11.5", "tangy-form-editor": "6.13.5", "translation-web-component": "0.0.3", From 8051bdcd14009692152f8f7b25db72b55b198a53 Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 29 Apr 2020 14:30:44 +0300 Subject: [PATCH 07/15] Update appcomponnet html to correspond to auth status --- editor/src/app/app.component.html | 203 ++++++++++++++---------------- 1 file changed, 96 insertions(+), 107 deletions(-) diff --git a/editor/src/app/app.component.html b/editor/src/app/app.component.html index 2e9219f574..7159ca0b2e 100644 --- a/editor/src/app/app.component.html +++ b/editor/src/app/app.component.html @@ -1,129 +1,131 @@
- + - - + + create - {{'Author'|translate}} + {{'Author'|translate}} - - + + list_alt - {{'Data'|translate}} + {{'Data'|translate}} - + settings_applications - {{'Configure'|translate}} + {{'Configure'|translate}} - + phonelink_ring - {{'Deploy'|translate}} + {{'Deploy'|translate}} - + group_work {{'Groups'|translate}} - + group {{'Manage Users'|translate}} @@ -132,10 +134,7 @@
- + group_work {{'Groups'|translate}} @@ -143,35 +142,25 @@ - + routerLink="/groups/{{menuService.groupId}}/support"> help {{'Help'|translate}} - - + + help {{'Help'|translate}} - - - + exit_to_app - {{'Logout'|translate}} + {{'Logout'|translate}} @@ -184,7 +173,7 @@ -
+
{{menuService.title}}
@@ -195,4 +184,4 @@ -
+
\ No newline at end of file From c94dc1b49780c05d22e0bbb439690eb60d20c44e Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 29 Apr 2020 14:32:51 +0300 Subject: [PATCH 08/15] Add Http interceptors to work with JWTs --- .../interceptors/auth-token-interceptor.ts | 28 +++++++++++++++++++ .../src/app/core/http/interceptors/index.ts | 10 +++++++ 2 files changed, 38 insertions(+) create mode 100644 editor/src/app/core/http/interceptors/auth-token-interceptor.ts create mode 100644 editor/src/app/core/http/interceptors/index.ts diff --git a/editor/src/app/core/http/interceptors/auth-token-interceptor.ts b/editor/src/app/core/http/interceptors/auth-token-interceptor.ts new file mode 100644 index 0000000000..bda019add1 --- /dev/null +++ b/editor/src/app/core/http/interceptors/auth-token-interceptor.ts @@ -0,0 +1,28 @@ +/** + * Adapted from https://stackoverflow.com/questions/45735655/how-do-i-set-the-baseurl-for-angular-httpclient + */ +import {Injectable} from '@angular/core'; +import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpHeaders, HttpErrorResponse} from '@angular/common/http'; +import {Observable} from 'rxjs/Observable'; +import { AuthenticationService } from '../../auth/_services/authentication.service'; +import { tap } from 'rxjs/operators'; + +@Injectable() +export class APIInterceptor implements HttpInterceptor { + constructor(private authenticationService: AuthenticationService) {} + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const token = localStorage.getItem('token'); + const apiReq = token + ? req.clone({ url: `${req.url}` , headers: req.headers.set('Authorization', token) }) + : req.clone({ url: `${req.url}` }); + return next.handle(apiReq).pipe( tap(() => {}, + (err: any) => { + if (err instanceof HttpErrorResponse) { + if (err.status !== 401) { + return; + } + this.authenticationService.logout(); + } + })); + } +} diff --git a/editor/src/app/core/http/interceptors/index.ts b/editor/src/app/core/http/interceptors/index.ts new file mode 100644 index 0000000000..1952149958 --- /dev/null +++ b/editor/src/app/core/http/interceptors/index.ts @@ -0,0 +1,10 @@ +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { APIInterceptor } from './auth-token-interceptor'; + +/** Http interceptor providers in outside-in order */ +/** + * Gotten from https://angular.io/guide/http#advanced-usage + */ +export const httpInterceptorProviders = [ + { provide: HTTP_INTERCEPTORS, useClass: APIInterceptor, multi: true }, +]; From 17e5e033316d9c8aff9a9875b245c56ea8792626 Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 29 Apr 2020 14:33:37 +0300 Subject: [PATCH 09/15] Add http interceptors to module --- editor/src/app/app.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/editor/src/app/app.module.ts b/editor/src/app/app.module.ts index 20330d3597..f68b134997 100644 --- a/editor/src/app/app.module.ts +++ b/editor/src/app/app.module.ts @@ -24,6 +24,7 @@ import { MatButtonModule, MatIconModule, MatCheckboxModule, MatCardModule, MatMenuModule, MatSidenavModule, MatToolbarModule, MatDividerModule } from '@angular/material'; +import { httpInterceptorProviders } from './core/http/interceptors'; export function HttpLoaderFactory(httpClient: HttpClient) { @@ -64,7 +65,8 @@ export function HttpLoaderFactory(httpClient: HttpClient) { }), BrowserAnimationsModule ], - providers: [TangyErrorHandler, WindowRef, { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, multi: true }], + providers: [httpInterceptorProviders, TangyErrorHandler, + WindowRef, { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, multi: true }], bootstrap: [AppComponent] }) export class AppModule { } From f4728bbf53cf9f1c6bf94ae01b67476d061d4239 Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 29 Apr 2020 14:34:29 +0300 Subject: [PATCH 10/15] Rework authentication to use JWTs on client side --- editor/src/app/app.component.ts | 47 ++++++++----------- .../auth/_components/login/login.component.ts | 20 +++----- .../auth/_services/authentication.service.ts | 32 ++++++++----- 3 files changed, 46 insertions(+), 53 deletions(-) diff --git a/editor/src/app/app.component.ts b/editor/src/app/app.component.ts index d5f72288f6..0008f2b593 100644 --- a/editor/src/app/app.component.ts +++ b/editor/src/app/app.component.ts @@ -10,6 +10,7 @@ import { MediaMatcher } from '@angular/cdk/layout'; import { HttpClient } from '@angular/common/http'; import { MatSidenav } from '@angular/material'; import { UserService } from './core/auth/_services/user.service'; +import { AppConfigService } from './shared/_services/app-config.service'; @Component({ @@ -43,7 +44,7 @@ export class AppComponent implements OnInit, OnDestroy { translate: TranslateService, changeDetectorRef: ChangeDetectorRef, media: MediaMatcher, - private http: HttpClient + private appConfigService: AppConfigService ) { translate.setDefaultLang('translation'); translate.use('translation'); @@ -59,37 +60,29 @@ export class AppComponent implements OnInit, OnDestroy { async logout() { await this.authenticationService.logout(); - this.router.navigate(['login']); - this.window.location.reload() + this.loggedIn = false; + this.isAdminUser = false; + this.canManageSitewideUsers = false; + this.user_id = null; + this.router.navigate(['/login']); } async ngOnInit() { - // Ensure user is logged in every 60 seconds. - await this.ensureLoggedIn(); - this.isAdminUser = await this.userService.isCurrentUserAdmin() - setInterval(() => this.ensureLoggedIn(), 60 * 1000); this.authenticationService.currentUserLoggedIn$.subscribe(async isLoggedIn => { - this.isAdminUser = await this.userService.isCurrentUserAdmin() - this.loggedIn = isLoggedIn; - this.user_id = localStorage.getItem('user_id'); - this.canManageSitewideUsers = await this.http.get('/user/permission/can-manage-sitewide-users').toPromise() - if (!isLoggedIn) { this.router.navigate(['login']); } + if (isLoggedIn) { + this.loggedIn = isLoggedIn; + this.isAdminUser = await this.userService.isCurrentUserAdmin(); + this.user_id = localStorage.getItem('user_id'); + this.canManageSitewideUsers = await this.userService.canManageSitewideUsers(); + } else { + this.loggedIn = false; + this.isAdminUser = false; + this.canManageSitewideUsers = false; + this.user_id = null; + this.router.navigate(['/login']); + } }); - fetch('assets/translation.json') - .then(response => response.json()) - .then(json => { - this.window.translation = json - }) - } - - async ensureLoggedIn() { - this.loggedIn = await this.authenticationService.isLoggedIn(); - if (this.loggedIn && await this.authenticationService.validateSession() === false) { - console.log('found invalid session'); - this.isAdminUser = false - this.canManageSitewideUsers = false - this.logout(); - } + this.window.translation = await this.appConfigService.getTranslations(); } ngOnDestroy(): void { this.mobileQuery.removeListener(this._mobileQueryListener); diff --git a/editor/src/app/core/auth/_components/login/login.component.ts b/editor/src/app/core/auth/_components/login/login.component.ts index 15631f9bb5..91b15e5188 100644 --- a/editor/src/app/core/auth/_components/login/login.component.ts +++ b/editor/src/app/core/auth/_components/login/login.component.ts @@ -17,30 +17,24 @@ export class LoginComponent implements OnInit { private authenticationService: AuthenticationService, private route: ActivatedRoute, private router: Router, - private windowRef: WindowRef ) { } - ngOnInit() { + async ngOnInit() { this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || 'projects'; + if (await this.authenticationService.isLoggedIn()) { + this.router.navigate([this.returnUrl]); + } } - async loginUser() { try { - const data = await this.authenticationService.login(this.user.username, this.user.password); - if (data) { - this.router.navigate(['projects']); - setTimeout(() => { - if (this.windowRef.nativeWindow.location.hash === '#/login') { - console.log('force navigation') - this.windowRef.nativeWindow.location.hash = '' - } - }, 3000) - + if (await this.authenticationService.login(this.user.username, this.user.password)) { + this.router.navigate(['/projects']); } else { this.errorMessage = _TRANSLATE('Login Unsuccesful'); } } catch (error) { this.errorMessage = _TRANSLATE('Login Unsuccesful'); + console.error(error); } } diff --git a/editor/src/app/core/auth/_services/authentication.service.ts b/editor/src/app/core/auth/_services/authentication.service.ts index d49dae6d98..67909765ec 100644 --- a/editor/src/app/core/auth/_services/authentication.service.ts +++ b/editor/src/app/core/auth/_services/authentication.service.ts @@ -2,27 +2,33 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { UserService } from './user.service'; import { Subject } from 'rxjs'; +import jwt_decode from 'jwt-decode'; @Injectable() export class AuthenticationService { public currentUserLoggedIn$: any; private _currentUserLoggedIn: boolean; - constructor(private userService: UserService, private httpClient: HttpClient) { + constructor(private userService: UserService, private http: HttpClient) { this.currentUserLoggedIn$ = new Subject(); } async login(username: string, password: string) { - const result = this.httpClient.post('/login', { username, password }); - result.subscribe(async (data: any) => { - if (data.statusCode === 200) { - await localStorage.setItem('token', data.name); - await localStorage.setItem('user_id', data.name); - await localStorage.setItem('password', data.statusMessage); - this._currentUserLoggedIn = true; - this.currentUserLoggedIn$.next(this._currentUserLoggedIn); + try { + const data = await this.http.post('/login', {username, password}, {observe: 'response'}).toPromise(); + if (data.status === 200) { + const token = data.body['data']['token']; + const jwtData = jwt_decode(token); + localStorage.setItem('token', token); + localStorage.setItem('user_id', jwtData.username); + return true; + } else { + return false; } - }); - return await result.toPromise().then((data: any) => data.statusCode === 200); + } catch (error) { + console.error(error); + localStorage.removeItem('token'); + localStorage.removeItem('user_id'); + return false; + } } - async isLoggedIn():Promise { this._currentUserLoggedIn = false; this._currentUserLoggedIn = !!localStorage.getItem('user_id'); @@ -31,7 +37,7 @@ export class AuthenticationService { } async validateSession():Promise { - const status = await this.httpClient.get(`/login/validate/${localStorage.getItem('user_id')}`).toPromise() + const status = await this.http.get(`/login/validate/${localStorage.getItem('user_id')}`).toPromise() return status['valid'] } From e4a4a7c1d3ee9a34e8567d1e4256df5cca5c905b Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 29 Apr 2020 14:36:02 +0300 Subject: [PATCH 11/15] Extract method for getting translations to service --- editor/src/app/shared/_services/app-config.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/editor/src/app/shared/_services/app-config.service.ts b/editor/src/app/shared/_services/app-config.service.ts index 7d2bd1e110..bec0b77296 100755 --- a/editor/src/app/shared/_services/app-config.service.ts +++ b/editor/src/app/shared/_services/app-config.service.ts @@ -21,4 +21,11 @@ export class AppConfigService { const result:any = await this.getAppConfig(groupName); return result.homeUrl; } + async getTranslations() { + try { + return await this.http.get('assets/translation.json').toPromise(); + } catch (error) { + console.error(error); + } + } } From 487e566d49a596443fe1fd9ace5a91cb32ed117b Mon Sep 17 00:00:00 2001 From: Evans D Date: Mon, 4 May 2020 15:17:06 +0300 Subject: [PATCH 12/15] Allow user to extend session before auto expiry --- editor/src/app/app.component.ts | 23 ++++++++++++++++++- .../auth/_services/authentication.service.ts | 18 +++++++++++++++ server/src/auth-utils.js | 6 ++--- server/src/auth.js | 7 ++++++ server/src/express-app.js | 4 +++- server/src/middleware/is-authenticated.js | 3 +-- 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/editor/src/app/app.component.ts b/editor/src/app/app.component.ts index 0008f2b593..c79192d2f0 100644 --- a/editor/src/app/app.component.ts +++ b/editor/src/app/app.component.ts @@ -11,6 +11,7 @@ import { HttpClient } from '@angular/common/http'; import { MatSidenav } from '@angular/material'; import { UserService } from './core/auth/_services/user.service'; import { AppConfigService } from './shared/_services/app-config.service'; +import { _TRANSLATE } from './shared/_services/translation-marker'; @Component({ @@ -28,7 +29,8 @@ export class AppComponent implements OnInit, OnDestroy { history: string[] = []; titleToUse: string; mobileQuery: MediaQueryList; - window:any + window: any; + sessionTimeoutCheckTimerID; @ViewChild('snav', {static: true}) snav: MatSidenav @@ -65,6 +67,7 @@ export class AppComponent implements OnInit, OnDestroy { this.canManageSitewideUsers = false; this.user_id = null; this.router.navigate(['/login']); + clearInterval(this.sessionTimeoutCheckTimerID); } async ngOnInit() { @@ -74,6 +77,7 @@ export class AppComponent implements OnInit, OnDestroy { this.isAdminUser = await this.userService.isCurrentUserAdmin(); this.user_id = localStorage.getItem('user_id'); this.canManageSitewideUsers = await this.userService.canManageSitewideUsers(); + this.sessionTimeoutCheck(); } else { this.loggedIn = false; this.isAdminUser = false; @@ -88,4 +92,21 @@ export class AppComponent implements OnInit, OnDestroy { this.mobileQuery.removeListener(this._mobileQueryListener); } + sessionTimeoutCheck() { + this.sessionTimeoutCheckTimerID = setInterval(async () => { + const token = localStorage.getItem('token'); + const claims = JSON.parse(atob(token.split('.')[1])); + const expiryTimeInMs = claims['exp'] * 1000; + const minutesBeforeExpiry = expiryTimeInMs - (15 * 60 * 1000); // warn 15 minutes before expiry of token + if (Date.now() >= minutesBeforeExpiry) { + const extendSession = confirm(_TRANSLATE('You are about to be logged out from Tangerine. Should we extend your session?')); + if (extendSession) { + await this.authenticationService.extendUserSession(); + } else { + await this.logout(); + } + } + }, 10 * 60 * 1000); // check every 10 minutes + } + } diff --git a/editor/src/app/core/auth/_services/authentication.service.ts b/editor/src/app/core/auth/_services/authentication.service.ts index 67909765ec..fa0abfbc26 100644 --- a/editor/src/app/core/auth/_services/authentication.service.ts +++ b/editor/src/app/core/auth/_services/authentication.service.ts @@ -49,4 +49,22 @@ export class AuthenticationService { this._currentUserLoggedIn = false; this.currentUserLoggedIn$.next(this._currentUserLoggedIn); } + + async extendUserSession() { + const username = localStorage.getItem('user_id'); + try { + const data = await this.http.post('/extendSession', {username}, {observe: 'response'}).toPromise(); + if (data.status === 200) { + const token = data.body['data']['token']; + const jwtData = jwt_decode(token); + localStorage.setItem('token', token); + localStorage.setItem('user_id', jwtData.username); + return true; + } else { + return false; + } + } catch (error) { + console.log(error); + } + } } diff --git a/server/src/auth-utils.js b/server/src/auth-utils.js index 64349f3fed..4b11b1150d 100644 --- a/server/src/auth-utils.js +++ b/server/src/auth-utils.js @@ -1,5 +1,5 @@ const jwt = require('jsonwebtoken'); -const expiresIn = process.env.JWT_TOKEN_VALIDITY || '7 days'; +const expiresIn = process.env.JWT_TOKEN_VALIDITY || '15 minutes'; const issuer = process.env.JWT_TOKEN_ISSUER || 'Tangerine Devs'; const jwtTokenSecret = process.env.JWT_TOKEN_SECRET || @@ -7,9 +7,9 @@ const jwtTokenSecret = const createLoginJWT = ({ username }) => { const signingOptions = { + expiresIn, issuer, subject: username, - expiresIn, }; return jwt.sign({ username }, jwtTokenSecret, signingOptions); }; @@ -34,6 +34,6 @@ const decodeJWT = (token) => { module.exports = { createLoginJWT, - verifyJWT, decodeJWT, + verifyJWT, }; diff --git a/server/src/auth.js b/server/src/auth.js index 7e446d5000..09c757dbe0 100644 --- a/server/src/auth.js +++ b/server/src/auth.js @@ -77,9 +77,16 @@ const hashPassword = async (password) => { } }; +const extendSession = async (req, res) => { + const {username} = req.body; + const token = createLoginJWT({ username }); + return res.status(200).send({data: { token }}); +}; + module.exports = { areCredentialsValid, doesUserExist, + extendSession, findUserByUsername, hashPassword, isSuperAdmin, diff --git a/server/src/express-app.js b/server/src/express-app.js index 24db7cc8b6..2f7b729244 100644 --- a/server/src/express-app.js +++ b/server/src/express-app.js @@ -38,7 +38,8 @@ const junk = require('junk'); const cors = require('cors') const sep = path.sep; const tangyModules = require('./modules/index.js')() -const {doesUserExist, findUserByUsername, isSuperAdmin, hashPassword, USERS_DB, login} = require('./auth') +const {doesUserExist, extendSession, findUserByUsername, isSuperAdmin, + hashPassword, USERS_DB, login} = require('./auth') log.info('heartbeat') setInterval(() => log.info('heartbeat'), 5*60*1000) @@ -93,6 +94,7 @@ var isAuthenticatedOrHasUploadToken = require('./middleware/is-authenticated-or- // Login service. app.post('/login', login); +app.post('/extendSession', isAuthenticated, extendSession); app.get('/login/validate/:userName', function (req, res) { diff --git a/server/src/middleware/is-authenticated.js b/server/src/middleware/is-authenticated.js index e18e0d0238..f9441d25e0 100644 --- a/server/src/middleware/is-authenticated.js +++ b/server/src/middleware/is-authenticated.js @@ -1,6 +1,6 @@ const log = require('tangy-log').log; const { verifyJWT, decodeJWT } = require('../auth-utils'); -module.exports = function (req, res, next) { +module.exports = function(req, res, next) { const token = req.headers.authorization; const errorMessage = `Permission denied at ${req.url}`; if (token && verifyJWT(token)) { @@ -11,7 +11,6 @@ module.exports = function (req, res, next) { } else { req.user= {} req.user.name = username; - res.locals.username = username; next(); } } else { From 4b890ec6e161a1ce7d6c7e457a8849179b976774 Mon Sep 17 00:00:00 2001 From: Evans D Date: Mon, 4 May 2020 16:01:37 +0300 Subject: [PATCH 13/15] Remove unused reference to httpclient and move up the clearinterval --- editor/src/app/app.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/editor/src/app/app.component.ts b/editor/src/app/app.component.ts index c79192d2f0..f3e8b86e36 100644 --- a/editor/src/app/app.component.ts +++ b/editor/src/app/app.component.ts @@ -7,7 +7,6 @@ import { AuthenticationService } from './core/auth/_services/authentication.serv import { RegistrationService } from './registration/services/registration.service'; import { WindowRef } from './core/window-ref.service'; import { MediaMatcher } from '@angular/cdk/layout'; -import { HttpClient } from '@angular/common/http'; import { MatSidenav } from '@angular/material'; import { UserService } from './core/auth/_services/user.service'; import { AppConfigService } from './shared/_services/app-config.service'; @@ -61,13 +60,13 @@ export class AppComponent implements OnInit, OnDestroy { } async logout() { + clearInterval(this.sessionTimeoutCheckTimerID); await this.authenticationService.logout(); this.loggedIn = false; this.isAdminUser = false; this.canManageSitewideUsers = false; this.user_id = null; this.router.navigate(['/login']); - clearInterval(this.sessionTimeoutCheckTimerID); } async ngOnInit() { From ee22a25f7bcf9a82c217952ced3862370bfc7740 Mon Sep 17 00:00:00 2001 From: Evans D Date: Wed, 6 May 2020 21:23:14 +0300 Subject: [PATCH 14/15] Generate JWT secret key using cryptoto generate 32byte key. Ensure that session validity is checked on Appcomponent init --- editor/src/app/app.component.ts | 16 +++++++--------- server/src/auth-utils.js | 8 +++----- server/src/auth.js | 4 ++-- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/editor/src/app/app.component.ts b/editor/src/app/app.component.ts index f3e8b86e36..75e4538d9e 100644 --- a/editor/src/app/app.component.ts +++ b/editor/src/app/app.component.ts @@ -20,7 +20,6 @@ import { _TRANSLATE } from './shared/_services/translation-marker'; }) export class AppComponent implements OnInit, OnDestroy { loggedIn = false; - validSession: boolean; user_id: string = localStorage.getItem('user_id'); private childValue: string; canManageSitewideUsers = false @@ -31,17 +30,17 @@ export class AppComponent implements OnInit, OnDestroy { window: any; sessionTimeoutCheckTimerID; - @ViewChild('snav', {static: true}) snav: MatSidenav + @ViewChild('snav', { static: true }) snav: MatSidenav private _mobileQueryListener: () => void; constructor( private windowRef: WindowRef, private router: Router, private _registrationService: RegistrationService, - private userService:UserService, - private menuService:MenuService, + private userService: UserService, + private menuService: MenuService, private authenticationService: AuthenticationService, - private tangyFormService:TangyFormService, + private tangyFormService: TangyFormService, translate: TranslateService, changeDetectorRef: ChangeDetectorRef, media: MediaMatcher, @@ -56,7 +55,6 @@ export class AppComponent implements OnInit, OnDestroy { this.window = this.windowRef.nativeWindow; // Tell tangyFormService which groupId to use. tangyFormService.initialize(window.location.pathname.split('/')[2]) - } async logout() { @@ -77,6 +75,8 @@ export class AppComponent implements OnInit, OnDestroy { this.user_id = localStorage.getItem('user_id'); this.canManageSitewideUsers = await this.userService.canManageSitewideUsers(); this.sessionTimeoutCheck(); + this.sessionTimeoutCheckTimerID = + setInterval(await this.sessionTimeoutCheck.bind(this), 10 * 60 * 1000); // check every 10 minutes } else { this.loggedIn = false; this.isAdminUser = false; @@ -91,8 +91,7 @@ export class AppComponent implements OnInit, OnDestroy { this.mobileQuery.removeListener(this._mobileQueryListener); } - sessionTimeoutCheck() { - this.sessionTimeoutCheckTimerID = setInterval(async () => { + async sessionTimeoutCheck() { const token = localStorage.getItem('token'); const claims = JSON.parse(atob(token.split('.')[1])); const expiryTimeInMs = claims['exp'] * 1000; @@ -105,7 +104,6 @@ export class AppComponent implements OnInit, OnDestroy { await this.logout(); } } - }, 10 * 60 * 1000); // check every 10 minutes } } diff --git a/server/src/auth-utils.js b/server/src/auth-utils.js index 4b11b1150d..e303c2a0c5 100644 --- a/server/src/auth-utils.js +++ b/server/src/auth-utils.js @@ -1,9 +1,7 @@ const jwt = require('jsonwebtoken'); -const expiresIn = process.env.JWT_TOKEN_VALIDITY || '15 minutes'; -const issuer = process.env.JWT_TOKEN_ISSUER || 'Tangerine Devs'; -const jwtTokenSecret = - process.env.JWT_TOKEN_SECRET || - 'tstststsgsvdsjheytetew4567823e76tfvbendi876tfvewbndfitw562yhwbdnfjytf2wvedtrrfwvbhytfwhystrwfgtwfgwhygwvbtwtgvwftwghbb'; +const expiresIn ='5 minutes'; +const issuer = 'Tangerine'; +const jwtTokenSecret = require('crypto').randomBytes(256).toString('base64'); const createLoginJWT = ({ username }) => { const signingOptions = { diff --git a/server/src/auth.js b/server/src/auth.js index 09c757dbe0..95cb3468a7 100644 --- a/server/src/auth.js +++ b/server/src/auth.js @@ -29,8 +29,8 @@ const areCredentialsValid = async (username, password) => { try { let isValid = false; if ( - username == process.env.T_USER1 && - password == process.env.T_USER1_PASSWORD + username === process.env.T_USER1 && + password === process.env.T_USER1_PASSWORD ) { isValid = true; return isValid; From bfeb2addb00a3fdffc15b3bfcc799185ded6728d Mon Sep 17 00:00:00 2001 From: Evans D Date: Fri, 8 May 2020 14:48:14 +0300 Subject: [PATCH 15/15] Update token expiry to 1hr --- server/src/auth-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/auth-utils.js b/server/src/auth-utils.js index e303c2a0c5..b12bd847f7 100644 --- a/server/src/auth-utils.js +++ b/server/src/auth-utils.js @@ -1,5 +1,5 @@ const jwt = require('jsonwebtoken'); -const expiresIn ='5 minutes'; +const expiresIn ='1h'; const issuer = 'Tangerine'; const jwtTokenSecret = require('crypto').randomBytes(256).toString('base64');