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",