diff --git a/client/package-lock.json b/client/package-lock.json
index 0a51ac3a..33a2ea2b 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -30,6 +30,7 @@
"react-i18next": "^12.0.0",
"react-input-mask": "^2.0.4",
"react-markdown": "^8.0.3",
+ "react-oidc-context": "^2.2.2",
"react-photoswipe-gallery": "^2.2.2",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.3",
@@ -7189,6 +7190,12 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-js": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
+ "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==",
+ "peer": true
+ },
"node_modules/crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -17035,6 +17042,19 @@
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
},
+ "node_modules/oidc-client-ts": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.2.4.tgz",
+ "integrity": "sha512-nOZwIomju+AmXObl5Oq5PjrES/qTt8bLsENJCIydVgi9TEWk7SCkOU6X3RNkY7yfySRM1OJJvDKdREZdmnDT2g==",
+ "peer": true,
+ "dependencies": {
+ "crypto-js": "^4.1.1",
+ "jwt-decode": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.13.0"
+ }
+ },
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -19276,6 +19296,18 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
+ "node_modules/react-oidc-context": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-2.2.2.tgz",
+ "integrity": "sha512-rke8goKuxxQhSAR11h8wVn56m7kEa1mcAfYDlIycsIgmbZOFzKA0Un0y0RodHZ/M/CG6u0JD1I8RKZHqRJjWnA==",
+ "engines": {
+ "node": ">=12.13.0"
+ },
+ "peerDependencies": {
+ "oidc-client-ts": "^2.2.1",
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/react-onclickoutside": {
"version": "6.12.2",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
@@ -28767,6 +28799,12 @@
"which": "^2.0.1"
}
},
+ "crypto-js": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
+ "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==",
+ "peer": true
+ },
"crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -35933,6 +35971,16 @@
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
},
+ "oidc-client-ts": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.2.4.tgz",
+ "integrity": "sha512-nOZwIomju+AmXObl5Oq5PjrES/qTt8bLsENJCIydVgi9TEWk7SCkOU6X3RNkY7yfySRM1OJJvDKdREZdmnDT2g==",
+ "peer": true,
+ "requires": {
+ "crypto-js": "^4.1.1",
+ "jwt-decode": "^3.1.2"
+ }
+ },
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -37371,6 +37419,12 @@
}
}
},
+ "react-oidc-context": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-2.2.2.tgz",
+ "integrity": "sha512-rke8goKuxxQhSAR11h8wVn56m7kEa1mcAfYDlIycsIgmbZOFzKA0Un0y0RodHZ/M/CG6u0JD1I8RKZHqRJjWnA==",
+ "requires": {}
+ },
"react-onclickoutside": {
"version": "6.12.2",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
diff --git a/client/package.json b/client/package.json
index d1dd8d6b..ad8303bb 100755
--- a/client/package.json
+++ b/client/package.json
@@ -77,6 +77,7 @@
"react-i18next": "^12.0.0",
"react-input-mask": "^2.0.4",
"react-markdown": "^8.0.3",
+ "react-oidc-context": "^2.2.2",
"react-photoswipe-gallery": "^2.2.2",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.3",
diff --git a/client/src/api/access-tokens.js b/client/src/api/access-tokens.js
index 1778618a..0a1dcf94 100755
--- a/client/src/api/access-tokens.js
+++ b/client/src/api/access-tokens.js
@@ -4,6 +4,8 @@ import socket from './socket';
/* Actions */
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
+const exchangeOidcToken = (accessToken, headers) =>
+ http.post('/access-tokens/exchange', { token: accessToken }, headers);
const deleteCurrentAccessToken = (headers) =>
socket.delete('/access-tokens/me', undefined, headers);
@@ -11,4 +13,5 @@ const deleteCurrentAccessToken = (headers) =>
export default {
createAccessToken,
deleteCurrentAccessToken,
+ exchangeOidcToken,
};
diff --git a/client/src/components/Header/Header.jsx b/client/src/components/Header/Header.jsx
index 443d3a88..0bb246e0 100755
--- a/client/src/components/Header/Header.jsx
+++ b/client/src/components/Header/Header.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { Button, Icon, Menu } from 'semantic-ui-react';
+import { useAuth } from 'react-oidc-context';
import { usePopup } from '../../lib/popup';
import Paths from '../../constants/Paths';
@@ -29,6 +30,7 @@ const Header = React.memo(
onUserSettingsClick,
onLogout,
}) => {
+ const auth = useAuth();
const handleProjectSettingsClick = useCallback(() => {
if (canEditProject) {
onProjectSettingsClick();
@@ -38,6 +40,11 @@ const Header = React.memo(
const NotificationsPopup = usePopup(NotificationsStep, POPUP_PROPS);
const UserPopup = usePopup(UserStep, POPUP_PROPS);
+ const onFullLogout = () => {
+ auth.signoutSilent();
+ onLogout();
+ };
+
return (
{!project && (
@@ -88,7 +95,7 @@ const Header = React.memo(
{user.name}
diff --git a/client/src/components/Login/Login.jsx b/client/src/components/Login/Login.jsx
index a7e67a16..ce09c3d4 100755
--- a/client/src/components/Login/Login.jsx
+++ b/client/src/components/Login/Login.jsx
@@ -1,5 +1,6 @@
import isEmail from 'validator/lib/isEmail';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
+import { useAuth } from 'react-oidc-context';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
@@ -48,6 +49,7 @@ const createMessage = (error) => {
const Login = React.memo(
({ defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss }) => {
+ const auth = useAuth();
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@@ -171,6 +173,9 @@ const Login = React.memo(
disabled={isSubmitting}
/>
+ auth.signinRedirect()}>
+ Log in with SSO
+
diff --git a/client/src/components/OIDC/OidcLogin.jsx b/client/src/components/OIDC/OidcLogin.jsx
new file mode 100644
index 00000000..9ccd5e8f
--- /dev/null
+++ b/client/src/components/OIDC/OidcLogin.jsx
@@ -0,0 +1,19 @@
+import { useAuth } from 'react-oidc-context';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+let isLoggingIn = true;
+const OidcLogin = React.memo(({ onAuthenticate }) => {
+ const auth = useAuth();
+ if (isLoggingIn && auth.user) {
+ isLoggingIn = false;
+ const { user } = auth;
+ onAuthenticate(user);
+ }
+});
+
+OidcLogin.propTypes = {
+ onAuthenticate: PropTypes.func.isRequired,
+};
+
+export default OidcLogin;
diff --git a/client/src/components/OIDC/index.js b/client/src/components/OIDC/index.js
new file mode 100644
index 00000000..ef357747
--- /dev/null
+++ b/client/src/components/OIDC/index.js
@@ -0,0 +1,3 @@
+import OidcLogin from './OidcLogin';
+
+export default OidcLogin;
diff --git a/client/src/components/Root.jsx b/client/src/components/Root.jsx
index a5671f97..d59b47d3 100755
--- a/client/src/components/Root.jsx
+++ b/client/src/components/Root.jsx
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { AuthProvider } from 'react-oidc-context';
import { Provider } from 'react-redux';
import { Route, Routes } from 'react-router-dom';
import { ReduxRouter } from '../lib/redux-router';
@@ -13,30 +14,41 @@ import 'react-datepicker/dist/react-datepicker.css';
import 'photoswipe/dist/photoswipe.css';
import 'easymde/dist/easymde.min.css';
import '../lib/custom-ui/styles.css';
-
import '../styles.module.scss';
+import OidcLoginContainer from '../containers/OidcLoginContainer';
-function Root({ store, history }) {
+function Root({ store, history, config }) {
return (
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
+ {
+ window.history.replaceState({}, document.title, window.location.pathname);
+ }}
+ >
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
);
}
-
Root.propTypes = {
/* eslint-disable react/forbid-prop-types */
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
+ config: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
};
diff --git a/client/src/constants/Paths.js b/client/src/constants/Paths.js
index 6f0d666a..60eba964 100755
--- a/client/src/constants/Paths.js
+++ b/client/src/constants/Paths.js
@@ -2,6 +2,7 @@ import Config from './Config';
const ROOT = `${Config.BASE_PATH}/`;
const LOGIN = `${Config.BASE_PATH}/login`;
+const OIDC_LOGIN = `${Config.BASE_PATH}/oidclogin`;
const PROJECTS = `${Config.BASE_PATH}/projects/:id`;
const BOARDS = `${Config.BASE_PATH}/boards/:id`;
const CARDS = `${Config.BASE_PATH}/cards/:id`;
@@ -12,4 +13,5 @@ export default {
PROJECTS,
BOARDS,
CARDS,
+ OIDC_LOGIN,
};
diff --git a/client/src/containers/OidcLoginContainer.js b/client/src/containers/OidcLoginContainer.js
new file mode 100644
index 00000000..15083ad8
--- /dev/null
+++ b/client/src/containers/OidcLoginContainer.js
@@ -0,0 +1,26 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import entryActions from '../entry-actions';
+import OidcLogin from '../components/OIDC';
+
+const mapStateToProps = ({
+ ui: {
+ authenticateForm: { data: defaultData, isSubmitting, error },
+ },
+}) => ({
+ defaultData,
+ isSubmitting,
+ error,
+});
+
+const mapDispatchToProps = (dispatch) =>
+ bindActionCreators(
+ {
+ onAuthenticate: entryActions.authenticate,
+ onMessageDismiss: entryActions.clearAuthenticateError,
+ },
+ dispatch,
+ );
+
+export default connect(mapStateToProps, mapDispatchToProps)(OidcLogin);
diff --git a/client/src/index.js b/client/src/index.js
index 574cafee..ef5afbad 100755
--- a/client/src/index.js
+++ b/client/src/index.js
@@ -1,11 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
+import Config from './constants/Config';
import store from './store';
import history from './history';
import Root from './components/Root';
-
import './i18n';
-const root = ReactDOM.createRoot(document.getElementById('root'));
-root.render(React.createElement(Root, { store, history }));
+fetch(`${Config.SERVER_BASE_URL}/api/appconfig`).then((response) => {
+ response.json().then((config) => {
+ const root = ReactDOM.createRoot(document.getElementById('root'));
+ root.render(React.createElement(Root, { store, history, config }));
+ });
+});
diff --git a/client/src/sagas/login/services/login.js b/client/src/sagas/login/services/login.js
index 7bdf7f2d..75bbd7f6 100644
--- a/client/src/sagas/login/services/login.js
+++ b/client/src/sagas/login/services/login.js
@@ -7,9 +7,13 @@ import { setAccessToken } from '../../../utils/access-token-storage';
export function* authenticate(data) {
yield put(actions.authenticate(data));
- let accessToken;
+ let accessToken = data.access_token;
try {
- ({ item: accessToken } = yield call(api.createAccessToken, data));
+ if (accessToken) {
+ ({ item: accessToken } = yield call(api.exchangeOidcToken, accessToken));
+ } else {
+ ({ item: accessToken } = yield call(api.createAccessToken, data));
+ }
} catch (error) {
yield put(actions.authenticate.failure(error));
return;
diff --git a/server/api/controllers/access-tokens/exchange.js b/server/api/controllers/access-tokens/exchange.js
new file mode 100644
index 00000000..04ba58d8
--- /dev/null
+++ b/server/api/controllers/access-tokens/exchange.js
@@ -0,0 +1,177 @@
+const jwt = require('jsonwebtoken');
+const jwksClient = require('jwks-rsa');
+const openidClient = require('openid-client');
+const { getRemoteAddress } = require('../../../utils/remoteAddress');
+
+const Errors = {
+ INVALID_TOKEN: {
+ invalidToken: 'Access Token is invalid',
+ },
+ MISSING_VALUES: {
+ missingValues:
+ 'Unable to retrieve required values. Verify the access token or UserInfo endpoint has email, username and name claims',
+ },
+};
+
+const jwks = jwksClient({
+ jwksUri: sails.config.custom.oidcJwksUri,
+ requestHeaders: {}, // Optional
+ timeout: 30000, // Defaults to 30s
+});
+
+const getJwtVerificationOptions = () => {
+ const options = {};
+ if (sails.config.custom.oidcIssuer) {
+ options.issuer = sails.config.custom.oidcIssuer;
+ }
+ if (sails.config.custom.oidcAudience) {
+ options.audience = sails.config.custom.oidcAudience;
+ }
+ return options;
+};
+
+const validateAndDecodeToken = async (accessToken, options) => {
+ const keys = await jwks.getSigningKeys();
+ let validToken = {};
+
+ const isTokenValid = keys.some((signingKey) => {
+ try {
+ const key = signingKey.getPublicKey();
+ validToken = jwt.verify(accessToken, key, options);
+ return 'true';
+ } catch (error) {
+ sails.log.error(error);
+ }
+ return false;
+ });
+
+ if (!isTokenValid) {
+ const tokenForLogging = jwt.decode(accessToken);
+ const remoteAddress = getRemoteAddress(this.req);
+
+ sails.log.warn(
+ `invalid token: sub: "${tokenForLogging.sub}" issuer: "${tokenForLogging.iss}" audience: "${tokenForLogging.aud}" exp: ${tokenForLogging.exp} (IP: ${remoteAddress})`,
+ );
+ throw Errors.INVALID_TOKEN;
+ }
+ return validToken;
+};
+
+const getUserInfo = async (accessToken, options) => {
+ if (sails.config.custom.oidcSkipUserInfo) {
+ return {};
+ }
+ const issuer = await openidClient.Issuer.discover(options.issuer);
+ const oidcClient = new issuer.Client({
+ client_id: 'irrelevant',
+ });
+ const userInfo = await oidcClient.userinfo(accessToken);
+ return userInfo;
+};
+const mergeUserData = (validToken, userInfo) => {
+ const oidcUser = { ...validToken, ...userInfo };
+ return oidcUser;
+};
+const getOrCreateUser = async (newUser) => {
+ const user = await User.findOne({
+ where: {
+ username: newUser.username,
+ },
+ });
+ if (user) {
+ return user;
+ }
+ return User.create(newUser).fetch();
+};
+module.exports = {
+ inputs: {
+ token: {
+ type: 'string',
+ required: true,
+ },
+ },
+
+ exits: {
+ invalidToken: {
+ responseType: 'unauthorized',
+ },
+ missingValues: {
+ responseType: 'unauthorized',
+ },
+ },
+
+ async fn(inputs) {
+ const options = getJwtVerificationOptions();
+ const validToken = await validateAndDecodeToken(inputs.token, options);
+ const userInfo = await getUserInfo(inputs.token, options);
+ const oidcUser = mergeUserData(validToken, userInfo);
+
+ const now = new Date();
+ let isAdmin = false;
+ if (sails.config.custom.oidcAdminRoles.includes('*')) isAdmin = true;
+ else if (Array.isArray(oidcUser[sails.config.custom.oidcRolesAttribute])) {
+ const userRoles = new Set(oidcUser[sails.config.custom.oidcRolesAttribute]);
+ isAdmin = sails.config.custom.oidcAdminRoles.findIndex((role) => userRoles.has(role)) > -1;
+ }
+
+ const newUser = {
+ email: oidcUser.email,
+ isAdmin,
+ name: oidcUser.name,
+ username: oidcUser.preferred_username,
+ subscribeToOwnCards: false,
+ createdAt: now,
+ updatedAt: now,
+ locked: true,
+ };
+
+ if (!newUser.email || !newUser.username || !newUser.name) {
+ sails.log.error(Errors.MISSING_VALUES.missingValues);
+ throw Errors.MISSING_VALUES;
+ }
+
+ const identityProviderUser = await IdentityProviderUser.findOne({
+ where: {
+ issuer: oidcUser.iss,
+ sub: oidcUser.sub,
+ },
+ }).populate('userId');
+
+ let user = identityProviderUser ? identityProviderUser.userId : {};
+ if (!identityProviderUser) {
+ user = await getOrCreateUser(newUser);
+ await IdentityProviderUser.create({
+ issuer: oidcUser.iss,
+ sub: oidcUser.sub,
+ userId: user.id,
+ });
+ }
+
+ const controlledFields = ['email', 'password', 'isAdmin', 'name', 'username'];
+ const updateFields = {};
+ controlledFields.forEach((field) => {
+ if (user[field] !== newUser[field]) {
+ updateFields[field] = newUser[field];
+ }
+ });
+
+ if (Object.keys(updateFields).length > 0) {
+ updateFields.updatedAt = now;
+ await User.updateOne({ id: user.id }).set(updateFields);
+ }
+
+ const plankaToken = sails.helpers.utils.createToken(user.id);
+
+ const remoteAddress = getRemoteAddress(this.req);
+ await Session.create({
+ accessToken: plankaToken,
+ remoteAddress,
+ userId: user.id,
+ userAgent: this.req.headers['user-agent'],
+ });
+
+ return {
+ item: plankaToken,
+ };
+ },
+};
diff --git a/server/api/controllers/appconfig/index.js b/server/api/controllers/appconfig/index.js
new file mode 100644
index 00000000..eef7be68
--- /dev/null
+++ b/server/api/controllers/appconfig/index.js
@@ -0,0 +1,11 @@
+module.exports = {
+ async fn() {
+ const config = {
+ authority: sails.config.custom.oidcIssuer,
+ clientId: sails.config.custom.oidcClientId,
+ redirectUri: sails.config.custom.oidcredirectUri,
+ scopes: sails.config.custom.oidcScopes,
+ };
+ return config;
+ },
+};
diff --git a/server/api/models/IdentityProviderUser.js b/server/api/models/IdentityProviderUser.js
new file mode 100644
index 00000000..c4ae0057
--- /dev/null
+++ b/server/api/models/IdentityProviderUser.js
@@ -0,0 +1,40 @@
+/**
+ * ProjectManager.js
+ *
+ * @description :: A model definition represents a database table/collection.
+ * @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
+ */
+
+module.exports = {
+ attributes: {
+ issuer: {
+ type: 'string',
+ isNotEmptyString: true,
+ allowNull: true,
+ },
+ sub: {
+ type: 'string',
+ isNotEmptyString: true,
+ allowNull: true,
+ },
+ // ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
+ // ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
+ // ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
+
+ // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
+ // ║╣ ║║║╠╩╗║╣ ║║╚═╗
+ // ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
+
+ // ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
+ // ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
+ // ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
+
+ userId: {
+ model: 'User',
+ required: true,
+ columnName: 'user_id',
+ },
+ },
+
+ tableName: 'identity_provider_user',
+};
diff --git a/server/api/models/User.js b/server/api/models/User.js
index 65cd3077..73e876b6 100755
--- a/server/api/models/User.js
+++ b/server/api/models/User.js
@@ -18,7 +18,6 @@ module.exports = {
},
password: {
type: 'string',
- required: true,
},
isAdmin: {
type: 'boolean',
@@ -68,6 +67,10 @@ module.exports = {
type: 'ref',
columnName: 'password_changed_at',
},
+ locked: {
+ type: 'boolean',
+ columnName: 'locked',
+ },
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
@@ -97,6 +100,10 @@ module.exports = {
via: 'userId',
through: 'CardMembership',
},
+ identityProviders: {
+ collection: 'IdentityProviderUser',
+ via: 'userId',
+ },
},
tableName: 'user_account',
diff --git a/server/config/custom.js b/server/config/custom.js
index 8d8043c2..4d21b642 100644
--- a/server/config/custom.js
+++ b/server/config/custom.js
@@ -30,4 +30,14 @@ module.exports.custom = {
attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'),
attachmentsUrl: `${process.env.BASE_URL}/attachments`,
+
+ oidcIssuer: process.env.OIDC_ISSUER,
+ oidcAudience: process.env.OIDC_AUDIENCE,
+ oidcClientId: process.env.OIDC_CLIENT_ID,
+ oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
+ oidcAdminRoles: process.env.OIDC_ADMIN_ROLES.split(',') || [],
+ oidcredirectUri: process.env.OIDC_REDIRECT_URI,
+ oidcJwksUri: process.env.OIDC_JWKS_URI,
+ oidcScopes: process.env.OIDC_SCOPES || 'openid profile email',
+ oidcSkipUserInfo: process.env.OIDC_SKIP_USER_INFO === 'true',
};
diff --git a/server/config/policies.js b/server/config/policies.js
index 9095e72a..272355c4 100644
--- a/server/config/policies.js
+++ b/server/config/policies.js
@@ -24,4 +24,6 @@ module.exports.policies = {
'projects/create': ['is-authenticated', 'is-admin'],
'access-tokens/create': true,
+ 'access-tokens/exchange': true,
+ 'appconfig/index': true,
};
diff --git a/server/config/routes.js b/server/config/routes.js
index 2453f918..9f380655 100644
--- a/server/config/routes.js
+++ b/server/config/routes.js
@@ -9,7 +9,10 @@
*/
module.exports.routes = {
+ 'GET /api/appconfig': 'appconfig/index',
+
'POST /api/access-tokens': 'access-tokens/create',
+ 'POST /api/access-tokens/exchange': 'access-tokens/exchange',
'DELETE /api/access-tokens/me': 'access-tokens/delete',
'GET /api/users': 'users/index',
diff --git a/server/db/migrations/20230809022050_create_identity_provider_user.js b/server/db/migrations/20230809022050_create_identity_provider_user.js
new file mode 100644
index 00000000..615cf09a
--- /dev/null
+++ b/server/db/migrations/20230809022050_create_identity_provider_user.js
@@ -0,0 +1,24 @@
+module.exports.up = (knex) =>
+ knex.schema.createTable('identity_provider_user', (table) => {
+ /* Columns */
+
+ table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
+ table.timestamp('created_at', true);
+ table.timestamp('updated_at', true);
+
+ table
+ .bigInteger('user_id')
+ .notNullable()
+ .references('id')
+ .inTable('user_account')
+ .onDelete('CASCADE');
+
+ table.text('issuer').notNullable();
+ table.text('sub').notNullable();
+
+ /* Indexes */
+
+ table.index('user_id');
+ });
+
+module.exports.down = (knex) => knex.schema.dropTable('identity_provider_user');
diff --git a/server/db/migrations/20230809024146_all_null_password_field.js b/server/db/migrations/20230809024146_all_null_password_field.js
new file mode 100644
index 00000000..c86529f1
--- /dev/null
+++ b/server/db/migrations/20230809024146_all_null_password_field.js
@@ -0,0 +1,11 @@
+module.exports.up = async (knex) => {
+ return knex.schema.table('user_account', (table) => {
+ table.setNullable('password');
+ });
+};
+
+module.exports.down = async (knex) => {
+ return knex.schema.table('user_account', (table) => {
+ table.dropNullable('password');
+ });
+};
diff --git a/server/db/migrations/20230809025904_add_lock_to_user_account.js b/server/db/migrations/20230809025904_add_lock_to_user_account.js
new file mode 100644
index 00000000..07c341f1
--- /dev/null
+++ b/server/db/migrations/20230809025904_add_lock_to_user_account.js
@@ -0,0 +1,11 @@
+module.exports.up = async (knex) => {
+ return knex.schema.table('user_account', (table) => {
+ table.boolean('locked').default(false);
+ });
+};
+
+module.exports.down = async (knex) => {
+ return knex.schema.table('user_account', (table) => {
+ table.dropColumn('locked');
+ });
+};
diff --git a/server/package-lock.json b/server/package-lock.json
index a37b0ff2..036a9b0f 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -11,10 +11,12 @@
"dotenv-cli": "^6.0.0",
"filenamify": "^4.3.0",
"jsonwebtoken": "^9.0.0",
+ "jwks-rsa": "^3.0.1",
"knex": "^2.3.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"move-file": "^2.1.0",
+ "openid-client": "^5.4.3",
"rimraf": "^3.0.2",
"sails": "^1.5.3",
"sails-hook-orm": "^4.0.2",
@@ -198,12 +200,103 @@
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
},
+ "node_modules/@types/body-parser": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+ "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.35",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+ "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.17",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
+ "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.17.35",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz",
+ "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ=="
+ },
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
+ "node_modules/@types/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
+ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
+ },
+ "node_modules/@types/node": {
+ "version": "20.4.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz",
+ "integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg=="
+ },
+ "node_modules/@types/qs": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
+ },
+ "node_modules/@types/send": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz",
+ "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz",
+ "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/mime": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -3634,6 +3727,14 @@
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
},
+ "node_modules/jose": {
+ "version": "4.14.4",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz",
+ "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-sdsl": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
@@ -3709,6 +3810,22 @@
"safe-buffer": "^5.0.1"
}
},
+ "node_modules/jwks-rsa": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz",
+ "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==",
+ "dependencies": {
+ "@types/express": "^4.17.14",
+ "@types/jsonwebtoken": "^9.0.0",
+ "debug": "^4.3.4",
+ "jose": "^4.10.4",
+ "limiter": "^1.1.5",
+ "lru-memoizer": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
@@ -3839,6 +3956,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/limiter": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
+ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
+ },
"node_modules/localforage": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.3.0.tgz",
@@ -3867,6 +3989,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
+ },
"node_modules/lodash.issafeinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.issafeinteger/-/lodash.issafeinteger-4.0.4.tgz",
@@ -3926,6 +4053,29 @@
"node": ">=10"
}
},
+ "node_modules/lru-memoizer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz",
+ "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==",
+ "dependencies": {
+ "lodash.clonedeep": "^4.5.0",
+ "lru-cache": "~4.0.0"
+ }
+ },
+ "node_modules/lru-memoizer/node_modules/lru-cache": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz",
+ "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==",
+ "dependencies": {
+ "pseudomap": "^1.0.1",
+ "yallist": "^2.0.0"
+ }
+ },
+ "node_modules/lru-memoizer/node_modules/yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
+ },
"node_modules/machine": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/machine/-/machine-15.2.2.tgz",
@@ -4546,6 +4696,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
@@ -4625,6 +4783,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/oidc-token-hash": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
+ "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==",
+ "engines": {
+ "node": "^10.13.0 || >=12.0.0"
+ }
+ },
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -4660,6 +4826,20 @@
"fn.name": "1.x.x"
}
},
+ "node_modules/openid-client": {
+ "version": "5.4.3",
+ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.3.tgz",
+ "integrity": "sha512-sVQOvjsT/sbSfYsQI/9liWQGVZH/Pp3rrtlGEwgk/bbHfrUDZ24DN57lAagIwFtuEu+FM9Ev7r85s8S/yPjimQ==",
+ "dependencies": {
+ "jose": "^4.14.4",
+ "lru-cache": "^6.0.0",
+ "object-hash": "^2.2.0",
+ "oidc-token-hash": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/opn": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
@@ -8039,12 +8219,103 @@
}
}
},
+ "@types/body-parser": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+ "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+ "requires": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/connect": {
+ "version": "3.4.35",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+ "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/express": {
+ "version": "4.17.17",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
+ "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==",
+ "requires": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "@types/express-serve-static-core": {
+ "version": "4.17.35",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz",
+ "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==",
+ "requires": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "@types/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ=="
+ },
"@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
+ "@types/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/mime": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
+ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
+ },
+ "@types/node": {
+ "version": "20.4.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz",
+ "integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg=="
+ },
+ "@types/qs": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
+ },
+ "@types/range-parser": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
+ },
+ "@types/send": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz",
+ "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==",
+ "requires": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "@types/serve-static": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz",
+ "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==",
+ "requires": {
+ "@types/http-errors": "*",
+ "@types/mime": "*",
+ "@types/node": "*"
+ }
+ },
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -10706,6 +10977,11 @@
}
}
},
+ "jose": {
+ "version": "4.14.4",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz",
+ "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g=="
+ },
"js-sdsl": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
@@ -10771,6 +11047,19 @@
"safe-buffer": "^5.0.1"
}
},
+ "jwks-rsa": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz",
+ "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==",
+ "requires": {
+ "@types/express": "^4.17.14",
+ "@types/jsonwebtoken": "^9.0.0",
+ "debug": "^4.3.4",
+ "jose": "^4.10.4",
+ "limiter": "^1.1.5",
+ "lru-memoizer": "^2.1.4"
+ }
+ },
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
@@ -10861,6 +11150,11 @@
}
}
},
+ "limiter": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
+ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
+ },
"localforage": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.3.0.tgz",
@@ -10883,6 +11177,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
+ },
"lodash.issafeinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.issafeinteger/-/lodash.issafeinteger-4.0.4.tgz",
@@ -10933,6 +11232,31 @@
"yallist": "^4.0.0"
}
},
+ "lru-memoizer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz",
+ "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==",
+ "requires": {
+ "lodash.clonedeep": "^4.5.0",
+ "lru-cache": "~4.0.0"
+ },
+ "dependencies": {
+ "lru-cache": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz",
+ "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==",
+ "requires": {
+ "pseudomap": "^1.0.1",
+ "yallist": "^2.0.0"
+ }
+ },
+ "yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
+ }
+ }
+ },
"machine": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/machine/-/machine-15.2.2.tgz",
@@ -11406,6 +11730,11 @@
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
+ "object-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="
+ },
"object-inspect": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
@@ -11461,6 +11790,11 @@
"es-abstract": "^1.20.4"
}
},
+ "oidc-token-hash": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
+ "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw=="
+ },
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -11490,6 +11824,17 @@
"fn.name": "1.x.x"
}
},
+ "openid-client": {
+ "version": "5.4.3",
+ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.3.tgz",
+ "integrity": "sha512-sVQOvjsT/sbSfYsQI/9liWQGVZH/Pp3rrtlGEwgk/bbHfrUDZ24DN57lAagIwFtuEu+FM9Ev7r85s8S/yPjimQ==",
+ "requires": {
+ "jose": "^4.14.4",
+ "lru-cache": "^6.0.0",
+ "object-hash": "^2.2.0",
+ "oidc-token-hash": "^5.0.3"
+ }
+ },
"opn": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
diff --git a/server/package.json b/server/package.json
index fd16000e..ff3980dd 100644
--- a/server/package.json
+++ b/server/package.json
@@ -32,10 +32,12 @@
"dotenv-cli": "^6.0.0",
"filenamify": "^4.3.0",
"jsonwebtoken": "^9.0.0",
+ "jwks-rsa": "^3.0.1",
"knex": "^2.3.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"move-file": "^2.1.0",
+ "openid-client": "^5.4.3",
"rimraf": "^3.0.2",
"sails": "^1.5.3",
"sails-hook-orm": "^4.0.2",