Skip to content

Handle token expiration in SSO #611

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 28, 2024
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"mui-color": "^2.0.0-beta.2",
"mui-nested-menu": "^3.2.1",
"neo4j-client-sso": "^1.2.2",
"neo4j-driver": "^5.12.0",
"openai": "^3.3.0",
"postcss": "^8.4.21",
"postcss-loader": "^7.2.4",
Expand Down
7 changes: 5 additions & 2 deletions src/application/ApplicationActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* This file contains all state-changing actions relevant for the main application.
*/

import { SSOProviderOriginal } from 'neo4j-client-sso';

export const CLEAR_NOTIFICATION = 'APPLICATION/CLEAR_NOTIFICATION';
export const clearNotification = () => ({
type: CLEAR_NOTIFICATION,
Expand Down Expand Up @@ -56,10 +58,11 @@ export const setConnectionProperties = (
port: string,
database: string,
username: string,
password: string
password: string,
ssoProviders?: SSOProviderOriginal[]
) => ({
type: SET_CONNECTION_PROPERTIES,
payload: { protocol, url, port, database, username, password },
payload: { protocol, url, port, database, username, password, ssoProviders },
});

export const SET_BASIC_CONNECTION_PROPERTIES = 'APPLICATION/SET_BASIC_CONNECTION_PROPERTIES';
Expand Down
4 changes: 3 additions & 1 deletion src/application/ApplicationReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const initialState = {
database: '',
username: 'neo4j',
password: '',
ssoProviders: [],
},
shareDetails: undefined,
desktopConnection: null,
Expand Down Expand Up @@ -246,7 +247,7 @@ export const applicationReducer = (state = initialState, action: { type: any; pa
return state;
}
case SET_CONNECTION_PROPERTIES: {
const { protocol, url, port, database, username, password } = payload;
const { protocol, url, port, database, username, password, ssoProviders } = payload;
state = update(state, {
connection: {
protocol: protocol,
Expand All @@ -255,6 +256,7 @@ export const applicationReducer = (state = initialState, action: { type: any; pa
database: database,
username: username,
password: password,
ssoProviders,
},
});
return state;
Expand Down
70 changes: 63 additions & 7 deletions src/application/ApplicationThunks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createDriver } from 'use-neo4j';
import { initializeSSO } from '../component/sso/SSOUtils';
import { DEFAULT_SCREEN, Screens } from '../config/ApplicationConfig';
import { setDashboard } from '../dashboard/DashboardActions';
Expand Down Expand Up @@ -44,6 +43,9 @@ import {
} from './ApplicationActions';
import { setLoggingMode, setLoggingDatabase, setLogErrorNotification } from './logging/LoggingActions';
import { version } from '../modal/AboutModal';
import neo4j, { auth, authTokenManagers } from 'neo4j-driver';
import type { Neo4jScheme } from 'use-neo4j/dist/neo4j-config.interface';
import { SSOProviderOriginal, handleRefreshingToken } from 'neo4j-client-sso';
import { applicationIsStandalone } from './ApplicationSelectors';
import { applicationGetLoggingSettings } from './logging/LoggingSelectors';
import { createLogThunk } from './logging/LoggingThunk';
Expand All @@ -54,6 +56,47 @@ import { createUUID } from '../utils/uuid';
* Several actions/other thunks may be dispatched from here.
*/

export const createDriver = (
scheme: Neo4jScheme,
host: string,
port: string | number,
username?: string,
password?: string,
config?: { userAgent?: string },
ssoProviders: SSOProviderOriginal[] = []
) => {
if (ssoProviders.length > 0) {
const authTokenMgr = authTokenManagers.bearer({
tokenProvider: async () => {
const credentials = await handleRefreshingToken(ssoProviders);
const token = auth.bearer(credentials.password);
// Get the expiration from the JWT's payload, which is a JSON string encoded
// using base64. You could also use a JWT parsing lib
const [, payloadBase64] = credentials.password.split('.');
const payload: unknown = JSON.parse(window.atob(payloadBase64 ?? ''));
let expiration: Date;
if (typeof payload === 'object' && payload !== null && 'exp' in payload) {
expiration = new Date(Number(payload.exp) * 1000);
} else {
expiration = new Date();
}

return {
expiration,
token,
};
},
});
return neo4j.driver(`${scheme}://${host}:${port}`, authTokenMgr, config);
}

if (!username || !password) {
return neo4j.driver(`${scheme}://${host}:${port}`);
}

return neo4j.driver(`${scheme}://${host}:${port}`, neo4j.auth.basic(username, password), config);
};

/**
* Establish a connection to Neo4j with the specified credentials. Open/close the relevant windows when connection is made (un)successfully.
* @param protocol - the neo4j protocol (e.g. bolt, bolt+s, neo4j+s, ...)
Expand All @@ -62,14 +105,24 @@ import { createUUID } from '../utils/uuid';
* @param database - the Neo4j database to connect to.
* @param username - Neo4j username.
* @param password - Neo4j password.
* @param SSOProviders - List of available SSO providers
*/
export const createConnectionThunk =
(protocol, url, port, database, username, password) => (dispatch: any, getState: any) => {
(protocol, url, port, database, username, password, SSOProviders = []) =>
(dispatch: any, getState: any) => {
const loggingState = getState();
const loggingSettings = applicationGetLoggingSettings(loggingState);
const neodashMode = applicationIsStandalone(loggingState) ? 'Standalone' : 'Editor';
try {
const driver = createDriver(protocol, url, port, username, password, { userAgent: `neodash/v${version}` });
const driver = createDriver(
protocol,
url,
port,
username,
password,
{ userAgent: `neodash/v${version}` },
SSOProviders
);
// eslint-disable-next-line no-console
console.log('Attempting to connect...');
const validateConnection = (records) => {
Expand Down Expand Up @@ -508,7 +561,7 @@ export const loadApplicationConfigThunk = () => async (dispatch: any, getState:
dispatch(setAboutModalOpen(false));
dispatch(setConnected(false));
dispatch(setWelcomeScreenOpen(false));
const success = await initializeSSO(state.application.cachedSSODiscoveryUrl, (credentials) => {
const success = await initializeSSO(state.application.cachedSSODiscoveryUrl, (credentials, ssoProviders) => {
if (standalone) {
// Redirected from SSO and running in viewer mode, merge retrieved config with hardcoded credentials.
dispatch(
Expand All @@ -518,7 +571,8 @@ export const loadApplicationConfigThunk = () => async (dispatch: any, getState:
config.standalonePort,
config.standaloneDatabase,
credentials.username,
credentials.password
credentials.password,
ssoProviders
)
);
dispatch(
Expand All @@ -528,7 +582,8 @@ export const loadApplicationConfigThunk = () => async (dispatch: any, getState:
config.standalonePort,
config.standaloneDatabase,
credentials.username,
credentials.password
credentials.password,
ssoProviders
)
);
} else {
Expand All @@ -540,7 +595,8 @@ export const loadApplicationConfigThunk = () => async (dispatch: any, getState:
state.application.connection.port,
state.application.connection.database,
credentials.username,
credentials.password
credentials.password,
ssoProviders
)
);
dispatch(setConnected(true));
Expand Down
2 changes: 1 addition & 1 deletion src/component/sso/SSOUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export const initializeSSO = async (cachedSSODiscoveryUrl, _setCredentials) => {
// Successful credentials retrieval.
// Log in at the Neo4j dbms now using the Neo4j (js) driver.
//
_setCredentials(credentials);
_setCredentials(credentials, mergedSSOProviders);

// Exemplifying retrieval of stored URL paramenters
_retrieveAdditionalURLParameters();
Expand Down
7 changes: 5 additions & 2 deletions src/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import NeoPage from '../page/Page';
import NeoDashboardHeader from './header/DashboardHeader';
import NeoDashboardTitle from './header/DashboardTitle';
import NeoDashboardHeaderPageList from './header/DashboardHeaderPageList';
import { createDriver, Neo4jProvider } from 'use-neo4j';
import { Neo4jProvider } from 'use-neo4j';
import { applicationGetConnection, applicationGetStandaloneSettings } from '../application/ApplicationSelectors';
import { connect } from 'react-redux';
import NeoDashboardConnectionUpdateHandler from '../component/misc/DashboardConnectionUpdateHandler';
import { forceRefreshPage } from '../page/PageActions';
import { getPageNumber } from '../settings/SettingsSelectors';
import { createNotificationThunk } from '../page/PageThunks';
import { version } from '../modal/AboutModal';
import { createDriver } from '../application/ApplicationThunks';
import NeoDashboardSidebar from './sidebar/DashboardSidebar';

const Dashboard = ({
Expand All @@ -32,8 +33,10 @@ const Dashboard = ({
connection.port,
connection.username,
connection.password,
{ userAgent: `neodash/v${version}` }
{ userAgent: `neodash/v${version}` },
connection.ssoProviders
);
// @ts-ignore wrong driver version
setDriver(newDriver);
}
const content = (
Expand Down
30 changes: 30 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10435,6 +10435,15 @@ neo4j-client-sso@^1.2.2:
jwt-decode "^3.1.2"
lodash.pick "^4.4.0"

[email protected]:
version "5.12.0"
resolved "https://registry.yarnpkg.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-5.12.0.tgz#aff161367d287579d7bdd3ee4179eed324398210"
integrity sha512-dlYbFsfT0HopGItitG5uDK4nAkcqSPNtRqMz318qy//7fb/7OXVLGYikj57Ve1toJiJD8IIVErt/dVuEUHVxGA==
dependencies:
buffer "^6.0.3"
neo4j-driver-core "5.12.0"
string_decoder "^1.3.0"

neo4j-driver-bolt-connection@^4.4.10:
version "4.4.10"
resolved "https://registry.yarnpkg.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-4.4.10.tgz#a8b5b7f82b1d6f9a71a43eafcb0e21512ea24908"
Expand All @@ -10444,6 +10453,11 @@ neo4j-driver-bolt-connection@^4.4.10:
neo4j-driver-core "^4.4.10"
string_decoder "^1.3.0"

[email protected]:
version "5.12.0"
resolved "https://registry.yarnpkg.com/neo4j-driver-core/-/neo4j-driver-core-5.12.0.tgz#1f8616da7e945921574811368a68f5d2501bfd35"
integrity sha512-xBRi5oezysDUvtvBiIgBchzumkDZxvR9ol9sUtA9PBgVENeSmPH3CncitY8S979CFELS6wH7kydcjPLB4QMOzA==

neo4j-driver-core@^4.4.10:
version "4.4.10"
resolved "https://registry.yarnpkg.com/neo4j-driver-core/-/neo4j-driver-core-4.4.10.tgz#6f4c1ccc1199f864b149bdcef5e50e45ff95c29e"
Expand All @@ -10459,6 +10473,15 @@ neo4j-driver@^4.4.5:
neo4j-driver-core "^4.4.10"
rxjs "^6.6.3"

neo4j-driver@^5.12.0:
version "5.12.0"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-5.12.0.tgz#1b2d7db1672ad224f0146542efee306a0a156a11"
integrity sha512-T2Vz63XDkL9TomM16dBusuXbo7d9SIGw2g3VR/rmrWTdbl1V1LYFx/u1P7AwBsFuX08oncKHfZwHGsWrCvdMyA==
dependencies:
neo4j-driver-bolt-connection "5.12.0"
neo4j-driver-core "5.12.0"
rxjs "^7.8.1"

next-tick@1, next-tick@^1.0.0, next-tick@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
Expand Down Expand Up @@ -12288,6 +12311,13 @@ rxjs@^7.5.1, rxjs@^7.5.5, rxjs@^7.8.0:
dependencies:
tslib "^2.1.0"

rxjs@^7.8.1:
version "7.8.1"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
dependencies:
tslib "^2.1.0"

sade@^1.7.3:
version "1.8.1"
resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
Expand Down