From f42d78b2fb915b8aec891b5f57883f4947ee336a Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Mon, 14 Nov 2022 21:20:58 +0100 Subject: [PATCH 01/14] oauth returns access token --- package.json | 1 + packages/api/env/auth/.env | 4 ++++ packages/api/package.json | 1 + packages/api/src/controllers/auth.js | 17 +++++++++++++++ packages/api/src/controllers/config.js | 1 + packages/api/src/main.js | 2 ++ packages/web/src/App.svelte | 29 ++++++++++++++++++++++++++ packages/web/src/main.ts | 16 ++++++++++++++ 8 files changed, 71 insertions(+) create mode 100644 packages/api/env/auth/.env create mode 100644 packages/api/src/controllers/auth.js diff --git a/package.json b/package.json index 161e8611..75dc2668 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "start:app:debug:ssh": "cd app && cross-env DEBUG=ssh yarn start", "start:api:portal": "yarn workspace dbgate-api start:portal", "start:api:singledb": "yarn workspace dbgate-api start:singledb", + "start:api:auth": "yarn workspace dbgate-api start:auth", "start:web": "yarn workspace dbgate-web dev", "start:sqltree": "yarn workspace dbgate-sqltree start", "start:tools": "yarn workspace dbgate-tools start", diff --git a/packages/api/env/auth/.env b/packages/api/env/auth/.env new file mode 100644 index 00000000..1737c73e --- /dev/null +++ b/packages/api/env/auth/.env @@ -0,0 +1,4 @@ +DEVMODE=1 +OAUTH=http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect +OAUTH_CLIENT_ID=dbgate +OAUTH_CLIENT_SECRET=ffd5634b-b60a-4c3a-bbec-b4144c73ea2a \ No newline at end of file diff --git a/packages/api/package.json b/packages/api/package.json index 8190e580..35822cea 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -57,6 +57,7 @@ "start": "env-cmd node src/index.js --listen-api", "start:portal": "env-cmd -f env/portal/.env node src/index.js --listen-api", "start:singledb": "env-cmd -f env/singledb/.env node src/index.js --listen-api", + "start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api", "start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api", "start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api", "ts": "tsc", diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js new file mode 100644 index 00000000..27a1c3e1 --- /dev/null +++ b/packages/api/src/controllers/auth.js @@ -0,0 +1,17 @@ +const axios = require('axios'); + +module.exports = { + oauthToken_meta: true, + async oauthToken(params) { + const { redirectUri, code } = params; + + const resp = await axios.default.post( + `${process.env.OAUTH}/token`, + `grant_type=authorization_code&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent( + redirectUri + )}&client_id=${process.env.OAUTH_CLIENT_ID}&client_secret=${process.env.OAUTH_CLIENT_SECRET}` + ); + + return resp.data; + }, +}; diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index 34e3e3c8..a75fd12b 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -40,6 +40,7 @@ module.exports = { isDocker: platformInfo.isDocker, permissions, login, + oauth: process.env.OAUTH, ...currentVersion, }; }, diff --git a/packages/api/src/main.js b/packages/api/src/main.js index eec93c02..9ce31bb6 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -20,6 +20,7 @@ const jsldata = require('./controllers/jsldata'); const config = require('./controllers/config'); const archive = require('./controllers/archive'); const apps = require('./controllers/apps'); +const auth = require('./controllers/auth'); const uploads = require('./controllers/uploads'); const plugins = require('./controllers/plugins'); const files = require('./controllers/files'); @@ -157,6 +158,7 @@ function useAllControllers(app, electron) { useController(app, electron, '/scheduler', scheduler); useController(app, electron, '/query-history', queryHistory); useController(app, electron, '/apps', apps); + useController(app, electron, '/auth', auth); } function setElectronSender(electronSender) { diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 6938737e..c3000f83 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -24,6 +24,34 @@ let loadedApi = false; let loadedPlugins = false; + async function handleAuth(config) { + if (config.oauth) { + const params = new URLSearchParams(location.search); + const sentCode = params.get('code'); + const sentState = params.get('state'); + if ( + sentCode && + sentState && + sentState.startsWith('dbg-oauth:') && + sentState == sessionStorage.getItem('oauthState') + ) { + const accessToken = await apiCall('auth/oauth-token', { + code: sentCode, + redirectUri: location.origin, + }); + console.log('TOKEN', accessToken); + } else { + const state = `dbg-oauth:${Math.random().toString().substr(2)}`; + sessionStorage.setItem('oauthState', state); + location.replace( + `${config.oauth}/auth?client_id=dbgate&response_type=code&redirect_uri=${encodeURIComponent( + location.origin + )}&state=${encodeURIComponent(state)}` + ); + } + } + } + async function loadApi() { // if (shouldWaitForElectronInitialize()) { // setTimeout(loadApi, 100); @@ -36,6 +64,7 @@ const connections = await apiCall('connections/list'); const settings = await getSettings(); const config = await getConfig(); + handleAuth(config); const apps = await getUsedApps(); loadedApi = settings && connections && config && apps; diff --git a/packages/web/src/main.ts b/packages/web/src/main.ts index 2a72600c..bfb1df04 100644 --- a/packages/web/src/main.ts +++ b/packages/web/src/main.ts @@ -4,6 +4,22 @@ import './utility/changeCurrentDbByTab'; import './commands/stdCommands'; import localStorageGarbageCollector from './utility/localStorageGarbageCollector'; +const params = new URLSearchParams(location.search); +console.log('CODE', params.get('code')); +// console.log( +// `http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect/auth?client_id=dbgate&response_type=code&redirect_uri=${encodeURIComponent( +// 'http://localhost:5001/oauth-redirect' +// )}&state=1234` +// ); + +console.log(location); + +// location.replace( +// `http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect/auth?client_id=dbgate&response_type=code&redirect_uri=${encodeURIComponent( +// 'http://localhost:5001/' +// )}&state=1234` +// ); + localStorageGarbageCollector(); const app = new App({ From 37a87837511546ea0d6b68b7977d13df75633fbe Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 17 Nov 2022 12:43:38 +0100 Subject: [PATCH 02/14] oauth working, but cycling sometimes --- packages/api/env/auth/.env | 3 +- packages/api/package.json | 1 + packages/api/src/controllers/auth.js | 52 +++++++++++++++++++++++++- packages/api/src/controllers/config.js | 2 +- packages/api/src/main.js | 4 ++ packages/web/src/App.svelte | 34 ++--------------- packages/web/src/clientAuth.ts | 46 +++++++++++++++++++++++ packages/web/src/main.ts | 16 -------- packages/web/src/utility/api.ts | 21 +++++++++-- packages/web/src/utility/resolveApi.ts | 7 +++- 10 files changed, 132 insertions(+), 54 deletions(-) create mode 100644 packages/web/src/clientAuth.ts diff --git a/packages/api/env/auth/.env b/packages/api/env/auth/.env index 1737c73e..9d4d6062 100644 --- a/packages/api/env/auth/.env +++ b/packages/api/env/auth/.env @@ -1,4 +1,5 @@ DEVMODE=1 -OAUTH=http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect +OAUTH_AUTH=http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect/auth +OAUTH_TOKEN=http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect/token OAUTH_CLIENT_ID=dbgate OAUTH_CLIENT_SECRET=ffd5634b-b60a-4c3a-bbec-b4144c73ea2a \ No newline at end of file diff --git a/packages/api/package.json b/packages/api/package.json index 35822cea..e3dc03f0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -42,6 +42,7 @@ "is-electron": "^2.2.1", "js-yaml": "^4.1.0", "json-stable-stringify": "^1.0.1", + "jsonwebtoken": "^8.5.1", "line-reader": "^0.4.0", "lodash": "^4.17.21", "ncp": "^2.0.0", diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index 27a1c3e1..7235ce12 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -1,4 +1,39 @@ const axios = require('axios'); +const jwt = require('jsonwebtoken'); +const getExpressPath = require('../utility/getExpressPath'); +const uuidv1 = require('uuid/v1'); + +const tokenSecret = uuidv1(); + +function shouldAuthorizeApi() { + return !!process.env.OAUTH_AUTH; +} + +function authMiddleware(req, res, next) { + const SKIP_AUTH_PATHS = ['/config/get', '/auth/oauth-token', '/stream']; + + if (!shouldAuthorizeApi()) { + return next(); + } + if (SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x))) { + return next(); + } + const authHeader = req.headers.authorization; + if (!authHeader) { + return res.send(401, 'missing authorization header'); + } + const token = authHeader.split(' ')[1]; + try { + const decoded = jwt.verify(token, tokenSecret); + req.user = decoded; + return next(); + } catch (err) { + console.log('&&&&&&&&&&&&&&&&&&&&&& IUNVALID TOKEN'); + console.log(token); + console.log(err); + return res.sendStatus(401).send('Invalid Token'); + } +} module.exports = { oauthToken_meta: true, @@ -6,12 +41,25 @@ module.exports = { const { redirectUri, code } = params; const resp = await axios.default.post( - `${process.env.OAUTH}/token`, + `${process.env.OAUTH_TOKEN}`, `grant_type=authorization_code&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent( redirectUri )}&client_id=${process.env.OAUTH_CLIENT_ID}&client_secret=${process.env.OAUTH_CLIENT_SECRET}` ); - return resp.data; + const { access_token, refresh_token } = resp.data; + + const payload = jwt.decode(access_token); + + if (access_token) { + return { + accessToken: jwt.sign({ user: 'oauth' }, tokenSecret, { expiresIn: '1m' }), + }; + } + + return { error: 'Token not found' }; }, + + authMiddleware, + shouldAuthorizeApi, }; diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index a75fd12b..8b59609d 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -40,7 +40,7 @@ module.exports = { isDocker: platformInfo.isDocker, permissions, login, - oauth: process.env.OAUTH, + oauth: process.env.OAUTH_AUTH, ...currentVersion, }; }, diff --git a/packages/api/src/main.js b/packages/api/src/main.js index 9ce31bb6..a8967044 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -54,6 +54,10 @@ function start() { app.use(cors()); + if (auth.shouldAuthorizeApi()) { + app.use(auth.authMiddleware); + } + app.get(getExpressPath('/stream'), async function (req, res) { res.set({ 'Cache-Control': 'no-cache', diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index c3000f83..ca59767c 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -20,38 +20,11 @@ import getElectron from './utility/getElectron'; import AppStartInfo from './widgets/AppStartInfo.svelte'; import SettingsListener from './utility/SettingsListener.svelte'; + import { handleAuthOnStartup } from './clientAuth'; let loadedApi = false; let loadedPlugins = false; - async function handleAuth(config) { - if (config.oauth) { - const params = new URLSearchParams(location.search); - const sentCode = params.get('code'); - const sentState = params.get('state'); - if ( - sentCode && - sentState && - sentState.startsWith('dbg-oauth:') && - sentState == sessionStorage.getItem('oauthState') - ) { - const accessToken = await apiCall('auth/oauth-token', { - code: sentCode, - redirectUri: location.origin, - }); - console.log('TOKEN', accessToken); - } else { - const state = `dbg-oauth:${Math.random().toString().substr(2)}`; - sessionStorage.setItem('oauthState', state); - location.replace( - `${config.oauth}/auth?client_id=dbgate&response_type=code&redirect_uri=${encodeURIComponent( - location.origin - )}&state=${encodeURIComponent(state)}` - ); - } - } - } - async function loadApi() { // if (shouldWaitForElectronInitialize()) { // setTimeout(loadApi, 100); @@ -61,10 +34,11 @@ try { // console.log('************** LOADING API'); + const config = await getConfig(); + await handleAuthOnStartup(config); + const connections = await apiCall('connections/list'); const settings = await getSettings(); - const config = await getConfig(); - handleAuth(config); const apps = await getUsedApps(); loadedApi = settings && connections && config && apps; diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts new file mode 100644 index 00000000..6024006a --- /dev/null +++ b/packages/web/src/clientAuth.ts @@ -0,0 +1,46 @@ +import { apiCall } from './utility/api'; +import { getConfig } from './utility/metadataLoaders'; + +export async function handleAuthOnStartup(config) { + console.log('********************* handleAuthOnStartup'); + if (config.oauth) { + const params = new URLSearchParams(location.search); + const sentCode = params.get('code'); + const sentState = params.get('state'); + + if ( + sentCode && + sentState && + sentState.startsWith('dbg-oauth:') && + sentState == sessionStorage.getItem('oauthState') + ) { + const authResp = await apiCall('auth/oauth-token', { + code: sentCode, + redirectUri: location.origin, + }); + const { accessToken } = authResp; + console.log('Got new access token:', accessToken); + localStorage.setItem('accessToken', accessToken); + location.replace('/'); + } else { + if (localStorage.getItem('accessToken')) { + return; + } + + redirectToLogin(config); + } + } +} + +export async function redirectToLogin(config = null) { + if (!config) config = await getConfig(); + + const state = `dbg-oauth:${Math.random().toString().substr(2)}`; + sessionStorage.setItem('oauthState', state); + console.log('Redirecting to OAUTH provider'); + location.replace( + `${config.oauth}?client_id=dbgate&response_type=code&redirect_uri=${encodeURIComponent( + location.origin + )}&state=${encodeURIComponent(state)}` + ); +} diff --git a/packages/web/src/main.ts b/packages/web/src/main.ts index bfb1df04..2a72600c 100644 --- a/packages/web/src/main.ts +++ b/packages/web/src/main.ts @@ -4,22 +4,6 @@ import './utility/changeCurrentDbByTab'; import './commands/stdCommands'; import localStorageGarbageCollector from './utility/localStorageGarbageCollector'; -const params = new URLSearchParams(location.search); -console.log('CODE', params.get('code')); -// console.log( -// `http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect/auth?client_id=dbgate&response_type=code&redirect_uri=${encodeURIComponent( -// 'http://localhost:5001/oauth-redirect' -// )}&state=1234` -// ); - -console.log(location); - -// location.replace( -// `http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect/auth?client_id=dbgate&response_type=code&redirect_uri=${encodeURIComponent( -// 'http://localhost:5001/' -// )}&state=1234` -// ); - localStorageGarbageCollector(); const app = new App({ diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index c4f6694b..54edbeaf 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -4,10 +4,16 @@ import { writable } from 'svelte/store'; import getElectron from './getElectron'; // import socket from './socket'; import { showSnackbarError } from '../utility/snackbar'; +import { redirectToLogin } from '../clientAuth'; let eventSource; let apiLogging = false; // let cacheCleanerRegistered; +// let apiDisabled = false; + +// export function disableApi() { +// apiDisabled = true; +// } function wantEventSource() { if (!eventSource) { @@ -17,9 +23,9 @@ function wantEventSource() { } function processApiResponse(route, args, resp) { - if (apiLogging) { - console.log('<<< API RESPONSE', route, args, resp); - } + // if (apiLogging) { + // console.log('<<< API RESPONSE', route, args, resp); + // } if (resp?.apiErrorMessage) { showSnackbarError('API error:' + resp?.apiErrorMessage); @@ -35,6 +41,10 @@ export async function apiCall(route: string, args: {} = undefined) { if (apiLogging) { console.log('>>> API CALL', route, args); } + if (apiDisabled) { + console.log('Error, API disabled!!'); + return null; + } const electron = getElectron(); if (electron) { @@ -51,6 +61,11 @@ export async function apiCall(route: string, args: {} = undefined) { body: JSON.stringify(args), }); + if (resp.status == 401) { + // unauthorized + redirectToLogin(); + } + const json = await resp.json(); return processApiResponse(route, args, json); } diff --git a/packages/web/src/utility/resolveApi.ts b/packages/web/src/utility/resolveApi.ts index e15506e6..f2c00b55 100644 --- a/packages/web/src/utility/resolveApi.ts +++ b/packages/web/src/utility/resolveApi.ts @@ -15,5 +15,10 @@ export default function resolveApi() { export function resolveApiHeaders() { const electron = getElectron(); - return {}; + const res = {}; + const accessToken = localStorage.getItem('accessToken'); + if (accessToken) { + res['Authorization'] = `Bearer ${accessToken}`; + } + return res; } From 576fc2062c4daaf7c45a3853e4fc5daae543a8db Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 17 Nov 2022 19:26:39 +0100 Subject: [PATCH 03/14] fix --- packages/web/src/utility/api.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index 54edbeaf..25b66e04 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -41,10 +41,6 @@ export async function apiCall(route: string, args: {} = undefined) { if (apiLogging) { console.log('>>> API CALL', route, args); } - if (apiDisabled) { - console.log('Error, API disabled!!'); - return null; - } const electron = getElectron(); if (electron) { From 94a91d5fed0a3e2e8fdd17513b72c8767e325633 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 17 Nov 2022 19:55:01 +0100 Subject: [PATCH 04/14] better oauth handle --- packages/api/src/controllers/auth.js | 17 ++++++--- packages/web/src/App.svelte | 8 +++-- packages/web/src/clientAuth.ts | 52 +++++++++++++++++----------- packages/web/src/main.ts | 1 + packages/web/src/utility/api.ts | 10 ++++-- 5 files changed, 58 insertions(+), 30 deletions(-) diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index 7235ce12..8d91da83 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -9,6 +9,16 @@ function shouldAuthorizeApi() { return !!process.env.OAUTH_AUTH; } +function unauthorizedResponse(req, res, text) { + // if (req.path == getExpressPath('/config/get-settings')) { + // return res.json({}); + // } + // if (req.path == getExpressPath('/connections/list')) { + // return res.json([]); + // } + return res.sendStatus(401).send(text); +} + function authMiddleware(req, res, next) { const SKIP_AUTH_PATHS = ['/config/get', '/auth/oauth-token', '/stream']; @@ -20,7 +30,7 @@ function authMiddleware(req, res, next) { } const authHeader = req.headers.authorization; if (!authHeader) { - return res.send(401, 'missing authorization header'); + return unauthorizedResponse(req, res, 'missing authorization header'); } const token = authHeader.split(' ')[1]; try { @@ -28,10 +38,7 @@ function authMiddleware(req, res, next) { req.user = decoded; return next(); } catch (err) { - console.log('&&&&&&&&&&&&&&&&&&&&&& IUNVALID TOKEN'); - console.log(token); - console.log(err); - return res.sendStatus(401).send('Invalid Token'); + return unauthorizedResponse(req, res, 'invalid token'); } } diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index ca59767c..e37018a1 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -20,12 +20,16 @@ import getElectron from './utility/getElectron'; import AppStartInfo from './widgets/AppStartInfo.svelte'; import SettingsListener from './utility/SettingsListener.svelte'; - import { handleAuthOnStartup } from './clientAuth'; + import { handleAuthOnStartup, handleOauthCallback } from './clientAuth'; let loadedApi = false; let loadedPlugins = false; + const isOauthCallback = handleOauthCallback(); async function loadApi() { + if (isOauthCallback) { + return; + } // if (shouldWaitForElectronInitialize()) { // setTimeout(loadApi, 100); // return; @@ -76,7 +80,7 @@ -{#if loadedApi} +{#if loadedApi && !isOauthCallback} diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts index 6024006a..ae3d6590 100644 --- a/packages/web/src/clientAuth.ts +++ b/packages/web/src/clientAuth.ts @@ -1,34 +1,44 @@ import { apiCall } from './utility/api'; import { getConfig } from './utility/metadataLoaders'; -export async function handleAuthOnStartup(config) { - console.log('********************* handleAuthOnStartup'); - if (config.oauth) { - const params = new URLSearchParams(location.search); - const sentCode = params.get('code'); - const sentState = params.get('state'); +export function handleOauthCallback() { + const params = new URLSearchParams(location.search); + const sentCode = params.get('code'); + const sentState = params.get('state'); - if ( - sentCode && - sentState && - sentState.startsWith('dbg-oauth:') && - sentState == sessionStorage.getItem('oauthState') - ) { - const authResp = await apiCall('auth/oauth-token', { - code: sentCode, - redirectUri: location.origin, - }); + if ( + sentCode && + sentState && + sentState.startsWith('dbg-oauth:') && + sentState == sessionStorage.getItem('oauthState') + ) { + sessionStorage.removeItem('oauthState'); + apiCall('auth/oauth-token', { + code: sentCode, + redirectUri: location.origin, + }).then(authResp => { const { accessToken } = authResp; console.log('Got new access token:', accessToken); localStorage.setItem('accessToken', accessToken); location.replace('/'); - } else { - if (localStorage.getItem('accessToken')) { - return; - } + }); - redirectToLogin(config); + console.log('handleOauthCallback TRUE'); + return true; + } + + console.log('handleOauthCallback FALSE'); + return false; +} + +export async function handleAuthOnStartup(config) { + console.log('********************* handleAuthOnStartup'); + if (config.oauth) { + if (localStorage.getItem('accessToken')) { + return; } + + redirectToLogin(config); } } diff --git a/packages/web/src/main.ts b/packages/web/src/main.ts index 2a72600c..81169a1b 100644 --- a/packages/web/src/main.ts +++ b/packages/web/src/main.ts @@ -3,6 +3,7 @@ import './utility/connectionsPinger'; import './utility/changeCurrentDbByTab'; import './commands/stdCommands'; import localStorageGarbageCollector from './utility/localStorageGarbageCollector'; +import { handleOauthCallback } from './clientAuth'; localStorageGarbageCollector(); diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index 25b66e04..32705f9f 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -9,7 +9,7 @@ import { redirectToLogin } from '../clientAuth'; let eventSource; let apiLogging = false; // let cacheCleanerRegistered; -// let apiDisabled = false; +let apiDisabled = false; // export function disableApi() { // apiDisabled = true; @@ -41,6 +41,10 @@ export async function apiCall(route: string, args: {} = undefined) { if (apiLogging) { console.log('>>> API CALL', route, args); } + if (apiDisabled) { + console.log('API disabled!!', route); + return; + } const electron = getElectron(); if (electron) { @@ -57,7 +61,9 @@ export async function apiCall(route: string, args: {} = undefined) { body: JSON.stringify(args), }); - if (resp.status == 401) { + if (resp.status == 401 && !apiDisabled) { + apiDisabled = true; + console.log('Disabling API', route); // unauthorized redirectToLogin(); } From 07b2a3e923d67bab428fdec0a9a60dd7c7c8ca53 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 17 Nov 2022 20:09:27 +0100 Subject: [PATCH 05/14] oauth disabling API --- packages/web/src/App.svelte | 6 +----- packages/web/src/clientAuth.ts | 24 ++++++++++++------------ packages/web/src/main.ts | 12 ++++++++---- packages/web/src/utility/api.ts | 15 ++++++++++----- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index e37018a1..c8fd1ad0 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -24,12 +24,8 @@ let loadedApi = false; let loadedPlugins = false; - const isOauthCallback = handleOauthCallback(); async function loadApi() { - if (isOauthCallback) { - return; - } // if (shouldWaitForElectronInitialize()) { // setTimeout(loadApi, 100); // return; @@ -80,7 +76,7 @@ -{#if loadedApi && !isOauthCallback} +{#if loadedApi} diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts index ae3d6590..71c1b922 100644 --- a/packages/web/src/clientAuth.ts +++ b/packages/web/src/clientAuth.ts @@ -1,38 +1,38 @@ -import { apiCall } from './utility/api'; +import { apiCall, disableApi } from './utility/api'; import { getConfig } from './utility/metadataLoaders'; -export function handleOauthCallback() { +export function isOauthCallback() { const params = new URLSearchParams(location.search); const sentCode = params.get('code'); const sentState = params.get('state'); - if ( - sentCode && - sentState && - sentState.startsWith('dbg-oauth:') && - sentState == sessionStorage.getItem('oauthState') - ) { + return ( + sentCode && sentState && sentState.startsWith('dbg-oauth:') && sentState == sessionStorage.getItem('oauthState') + ); +} + +export function handleOauthCallback() { + const params = new URLSearchParams(location.search); + const sentCode = params.get('code'); + + if (isOauthCallback()) { sessionStorage.removeItem('oauthState'); apiCall('auth/oauth-token', { code: sentCode, redirectUri: location.origin, }).then(authResp => { const { accessToken } = authResp; - console.log('Got new access token:', accessToken); localStorage.setItem('accessToken', accessToken); location.replace('/'); }); - console.log('handleOauthCallback TRUE'); return true; } - console.log('handleOauthCallback FALSE'); return false; } export async function handleAuthOnStartup(config) { - console.log('********************* handleAuthOnStartup'); if (config.oauth) { if (localStorage.getItem('accessToken')) { return; diff --git a/packages/web/src/main.ts b/packages/web/src/main.ts index 81169a1b..337d547c 100644 --- a/packages/web/src/main.ts +++ b/packages/web/src/main.ts @@ -5,12 +5,16 @@ import './commands/stdCommands'; import localStorageGarbageCollector from './utility/localStorageGarbageCollector'; import { handleOauthCallback } from './clientAuth'; +const isOauthCallback = handleOauthCallback(); + localStorageGarbageCollector(); -const app = new App({ - target: document.body, - props: {}, -}); +const app = isOauthCallback + ? null + : new App({ + target: document.body, + props: {}, + }); // const app = null; diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index 32705f9f..ee878f25 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -4,16 +4,17 @@ import { writable } from 'svelte/store'; import getElectron from './getElectron'; // import socket from './socket'; import { showSnackbarError } from '../utility/snackbar'; -import { redirectToLogin } from '../clientAuth'; +import { isOauthCallback, redirectToLogin } from '../clientAuth'; let eventSource; let apiLogging = false; // let cacheCleanerRegistered; let apiDisabled = false; +const disabledOnOauth = isOauthCallback(); -// export function disableApi() { -// apiDisabled = true; -// } +export function disableApi() { + apiDisabled = true; +} function wantEventSource() { if (!eventSource) { @@ -45,6 +46,10 @@ export async function apiCall(route: string, args: {} = undefined) { console.log('API disabled!!', route); return; } + if (disabledOnOauth && route != 'auth/oauth-token') { + console.log('API disabled because oauth callback!!', route); + return; + } const electron = getElectron(); if (electron) { @@ -62,7 +67,7 @@ export async function apiCall(route: string, args: {} = undefined) { }); if (resp.status == 401 && !apiDisabled) { - apiDisabled = true; + disableApi(); console.log('Disabling API', route); // unauthorized redirectToLogin(); From 70413b954b39cf0a0476398cb3c23bf88ab4c7cc Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Fri, 25 Nov 2022 13:36:18 +0100 Subject: [PATCH 06/14] login page --- packages/api/env/auth/.env | 4 +- packages/api/src/controllers/auth.js | 4 ++ packages/web/src/LoginPage.svelte | 97 +++++++++++++++++++++++++++ packages/web/src/NotLoggedPage.svelte | 18 +++++ packages/web/src/clientAuth.ts | 4 +- packages/web/src/main.ts | 36 ++++++++-- 6 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 packages/web/src/LoginPage.svelte create mode 100644 packages/web/src/NotLoggedPage.svelte diff --git a/packages/api/env/auth/.env b/packages/api/env/auth/.env index 9d4d6062..2f525b9b 100644 --- a/packages/api/env/auth/.env +++ b/packages/api/env/auth/.env @@ -2,4 +2,6 @@ DEVMODE=1 OAUTH_AUTH=http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect/auth OAUTH_TOKEN=http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect/token OAUTH_CLIENT_ID=dbgate -OAUTH_CLIENT_SECRET=ffd5634b-b60a-4c3a-bbec-b4144c73ea2a \ No newline at end of file +OAUTH_CLIENT_SECRET=ffd5634b-b60a-4c3a-bbec-b4144c73ea2a +OAUTH_LOGIN_FIELD=given_name +OAUTH_ALLOWED_LOGINS=Student1 \ No newline at end of file diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index 8d91da83..e6d36bb7 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -58,6 +58,10 @@ module.exports = { const payload = jwt.decode(access_token); + const login = process.env.OAUTH_LOGIN_FIELD ? payload[process.env.OAUTH_LOGIN_FIELD] : 'oauth'; + + console.log(payload); + if (access_token) { return { accessToken: jwt.sign({ user: 'oauth' }, tokenSecret, { expiresIn: '1m' }), diff --git a/packages/web/src/LoginPage.svelte b/packages/web/src/LoginPage.svelte new file mode 100644 index 00000000..705d8321 --- /dev/null +++ b/packages/web/src/LoginPage.svelte @@ -0,0 +1,97 @@ + + +
+
DbGate
+
+ +
+
Log In
+ + + + +
+ { + console.log('log in', e); + }} + /> +
+
+
+
+
+ + diff --git a/packages/web/src/NotLoggedPage.svelte b/packages/web/src/NotLoggedPage.svelte new file mode 100644 index 00000000..6c314b50 --- /dev/null +++ b/packages/web/src/NotLoggedPage.svelte @@ -0,0 +1,18 @@ + + +
Sorry, you are not authorized to run DbGate
+ + diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts index 71c1b922..5b9a304a 100644 --- a/packages/web/src/clientAuth.ts +++ b/packages/web/src/clientAuth.ts @@ -19,7 +19,7 @@ export function handleOauthCallback() { sessionStorage.removeItem('oauthState'); apiCall('auth/oauth-token', { code: sentCode, - redirectUri: location.origin, + redirectUri: location.origin + location.pathname, }).then(authResp => { const { accessToken } = authResp; localStorage.setItem('accessToken', accessToken); @@ -50,7 +50,7 @@ export async function redirectToLogin(config = null) { console.log('Redirecting to OAUTH provider'); location.replace( `${config.oauth}?client_id=dbgate&response_type=code&redirect_uri=${encodeURIComponent( - location.origin + location.origin + location.pathname )}&state=${encodeURIComponent(state)}` ); } diff --git a/packages/web/src/main.ts b/packages/web/src/main.ts index 337d547c..2bfcb474 100644 --- a/packages/web/src/main.ts +++ b/packages/web/src/main.ts @@ -4,18 +4,40 @@ import './utility/changeCurrentDbByTab'; import './commands/stdCommands'; import localStorageGarbageCollector from './utility/localStorageGarbageCollector'; import { handleOauthCallback } from './clientAuth'; +import LoginPage from './LoginPage.svelte'; +import NotLoggedPage from './NotLoggedPage.svelte'; const isOauthCallback = handleOauthCallback(); +const params = new URLSearchParams(location.search); +const page = params.get('page'); + localStorageGarbageCollector(); -const app = isOauthCallback - ? null - : new App({ - target: document.body, - props: {}, - }); +function createApp() { + if (isOauthCallback) { + return null; + } -// const app = null; + switch (page) { + case 'login': + return new LoginPage({ + target: document.body, + props: {}, + }); + case 'not-logged': + return new NotLoggedPage({ + target: document.body, + props: {}, + }); + } + + return new App({ + target: document.body, + props: {}, + }); +} + +const app = createApp(); export default app; From 5e4c28642751cfef1ce601c7df65937c549b0ee4 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Fri, 25 Nov 2022 16:15:41 +0100 Subject: [PATCH 07/14] ignore auth .env --- packages/api/env/auth/.env | 7 ------- packages/api/env/auth/.gitignore | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 packages/api/env/auth/.env create mode 100644 packages/api/env/auth/.gitignore diff --git a/packages/api/env/auth/.env b/packages/api/env/auth/.env deleted file mode 100644 index 2f525b9b..00000000 --- a/packages/api/env/auth/.env +++ /dev/null @@ -1,7 +0,0 @@ -DEVMODE=1 -OAUTH_AUTH=http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect/auth -OAUTH_TOKEN=http://auth.metrostav.vychozi.cz/auth/realms/metrostav/protocol/openid-connect/token -OAUTH_CLIENT_ID=dbgate -OAUTH_CLIENT_SECRET=ffd5634b-b60a-4c3a-bbec-b4144c73ea2a -OAUTH_LOGIN_FIELD=given_name -OAUTH_ALLOWED_LOGINS=Student1 \ No newline at end of file diff --git a/packages/api/env/auth/.gitignore b/packages/api/env/auth/.gitignore new file mode 100644 index 00000000..2eea525d --- /dev/null +++ b/packages/api/env/auth/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file From 5ccd7241666aed7f4266ab96344e4853665b1931 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Fri, 25 Nov 2022 16:38:17 +0100 Subject: [PATCH 08/14] support for acticve directory #261 --- packages/api/package.json | 1 + packages/api/src/controllers/auth.js | 45 ++++++++++++++-- packages/web/src/LoginPage.svelte | 14 +++-- packages/web/src/utility/api.ts | 4 ++ yarn.lock | 78 +++++++++++++++++++++++++++- 5 files changed, 133 insertions(+), 9 deletions(-) diff --git a/packages/api/package.json b/packages/api/package.json index e3dc03f0..236d4211 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -17,6 +17,7 @@ "dbgate" ], "dependencies": { + "activedirectory2": "^2.1.0", "async-lock": "^1.2.4", "axios": "^0.21.1", "body-parser": "^1.19.0", diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index e6d36bb7..df33d4f5 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -2,6 +2,8 @@ const axios = require('axios'); const jwt = require('jsonwebtoken'); const getExpressPath = require('../utility/getExpressPath'); const uuidv1 = require('uuid/v1'); +const { getLogins } = require('../utility/hasPermission'); +const AD = require('activedirectory2').promiseWrapper; const tokenSecret = uuidv1(); @@ -20,7 +22,7 @@ function unauthorizedResponse(req, res, text) { } function authMiddleware(req, res, next) { - const SKIP_AUTH_PATHS = ['/config/get', '/auth/oauth-token', '/stream']; + const SKIP_AUTH_PATHS = ['/config/get', '/auth/oauth-token', 'auth/login', '/stream']; if (!shouldAuthorizeApi()) { return next(); @@ -60,16 +62,51 @@ module.exports = { const login = process.env.OAUTH_LOGIN_FIELD ? payload[process.env.OAUTH_LOGIN_FIELD] : 'oauth'; - console.log(payload); - if (access_token) { return { - accessToken: jwt.sign({ user: 'oauth' }, tokenSecret, { expiresIn: '1m' }), + accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: '1m' }), }; } return { error: 'Token not found' }; }, + login_meta: true, + async login(params) { + const { login, password } = params; + + if (process.env.AD_URL && process.env.AD_BASEDN) { + const adConfig = { + url: process.env.AD_URL, + baseDN: process.env.AD_BASEDN, + username: process.env.AD_USERNAME, + password: process.env.AD_PASSOWRD, + }; + const ad = new AD(adConfig); + try { + const res = await ad.authenticate(login, password); + if (!res) { + return { error: 'login failed' }; + } + return { + accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: '1m' }), + }; + } catch (err) { + console.log('Failed active directory authentization', err.message); + return { error: err.message }; + } + } + + const logins = getLogins(); + if (!logins) { + return { error: 'Logins not configured' }; + } + if (logins[login] == password) { + return { + accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: '1m' }), + }; + } + return { error: 'Invalid credentials' }; + }, authMiddleware, shouldAuthorizeApi, diff --git a/packages/web/src/LoginPage.svelte b/packages/web/src/LoginPage.svelte index 705d8321..026b3874 100644 --- a/packages/web/src/LoginPage.svelte +++ b/packages/web/src/LoginPage.svelte @@ -5,6 +5,7 @@ import FormProvider from './forms/FormProvider.svelte'; import FormSubmit from './forms/FormSubmit.svelte'; import FormTextField from './forms/FormTextField.svelte'; + import { apiCall, enableApi } from './utility/api'; onMount(() => { const removed = document.getElementById('starting_dbgate_zero'); @@ -28,7 +29,8 @@ { - console.log('log in', e); + enableApi(); + apiCall('auth/login', e.detail); }} /> @@ -51,8 +53,10 @@ position: fixed; top: 1rem; left: 1rem; - font-size: 40pt; + font-size: 30pt; + font-family: monospace; color: var(--theme-bg-2); + text-transform: uppercase; } .submit { margin: var(--dim-large-form-margin); @@ -78,8 +82,10 @@ } .box { - max-width: 600px; - width: 40vw; + width: 600px; + max-width: 80vw; + /* max-width: 600px; + width: 40vw; */ border: 1px solid var(--theme-border); border-radius: 4px; background-color: var(--theme-bg-0); diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index ee878f25..2ca869a0 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -16,6 +16,10 @@ export function disableApi() { apiDisabled = true; } +export function enableApi() { + apiDisabled = false; +} + function wantEventSource() { if (!eventSource) { eventSource = new EventSource(`${resolveApi()}/stream`); diff --git a/yarn.lock b/yarn.lock index f5a01b77..ccb5e7f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1701,6 +1701,11 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abstract-logging@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" + integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA== + accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -1765,6 +1770,16 @@ acorn@^8.2.4, acorn@^8.5.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== +activedirectory2@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/activedirectory2/-/activedirectory2-2.1.0.tgz#4293f72ade8ff36e9199cf5fa5cae3818bdb947a" + integrity sha512-HaccG+/mf5NpHL1mAcLzXed4+gGlO6l7mkBi8vNIo6sTJvLoJjHgvJg12F4cy5CNcRqvPS48++s5tfdSiafn4Q== + dependencies: + abstract-logging "^2.0.0" + async "^3.1.0" + ldapjs "^2.1.0" + merge-options "^2.0.0" + adler-32@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25" @@ -2058,7 +2073,7 @@ async@^2.6.2: dependencies: lodash "^4.17.14" -async@^3.2.0, async@^3.2.3: +async@^3.1.0, async@^3.2.0, async@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== @@ -2225,6 +2240,13 @@ babel-preset-jest@^28.1.3: babel-plugin-jest-hoist "^28.1.3" babel-preset-current-node-syntax "^1.0.0" +backoff@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/backoff/-/backoff-2.5.0.tgz#f616eda9d3e4b66b8ca7fca79f695722c5f8e26f" + integrity sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA== + dependencies: + precond "0.2" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -5416,6 +5438,11 @@ is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== +is-plain-obj@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -7044,6 +7071,27 @@ kleur@^3.0.0, kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +ldap-filter@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/ldap-filter/-/ldap-filter-0.3.3.tgz#2b14c68a2a9d4104dbdbc910a1ca85fd189e9797" + integrity sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg== + dependencies: + assert-plus "^1.0.0" + +ldapjs@^2.1.0: + version "2.3.3" + resolved "https://registry.yarnpkg.com/ldapjs/-/ldapjs-2.3.3.tgz#06c317d3cbb5ac42fbba741e1a8b130ffcf997ab" + integrity sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg== + dependencies: + abstract-logging "^2.0.0" + asn1 "^0.2.4" + assert-plus "^1.0.0" + backoff "^2.5.0" + ldap-filter "^0.3.3" + once "^1.4.0" + vasync "^2.2.0" + verror "^1.8.1" + leaflet@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.8.0.tgz#4615db4a22a304e8e692cae9270b983b38a2055e" @@ -7398,6 +7446,13 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-options@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-2.0.0.tgz#36ca5038badfc3974dbde5e58ba89d3df80882c3" + integrity sha512-S7xYIeWHl2ZUKF7SDeBhGg6rfv5bKxVBdk95s/I7wVF8d+hjLSztJ/B271cnUiF6CAFduEQ5Zn3HYwAjT16DlQ== + dependencies: + is-plain-obj "^2.0.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -8623,6 +8678,11 @@ prebuild-install@^7.0.1, prebuild-install@^7.1.0, prebuild-install@^7.1.1: tar-fs "^2.0.0" tunnel-agent "^0.6.0" +precond@0.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" + integrity sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -10864,6 +10924,13 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +vasync@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/vasync/-/vasync-2.2.1.tgz#d881379ff3685e4affa8e775cf0fd369262a201b" + integrity sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ== + dependencies: + verror "1.10.0" + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -10873,6 +10940,15 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +verror@^1.8.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.1.tgz#4bf09eeccf4563b109ed4b3d458380c972b0cdeb" + integrity sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + vm-browserify@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" From 9a5287725bf40b42f24f2af02f726c4f3caa80b9 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Fri, 25 Nov 2022 16:59:41 +0100 Subject: [PATCH 09/14] login WIP --- packages/api/src/controllers/auth.js | 2 +- packages/api/src/controllers/config.js | 1 + packages/web/src/clientAuth.ts | 26 +++++++++++++++++--------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index df33d4f5..9d326abd 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -74,7 +74,7 @@ module.exports = { async login(params) { const { login, password } = params; - if (process.env.AD_URL && process.env.AD_BASEDN) { + if (process.env.AD_URL) { const adConfig = { url: process.env.AD_URL, baseDN: process.env.AD_BASEDN, diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index 8b59609d..f6a3c8bd 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -41,6 +41,7 @@ module.exports = { permissions, login, oauth: process.env.OAUTH_AUTH, + isLoginForm: !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH), ...currentVersion, }; }, diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts index 5b9a304a..af8a6511 100644 --- a/packages/web/src/clientAuth.ts +++ b/packages/web/src/clientAuth.ts @@ -33,7 +33,7 @@ export function handleOauthCallback() { } export async function handleAuthOnStartup(config) { - if (config.oauth) { + if (config.oauth || config.isLoginForm) { if (localStorage.getItem('accessToken')) { return; } @@ -45,12 +45,20 @@ export async function handleAuthOnStartup(config) { export async function redirectToLogin(config = null) { if (!config) config = await getConfig(); - const state = `dbg-oauth:${Math.random().toString().substr(2)}`; - sessionStorage.setItem('oauthState', state); - console.log('Redirecting to OAUTH provider'); - location.replace( - `${config.oauth}?client_id=dbgate&response_type=code&redirect_uri=${encodeURIComponent( - location.origin + location.pathname - )}&state=${encodeURIComponent(state)}` - ); + if (config.isLoginForm) { + const index = location.pathname.lastIndexOf('/'); + const loginPath = index >= 0 ? location.pathname.substring(0, index) + '/?page=login' : '/?page=login'; + location.replace(loginPath); + } + + if (config.oauth) { + const state = `dbg-oauth:${Math.random().toString().substr(2)}`; + sessionStorage.setItem('oauthState', state); + console.log('Redirecting to OAUTH provider'); + location.replace( + `${config.oauth}?client_id=dbgate&response_type=code&redirect_uri=${encodeURIComponent( + location.origin + location.pathname + )}&state=${encodeURIComponent(state)}` + ); + } } From b1ae7d53b9ad67425c7e9671968e60259ba41fde Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 26 Nov 2022 11:21:37 +0100 Subject: [PATCH 10/14] forms login --- packages/api/src/controllers/auth.js | 13 +++++---- packages/api/src/main.js | 2 +- packages/web/src/LoginPage.svelte | 20 ++++++++++--- packages/web/src/NotLoggedPage.svelte | 37 +++++++++++++++++++++++-- packages/web/src/clientAuth.ts | 29 ++++++++++++++----- packages/web/src/forms/TextField.svelte | 3 +- 6 files changed, 84 insertions(+), 20 deletions(-) diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index 9d326abd..b9e6a9af 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -8,7 +8,8 @@ const AD = require('activedirectory2').promiseWrapper; const tokenSecret = uuidv1(); function shouldAuthorizeApi() { - return !!process.env.OAUTH_AUTH; + const logins = getLogins(); + return !!process.env.OAUTH_AUTH || !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH); } function unauthorizedResponse(req, res, text) { @@ -22,7 +23,7 @@ function unauthorizedResponse(req, res, text) { } function authMiddleware(req, res, next) { - const SKIP_AUTH_PATHS = ['/config/get', '/auth/oauth-token', 'auth/login', '/stream']; + const SKIP_AUTH_PATHS = ['/config/get', '/auth/oauth-token', '/auth/login', '/stream']; if (!shouldAuthorizeApi()) { return next(); @@ -85,14 +86,16 @@ module.exports = { try { const res = await ad.authenticate(login, password); if (!res) { - return { error: 'login failed' }; + return { error: 'Login failed' }; } return { accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: '1m' }), }; } catch (err) { console.log('Failed active directory authentization', err.message); - return { error: err.message }; + return { + error: err.message, + }; } } @@ -100,7 +103,7 @@ module.exports = { if (!logins) { return { error: 'Logins not configured' }; } - if (logins[login] == password) { + if (logins.find(x => x.login == login)?.password == password) { return { accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: '1m' }), }; diff --git a/packages/api/src/main.js b/packages/api/src/main.js index a8967044..2edf4ca8 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -42,7 +42,7 @@ function start() { const server = http.createServer(app); const logins = getLogins(); - if (logins) { + if (logins && process.env.BASIC_AUTH) { app.use( basicAuth({ users: _.fromPairs(logins.map(x => [x.login, x.password])), diff --git a/packages/web/src/LoginPage.svelte b/packages/web/src/LoginPage.svelte index 026b3874..68944175 100644 --- a/packages/web/src/LoginPage.svelte +++ b/packages/web/src/LoginPage.svelte @@ -1,5 +1,6 @@ -
Sorry, you are not authorized to run DbGate
+
+
Sorry, you are not authorized to run DbGate
+ {#if error} +
{error}
+ {/if} + +
+ +
+
diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts index af8a6511..ac3e1ba3 100644 --- a/packages/web/src/clientAuth.ts +++ b/packages/web/src/clientAuth.ts @@ -1,4 +1,4 @@ -import { apiCall, disableApi } from './utility/api'; +import { apiCall, disableApi, enableApi } from './utility/api'; import { getConfig } from './utility/metadataLoaders'; export function isOauthCallback() { @@ -23,7 +23,7 @@ export function handleOauthCallback() { }).then(authResp => { const { accessToken } = authResp; localStorage.setItem('accessToken', accessToken); - location.replace('/'); + internalRedirectTo('/'); }); return true; @@ -42,13 +42,21 @@ export async function handleAuthOnStartup(config) { } } -export async function redirectToLogin(config = null) { - if (!config) config = await getConfig(); +export async function redirectToLogin(config = null, force = false) { + if (!config) { + enableApi(); + config = await getConfig(); + } if (config.isLoginForm) { - const index = location.pathname.lastIndexOf('/'); - const loginPath = index >= 0 ? location.pathname.substring(0, index) + '/?page=login' : '/?page=login'; - location.replace(loginPath); + if (!force) { + const params = new URLSearchParams(location.search); + if (params.get('page') == 'login' || params.get('page') == 'not-logged') { + return; + } + } + internalRedirectTo('/?page=login'); + return; } if (config.oauth) { @@ -60,5 +68,12 @@ export async function redirectToLogin(config = null) { location.origin + location.pathname )}&state=${encodeURIComponent(state)}` ); + return; } } + +export function internalRedirectTo(path) { + const index = location.pathname.lastIndexOf('/'); + const newPath = index >= 0 ? location.pathname.substring(0, index) + path : path; + location.replace(newPath); +} diff --git a/packages/web/src/forms/TextField.svelte b/packages/web/src/forms/TextField.svelte index 55f9a350..e373dce9 100644 --- a/packages/web/src/forms/TextField.svelte +++ b/packages/web/src/forms/TextField.svelte @@ -4,6 +4,7 @@ export let value; export let focused = false; export let domEditor = undefined; + export let autocomplete = 'new-password'; if (focused) onMount(() => domEditor.focus()); @@ -17,5 +18,5 @@ on:click bind:this={domEditor} on:keydown - autocomplete="new-password" + {autocomplete} /> From d84adcca5d4ad9e3e9ade8c6909e0bbfc2178572 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 27 Nov 2022 10:43:25 +0100 Subject: [PATCH 11/14] more robust oauth --- packages/api/src/controllers/auth.js | 20 +++++++++++++++++--- packages/api/src/controllers/config.js | 3 ++- packages/web/src/clientAuth.ts | 13 ++++++++++--- packages/web/src/commands/stdCommands.ts | 16 +++++++++++++++- packages/web/src/utility/api.ts | 9 +++++++-- 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index b9e6a9af..fbd359d4 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -28,11 +28,13 @@ function authMiddleware(req, res, next) { if (!shouldAuthorizeApi()) { return next(); } - if (SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x))) { - return next(); - } + let skipAuth = !!SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x)); + const authHeader = req.headers.authorization; if (!authHeader) { + if (skipAuth) { + return next(); + } return unauthorizedResponse(req, res, 'missing authorization header'); } const token = authHeader.split(' ')[1]; @@ -41,6 +43,12 @@ function authMiddleware(req, res, next) { req.user = decoded; return next(); } catch (err) { + if (skipAuth) { + return next(); + } + + console.log('Sending invalid token error', err.message); + return unauthorizedResponse(req, res, 'invalid token'); } } @@ -63,6 +71,12 @@ module.exports = { const login = process.env.OAUTH_LOGIN_FIELD ? payload[process.env.OAUTH_LOGIN_FIELD] : 'oauth'; + if ( + process.env.OAUTH_ALLOWED_LOGINS && + !process.env.OAUTH_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() != login.toLowerCase().trim()) + ) { + return { error: `Username ${login} not allowed to log in` }; + } if (access_token) { return { accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: '1m' }), diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index f6a3c8bd..dbfe6ef0 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -28,7 +28,7 @@ module.exports = { get_meta: true, async get(_params, req) { const logins = getLogins(); - const login = logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null; + const login = req.user ? req.user.login : logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null; const permissions = login ? login.permissions : process.env.PERMISSIONS; return { @@ -41,6 +41,7 @@ module.exports = { permissions, login, oauth: process.env.OAUTH_AUTH, + oauthLogout: process.env.OAUTH_LOGOUT, isLoginForm: !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH), ...currentVersion, }; diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts index ac3e1ba3..367c3881 100644 --- a/packages/web/src/clientAuth.ts +++ b/packages/web/src/clientAuth.ts @@ -21,9 +21,16 @@ export function handleOauthCallback() { code: sentCode, redirectUri: location.origin + location.pathname, }).then(authResp => { - const { accessToken } = authResp; - localStorage.setItem('accessToken', accessToken); - internalRedirectTo('/'); + const { accessToken, error, errorMessage } = authResp; + + if (accessToken) { + console.log('Settings access token from OAUTH'); + localStorage.setItem('accessToken', accessToken); + internalRedirectTo('/'); + } else { + console.log('Error when processing OAUTH callback', error || errorMessage); + internalRedirectTo(`/?page=not-logged&error=${error || errorMessage}`); + } }); return true; diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index b0260cce..5beb4f8c 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -36,6 +36,7 @@ import runCommand from './runCommand'; import { openWebLink } from '../utility/exportFileTools'; import { getSettings } from '../utility/metadataLoaders'; import { isMac } from '../utility/common'; +import { internalRedirectTo } from '../clientAuth'; // function themeCommand(theme: ThemeDefinition) { // return { @@ -549,7 +550,20 @@ registerCommand({ name: 'Logout', testEnabled: () => getCurrentConfig()?.login != null, onClick: () => { - window.location.href = 'config/logout'; + const config = getCurrentConfig(); + if (config.oauth) { + localStorage.removeItem('accessToken'); + if (config.oauthLogout) { + window.location.href = config.oauthLogout; + } else { + internalRedirectTo('/?page=not-logged'); + } + } else if (config.isLoginForm) { + localStorage.removeItem('accessToken'); + internalRedirectTo('/?page=not-logged'); + } else { + window.location.href = 'config/logout'; + } }, }); diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index 2ca869a0..e55c839c 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -71,10 +71,15 @@ export async function apiCall(route: string, args: {} = undefined) { }); if (resp.status == 401 && !apiDisabled) { + const params = new URLSearchParams(location.search); + disableApi(); console.log('Disabling API', route); - // unauthorized - redirectToLogin(); + if (params.get('page') != 'login' && params.get('page') != 'not-logged') { + // unauthorized + redirectToLogin(); + } + return; } const json = await resp.json(); From 012d3ec2e1ceaa11ce3bd30dec7ced54bf917e8a Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 27 Nov 2022 10:56:50 +0100 Subject: [PATCH 12/14] logout button from not logged page --- packages/api/src/controllers/auth.js | 4 +++- packages/web/src/NotLoggedPage.svelte | 3 ++- packages/web/src/clientAuth.ts | 23 ++++++++++++++++++++++- packages/web/src/commands/stdCommands.ts | 19 ++----------------- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index fbd359d4..f2822782 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -69,11 +69,13 @@ module.exports = { const payload = jwt.decode(access_token); + console.log('User payload returned from OAUTH:', payload); + const login = process.env.OAUTH_LOGIN_FIELD ? payload[process.env.OAUTH_LOGIN_FIELD] : 'oauth'; if ( process.env.OAUTH_ALLOWED_LOGINS && - !process.env.OAUTH_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() != login.toLowerCase().trim()) + !process.env.OAUTH_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim()) ) { return { error: `Username ${login} not allowed to log in` }; } diff --git a/packages/web/src/NotLoggedPage.svelte b/packages/web/src/NotLoggedPage.svelte index 7af0fc6e..0dad204f 100644 --- a/packages/web/src/NotLoggedPage.svelte +++ b/packages/web/src/NotLoggedPage.svelte @@ -1,7 +1,7 @@