diff --git a/client/package-lock.json b/client/package-lock.json
index 33a2ea2b..63bb66c1 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -19,6 +19,7 @@
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"node-sass": "^8.0.0",
+ "oidc-client-ts": "^2.3.0",
"photoswipe": "^5.3.3",
"prop-types": "^15.8.1",
"react": "^18.2.0",
@@ -30,7 +31,6 @@
"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",
@@ -7193,8 +7193,7 @@
"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
+ "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
},
"node_modules/crypto-random-string": {
"version": "2.0.0",
@@ -17043,10 +17042,9 @@
"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,
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.3.0.tgz",
+ "integrity": "sha512-7RUKU+TJFQo+4X9R50IGJAIDF18uRBaFXyZn4VVCfwmwbSUhKcdDnw4zgeut3uEXkiD3NqURq+d88sDPxjf1FA==",
"dependencies": {
"crypto-js": "^4.1.1",
"jwt-decode": "^3.1.2"
@@ -19296,18 +19294,6 @@
"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",
@@ -28802,8 +28788,7 @@
"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
+ "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
},
"crypto-random-string": {
"version": "2.0.0",
@@ -35972,10 +35957,9 @@
"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,
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.3.0.tgz",
+ "integrity": "sha512-7RUKU+TJFQo+4X9R50IGJAIDF18uRBaFXyZn4VVCfwmwbSUhKcdDnw4zgeut3uEXkiD3NqURq+d88sDPxjf1FA==",
"requires": {
"crypto-js": "^4.1.1",
"jwt-decode": "^3.1.2"
@@ -37419,12 +37403,6 @@
}
}
},
- "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 ad8303bb..1a970362 100755
--- a/client/package.json
+++ b/client/package.json
@@ -66,6 +66,7 @@
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"node-sass": "^8.0.0",
+ "oidc-client-ts": "^2.3.0",
"photoswipe": "^5.3.3",
"prop-types": "^15.8.1",
"react": "^18.2.0",
@@ -77,7 +78,6 @@
"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/actions/core.js b/client/src/actions/core.js
index bed09c3d..81dbbba0 100644
--- a/client/src/actions/core.js
+++ b/client/src/actions/core.js
@@ -1,6 +1,7 @@
import ActionTypes from '../constants/ActionTypes';
const initializeCore = (
+ config,
user,
board,
users,
@@ -20,6 +21,7 @@ const initializeCore = (
) => ({
type: ActionTypes.CORE_INITIALIZE,
payload: {
+ config,
user,
board,
users,
diff --git a/client/src/actions/login.js b/client/src/actions/login.js
index 8ffcded1..d7304c27 100644
--- a/client/src/actions/login.js
+++ b/client/src/actions/login.js
@@ -1,5 +1,12 @@
import ActionTypes from '../constants/ActionTypes';
+const initializeLogin = (config) => ({
+ type: ActionTypes.LOGIN_INITIALIZE,
+ payload: {
+ config,
+ },
+});
+
const authenticate = (data) => ({
type: ActionTypes.AUTHENTICATE,
payload: {
@@ -21,12 +28,33 @@ authenticate.failure = (error) => ({
},
});
+const authenticateWithOidc = () => ({
+ type: ActionTypes.WITH_OIDC_AUTHENTICATE,
+ payload: {},
+});
+
+authenticateWithOidc.success = (accessToken) => ({
+ type: ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS,
+ payload: {
+ accessToken,
+ },
+});
+
+authenticateWithOidc.failure = (error) => ({
+ type: ActionTypes.WITH_OIDC_AUTHENTICATE__FAILURE,
+ payload: {
+ error,
+ },
+});
+
const clearAuthenticateError = () => ({
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
payload: {},
});
export default {
+ initializeLogin,
authenticate,
+ authenticateWithOidc,
clearAuthenticateError,
};
diff --git a/client/src/api/access-tokens.js b/client/src/api/access-tokens.js
index 0a1dcf94..2c538272 100755
--- a/client/src/api/access-tokens.js
+++ b/client/src/api/access-tokens.js
@@ -4,14 +4,15 @@ 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 exchangeToAccessToken = (data, headers) =>
+ http.post('/access-tokens/exchange', data, headers);
const deleteCurrentAccessToken = (headers) =>
socket.delete('/access-tokens/me', undefined, headers);
export default {
createAccessToken,
+ exchangeToAccessToken,
deleteCurrentAccessToken,
- exchangeOidcToken,
};
diff --git a/client/src/api/http.js b/client/src/api/http.js
index 646528da..2119de2b 100755
--- a/client/src/api/http.js
+++ b/client/src/api/http.js
@@ -5,13 +5,15 @@ import Config from '../constants/Config';
const http = {};
// TODO: add all methods
-['POST'].forEach((method) => {
+['GET', 'POST'].forEach((method) => {
http[method.toLowerCase()] = (url, data, headers) => {
- const formData = Object.keys(data).reduce((result, key) => {
- result.append(key, data[key]);
+ const formData =
+ data &&
+ Object.keys(data).reduce((result, key) => {
+ result.append(key, data[key]);
- return result;
- }, new FormData());
+ return result;
+ }, new FormData());
return fetch(`${Config.SERVER_BASE_URL}/api${url}`, {
method,
diff --git a/client/src/api/index.js b/client/src/api/index.js
index fd9db7c0..dc4ed4d7 100755
--- a/client/src/api/index.js
+++ b/client/src/api/index.js
@@ -1,5 +1,6 @@
import http from './http';
import socket from './socket';
+import root from './root';
import accessTokens from './access-tokens';
import users from './users';
import projects from './projects';
@@ -20,6 +21,7 @@ import notifications from './notifications';
export { http, socket };
export default {
+ ...root,
...accessTokens,
...users,
...projects,
diff --git a/client/src/api/root.js b/client/src/api/root.js
new file mode 100644
index 00000000..4dfde4c0
--- /dev/null
+++ b/client/src/api/root.js
@@ -0,0 +1,9 @@
+import http from './http';
+
+/* Actions */
+
+const getConfig = (headers) => http.get('/config', undefined, headers);
+
+export default {
+ getConfig,
+};
diff --git a/client/src/components/Header/Header.jsx b/client/src/components/Header/Header.jsx
index 0bb246e0..443d3a88 100755
--- a/client/src/components/Header/Header.jsx
+++ b/client/src/components/Header/Header.jsx
@@ -3,7 +3,6 @@ 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';
@@ -30,7 +29,6 @@ const Header = React.memo(
onUserSettingsClick,
onLogout,
}) => {
- const auth = useAuth();
const handleProjectSettingsClick = useCallback(() => {
if (canEditProject) {
onProjectSettingsClick();
@@ -40,11 +38,6 @@ const Header = React.memo(
const NotificationsPopup = usePopup(NotificationsStep, POPUP_PROPS);
const UserPopup = usePopup(UserStep, POPUP_PROPS);
- const onFullLogout = () => {
- auth.signoutSilent();
- onLogout();
- };
-
return (
{!project && (
@@ -95,7 +88,7 @@ const Header = React.memo(
{user.name}
diff --git a/client/src/components/Login/Login.jsx b/client/src/components/Login/Login.jsx
index ce09c3d4..2b380a8e 100755
--- a/client/src/components/Login/Login.jsx
+++ b/client/src/components/Login/Login.jsx
@@ -1,10 +1,9 @@
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';
-import { Form, Grid, Header, Message } from 'semantic-ui-react';
+import { Button, Form, Grid, Header, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input } from '../../lib/custom-ui';
@@ -29,6 +28,21 @@ const createMessage = (error) => {
type: 'error',
content: 'common.invalidPassword',
};
+ case 'Use single sign-on':
+ return {
+ type: 'error',
+ content: 'common.useSingleSignOn',
+ };
+ case 'Email already in use':
+ return {
+ type: 'error',
+ content: 'common.emailAlreadyInUse',
+ };
+ case 'Username already in use':
+ return {
+ type: 'error',
+ content: 'common.usernameAlreadyInUse',
+ };
case 'Failed to fetch':
return {
type: 'warning',
@@ -48,8 +62,16 @@ const createMessage = (error) => {
};
const Login = React.memo(
- ({ defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss }) => {
- const auth = useAuth();
+ ({
+ defaultData,
+ isSubmitting,
+ isSubmittingWithOidc,
+ error,
+ withOidc,
+ onAuthenticate,
+ onAuthenticateWithOidc,
+ onMessageDismiss,
+ }) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@@ -170,12 +192,19 @@ const Login = React.memo(
content={t('action.logIn')}
floated="right"
loading={isSubmitting}
- disabled={isSubmitting}
+ disabled={isSubmitting || isSubmittingWithOidc}
/>
- auth.signinRedirect()}>
- Log in with SSO
-
+ {withOidc && (
+
+ )}
@@ -206,10 +235,15 @@ const Login = React.memo(
);
Login.propTypes = {
- defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
+ /* eslint-disable react/forbid-prop-types */
+ defaultData: PropTypes.object.isRequired,
+ /* eslint-enable react/forbid-prop-types */
isSubmitting: PropTypes.bool.isRequired,
+ isSubmittingWithOidc: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
+ withOidc: PropTypes.bool.isRequired,
onAuthenticate: PropTypes.func.isRequired,
+ onAuthenticateWithOidc: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
};
diff --git a/client/src/components/LoginWrapper.jsx b/client/src/components/LoginWrapper.jsx
new file mode 100644
index 00000000..f437fdf8
--- /dev/null
+++ b/client/src/components/LoginWrapper.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Loader } from 'semantic-ui-react';
+
+import LoginContainer from '../containers/LoginContainer';
+
+const LoginWrapper = React.memo(({ isInitializing }) => {
+ if (isInitializing) {
+ return ;
+ }
+
+ return ;
+});
+
+LoginWrapper.propTypes = {
+ isInitializing: PropTypes.bool.isRequired,
+};
+
+export default LoginWrapper;
diff --git a/client/src/components/OIDC/OidcLogin.jsx b/client/src/components/OIDC/OidcLogin.jsx
deleted file mode 100644
index 9ccd5e8f..00000000
--- a/client/src/components/OIDC/OidcLogin.jsx
+++ /dev/null
@@ -1,19 +0,0 @@
-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
deleted file mode 100644
index ef357747..00000000
--- a/client/src/components/OIDC/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import OidcLogin from './OidcLogin';
-
-export default OidcLogin;
diff --git a/client/src/components/Root.jsx b/client/src/components/Root.jsx
index d59b47d3..630a20d0 100755
--- a/client/src/components/Root.jsx
+++ b/client/src/components/Root.jsx
@@ -1,12 +1,11 @@
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';
import Paths from '../constants/Paths';
-import LoginContainer from '../containers/LoginContainer';
+import LoginWrapperContainer from '../containers/LoginWrapperContainer';
import CoreContainer from '../containers/CoreContainer';
import NotFound from './NotFound';
@@ -15,40 +14,29 @@ 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, config }) {
+function Root({ store, history }) {
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/components/UserSettingsModal/UserSettingsModal.jsx b/client/src/components/UserSettingsModal/UserSettingsModal.jsx
index 6cc0b826..fc8ac9c7 100644
--- a/client/src/components/UserSettingsModal/UserSettingsModal.jsx
+++ b/client/src/components/UserSettingsModal/UserSettingsModal.jsx
@@ -16,8 +16,8 @@ const UserSettingsModal = React.memo(
phone,
organization,
language,
- isLocked,
subscribeToOwnCards,
+ isLocked,
isAvatarUpdating,
usernameUpdateForm,
emailUpdateForm,
@@ -106,8 +106,8 @@ UserSettingsModal.propTypes = {
phone: PropTypes.string,
organization: PropTypes.string,
language: PropTypes.string,
- isLocked: PropTypes.bool.isRequired,
subscribeToOwnCards: PropTypes.bool.isRequired,
+ isLocked: PropTypes.bool.isRequired,
isAvatarUpdating: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
usernameUpdateForm: PropTypes.object.isRequired,
diff --git a/client/src/components/UsersModal/Item/ActionsStep.jsx b/client/src/components/UsersModal/Item/ActionsStep.jsx
index a97d13a8..88c089ac 100644
--- a/client/src/components/UsersModal/Item/ActionsStep.jsx
+++ b/client/src/components/UsersModal/Item/ActionsStep.jsx
@@ -153,13 +153,15 @@ const ActionsStep = React.memo(
context: 'title',
})}
-
- {t('action.deleteUser', {
- context: 'title',
- })}
-
>
)}
+ {!user.isLockedAdmin && (
+
+ {t('action.deleteUser', {
+ context: 'title',
+ })}
+
+ )}
>
diff --git a/client/src/components/UsersModal/Item/Item.jsx b/client/src/components/UsersModal/Item/Item.jsx
index 0627baf6..da73c5c8 100755
--- a/client/src/components/UsersModal/Item/Item.jsx
+++ b/client/src/components/UsersModal/Item/Item.jsx
@@ -18,6 +18,7 @@ const Item = React.memo(
phone,
isAdmin,
isLocked,
+ isLockedAdmin,
emailUpdateForm,
passwordUpdateForm,
usernameUpdateForm,
@@ -47,7 +48,7 @@ const Item = React.memo(
{username || '-'}
{email}
-
+
{
- const isCoreInitializing = selectors.selectIsCoreInitializing(state);
+ const isInitializing = selectors.selectIsInitializing(state);
const isSocketDisconnected = selectors.selectIsSocketDisconnected(state);
const currentModal = selectors.selectCurrentModal(state);
const currentProject = selectors.selectCurrentProject(state);
const currentBoard = selectors.selectCurrentBoard(state);
return {
+ isInitializing,
isSocketDisconnected,
currentModal,
currentProject,
currentBoard,
- isInitializing: isCoreInitializing,
};
};
diff --git a/client/src/containers/LoginContainer.js b/client/src/containers/LoginContainer.js
index af25d0d5..34f15fdd 100755
--- a/client/src/containers/LoginContainer.js
+++ b/client/src/containers/LoginContainer.js
@@ -1,23 +1,33 @@
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
+import selectors from '../selectors';
import entryActions from '../entry-actions';
import Login from '../components/Login';
-const mapStateToProps = ({
- ui: {
- authenticateForm: { data: defaultData, isSubmitting, error },
- },
-}) => ({
- defaultData,
- isSubmitting,
- error,
-});
+const mapStateToProps = (state) => {
+ const oidcConfig = selectors.selectOidcConfig(state);
+
+ const {
+ ui: {
+ authenticateForm: { data: defaultData, isSubmitting, isSubmittingWithOidc, error },
+ },
+ } = state;
+
+ return {
+ defaultData,
+ isSubmitting,
+ isSubmittingWithOidc,
+ error,
+ withOidc: !!oidcConfig,
+ };
+};
const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
onAuthenticate: entryActions.authenticate,
+ onAuthenticateWithOidc: entryActions.authenticateWithOidc,
onMessageDismiss: entryActions.clearAuthenticateError,
},
dispatch,
diff --git a/client/src/containers/LoginWrapperContainer.js b/client/src/containers/LoginWrapperContainer.js
new file mode 100644
index 00000000..7bfa5923
--- /dev/null
+++ b/client/src/containers/LoginWrapperContainer.js
@@ -0,0 +1,14 @@
+import { connect } from 'react-redux';
+
+import selectors from '../selectors';
+import LoginWrapper from '../components/LoginWrapper';
+
+const mapStateToProps = (state) => {
+ const isInitializing = selectors.selectIsInitializing(state);
+
+ return {
+ isInitializing,
+ };
+};
+
+export default connect(mapStateToProps)(LoginWrapper);
diff --git a/client/src/containers/OidcLoginContainer.js b/client/src/containers/OidcLoginContainer.js
deleted file mode 100644
index 15083ad8..00000000
--- a/client/src/containers/OidcLoginContainer.js
+++ /dev/null
@@ -1,26 +0,0 @@
-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/containers/UserSettingsModalContainer.js b/client/src/containers/UserSettingsModalContainer.js
index 1ce3277b..620632e0 100644
--- a/client/src/containers/UserSettingsModalContainer.js
+++ b/client/src/containers/UserSettingsModalContainer.js
@@ -14,8 +14,8 @@ const mapStateToProps = (state) => {
phone,
organization,
language,
- isLocked,
subscribeToOwnCards,
+ isLocked,
isAvatarUpdating,
emailUpdateForm,
passwordUpdateForm,
@@ -30,8 +30,8 @@ const mapStateToProps = (state) => {
phone,
organization,
language,
- isLocked,
subscribeToOwnCards,
+ isLocked,
isAvatarUpdating,
emailUpdateForm,
passwordUpdateForm,
diff --git a/client/src/entry-actions/core.js b/client/src/entry-actions/core.js
index f76395c2..c1e03fff 100644
--- a/client/src/entry-actions/core.js
+++ b/client/src/entry-actions/core.js
@@ -1,16 +1,10 @@
import EntryActionTypes from '../constants/EntryActionTypes';
-const initializeCore = () => ({
- type: EntryActionTypes.CORE_INITIALIZE,
- payload: {},
-});
-
const logout = () => ({
type: EntryActionTypes.LOGOUT,
payload: {},
});
export default {
- initializeCore,
logout,
};
diff --git a/client/src/entry-actions/login.js b/client/src/entry-actions/login.js
index db0d37dd..89769217 100755
--- a/client/src/entry-actions/login.js
+++ b/client/src/entry-actions/login.js
@@ -7,6 +7,11 @@ const authenticate = (data) => ({
},
});
+const authenticateWithOidc = () => ({
+ type: EntryActionTypes.WITH_OIDC_AUTHENTICATE,
+ payload: {},
+});
+
const clearAuthenticateError = () => ({
type: EntryActionTypes.AUTHENTICATE_ERROR_CLEAR,
payload: {},
@@ -14,5 +19,6 @@ const clearAuthenticateError = () => ({
export default {
authenticate,
+ authenticateWithOidc,
clearAuthenticateError,
};
diff --git a/client/src/index.js b/client/src/index.js
index ef5afbad..574cafee 100755
--- a/client/src/index.js
+++ b/client/src/index.js
@@ -1,15 +1,11 @@
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';
-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 }));
- });
-});
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(React.createElement(Root, { store, history }));
diff --git a/client/src/locales/en/login.js b/client/src/locales/en/login.js
index 598c28ea..5e7f10c3 100644
--- a/client/src/locales/en/login.js
+++ b/client/src/locales/en/login.js
@@ -11,10 +11,12 @@ export default {
projectManagement: 'Project management',
serverConnectionFailed: 'Server connection failed',
unknownError: 'Unknown error, try again later',
+ useSingleSignOn: 'Use single sign-on',
},
action: {
logIn: 'Log in',
+ logInWithSSO: 'Log in with SSO',
},
},
};
diff --git a/client/src/models/User.js b/client/src/models/User.js
index 39a4330f..cce2a78a 100755
--- a/client/src/models/User.js
+++ b/client/src/models/User.js
@@ -43,16 +43,13 @@ export default class extends BaseModel {
organization: attr(),
language: attr(),
subscribeToOwnCards: attr(),
+ isAdmin: attr(),
+ isLocked: attr(),
+ isLockedAdmin: attr(),
+ deletedAt: attr(),
createdAt: attr({
getDefault: () => new Date(),
}),
- deletedAt: attr(),
- isAdmin: attr({
- getDefault: () => false,
- }),
- isLocked: attr({
- getDefault: () => false,
- }),
isAvatarUpdating: attr({
getDefault: () => false,
}),
diff --git a/client/src/reducers/auth.js b/client/src/reducers/auth.js
index 9ca6ae52..8af35d46 100755
--- a/client/src/reducers/auth.js
+++ b/client/src/reducers/auth.js
@@ -10,6 +10,7 @@ const initialState = {
export default (state = initialState, { type, payload }) => {
switch (type) {
case ActionTypes.AUTHENTICATE__SUCCESS:
+ case ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS:
return {
...state,
accessToken: payload.accessToken,
diff --git a/client/src/reducers/core.js b/client/src/reducers/core.js
index af67a3f1..94d73a52 100755
--- a/client/src/reducers/core.js
+++ b/client/src/reducers/core.js
@@ -4,7 +4,6 @@ import ActionTypes from '../constants/ActionTypes';
import ModalTypes from '../constants/ModalTypes';
const initialState = {
- isInitializing: true,
isLogouting: false,
currentModal: null,
};
@@ -18,11 +17,6 @@ export default (state = initialState, { type, payload }) => {
...state,
currentModal: null,
};
- case ActionTypes.CORE_INITIALIZE:
- return {
- ...state,
- isInitializing: false,
- };
case ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE:
return {
...state,
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index 6d90bbac..83d62e55 100755
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -3,6 +3,7 @@ import { combineReducers } from 'redux';
import router from './router';
import socket from './socket';
import orm from './orm';
+import root from './root';
import auth from './auth';
import core from './core';
import ui from './ui';
@@ -11,6 +12,7 @@ export default combineReducers({
router,
socket,
orm,
+ root,
auth,
core,
ui,
diff --git a/client/src/reducers/root.js b/client/src/reducers/root.js
new file mode 100644
index 00000000..d11c5846
--- /dev/null
+++ b/client/src/reducers/root.js
@@ -0,0 +1,34 @@
+import ActionTypes from '../constants/ActionTypes';
+
+const initialState = {
+ isInitializing: true,
+ config: null,
+};
+
+// eslint-disable-next-line default-param-last
+export default (state = initialState, { type, payload }) => {
+ switch (type) {
+ case ActionTypes.LOGIN_INITIALIZE:
+ return {
+ ...state,
+ isInitializing: false,
+ config: payload.config,
+ };
+ case ActionTypes.AUTHENTICATE__SUCCESS:
+ case ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS:
+ return {
+ ...state,
+ isInitializing: true,
+ };
+ case ActionTypes.CORE_INITIALIZE:
+ return {
+ ...state,
+ isInitializing: false,
+ ...(payload.config && {
+ config: payload.config,
+ }),
+ };
+ default:
+ return state;
+ }
+};
diff --git a/client/src/reducers/ui/authenticate-form.js b/client/src/reducers/ui/authenticate-form.js
index 163c4dd4..4a0414c0 100644
--- a/client/src/reducers/ui/authenticate-form.js
+++ b/client/src/reducers/ui/authenticate-form.js
@@ -1,4 +1,7 @@
+import { LOCATION_CHANGE_HANDLE } from '../../lib/redux-router';
+
import ActionTypes from '../../constants/ActionTypes';
+import Paths from '../../constants/Paths';
const initialState = {
data: {
@@ -6,12 +9,22 @@ const initialState = {
password: '',
},
isSubmitting: false,
+ isSubmittingWithOidc: false,
error: null,
};
// eslint-disable-next-line default-param-last
export default (state = initialState, { type, payload }) => {
switch (type) {
+ case LOCATION_CHANGE_HANDLE:
+ if (payload.location.pathname === Paths.OIDC_CALLBACK) {
+ return {
+ ...state,
+ isSubmittingWithOidc: true,
+ };
+ }
+
+ return state;
case ActionTypes.AUTHENTICATE:
return {
...state,
@@ -22,6 +35,7 @@ export default (state = initialState, { type, payload }) => {
isSubmitting: true,
};
case ActionTypes.AUTHENTICATE__SUCCESS:
+ case ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS:
return initialState;
case ActionTypes.AUTHENTICATE__FAILURE:
return {
@@ -29,6 +43,12 @@ export default (state = initialState, { type, payload }) => {
isSubmitting: false,
error: payload.error,
};
+ case ActionTypes.WITH_OIDC_AUTHENTICATE__FAILURE:
+ return {
+ ...state,
+ isSubmittingWithOidc: false,
+ error: payload.error,
+ };
case ActionTypes.AUTHENTICATE_ERROR_CLEAR:
return {
...state,
diff --git a/client/src/sagas/core/services/core.js b/client/src/sagas/core/services/core.js
index edf4c01a..30389d6c 100644
--- a/client/src/sagas/core/services/core.js
+++ b/client/src/sagas/core/services/core.js
@@ -1,13 +1,22 @@
-import { call, put, take } from 'redux-saga/effects';
+import { apply, call, put, select, take } from 'redux-saga/effects';
import request from '../request';
import requests from '../requests';
+import selectors from '../../../selectors';
import actions from '../../../actions';
import api from '../../../api';
import i18n from '../../../i18n';
+import { createOidcManager } from '../../../utils/oidc-manager';
import { removeAccessToken } from '../../../utils/access-token-storage';
export function* initializeCore() {
+ const currentConfig = yield select(selectors.selectConfig); // TODO: add boolean selector?
+
+ let config;
+ if (!currentConfig) {
+ ({ item: config } = yield call(api.getConfig)); // TODO: handle error
+ }
+
const {
user,
board,
@@ -32,6 +41,7 @@ export function* initializeCore() {
yield put(
actions.initializeCore(
+ config,
user,
board,
users,
@@ -74,6 +84,16 @@ export function* logout(invalidateAccessToken = true) {
} catch (error) {} // eslint-disable-line no-empty
}
+ const oidcConfig = yield select(selectors.selectOidcConfig);
+
+ if (oidcConfig) {
+ const oidcManager = createOidcManager(oidcConfig);
+
+ try {
+ yield apply(oidcManager, oidcManager.logout);
+ } catch (error) {} // eslint-disable-line no-empty
+ }
+
yield put(actions.logout());
yield take();
}
diff --git a/client/src/sagas/core/services/router.js b/client/src/sagas/core/services/router.js
index 2585dc01..7b981e49 100644
--- a/client/src/sagas/core/services/router.js
+++ b/client/src/sagas/core/services/router.js
@@ -33,15 +33,16 @@ export function* handleLocationChange() {
switch (pathsMatch.pattern.path) {
case Paths.LOGIN:
+ case Paths.OIDC_CALLBACK:
yield call(goToRoot);
- break;
+ return;
default:
}
- const isCoreInitializing = yield select(selectors.selectIsCoreInitializing);
+ const isInitializing = yield select(selectors.selectIsInitializing);
- if (isCoreInitializing) {
+ if (isInitializing) {
yield take(ActionTypes.CORE_INITIALIZE);
}
diff --git a/client/src/sagas/core/watchers/core.js b/client/src/sagas/core/watchers/core.js
index 40485b86..cab47046 100644
--- a/client/src/sagas/core/watchers/core.js
+++ b/client/src/sagas/core/watchers/core.js
@@ -4,8 +4,5 @@ import services from '../services';
import EntryActionTypes from '../../../constants/EntryActionTypes';
export default function* coreWatchers() {
- yield all([
- takeEvery(EntryActionTypes.CORE_INITIALIZE, () => services.initializeCore()),
- takeEvery(EntryActionTypes.LOGOUT, () => services.logout()),
- ]);
+ yield all([takeEvery(EntryActionTypes.LOGOUT, () => services.logout())]);
}
diff --git a/client/src/sagas/login/index.js b/client/src/sagas/login/index.js
index d47b352d..f77fed9c 100755
--- a/client/src/sagas/login/index.js
+++ b/client/src/sagas/login/index.js
@@ -7,7 +7,9 @@ import ActionTypes from '../../constants/ActionTypes';
export default function* loginSaga() {
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
- yield take(ActionTypes.AUTHENTICATE__SUCCESS);
+ yield fork(services.initializeLogin);
+
+ yield take([ActionTypes.AUTHENTICATE__SUCCESS, ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS]);
yield cancel(watcherTasks);
yield call(services.goToRoot);
diff --git a/client/src/sagas/login/services/login.js b/client/src/sagas/login/services/login.js
index 75bbd7f6..04c029ad 100644
--- a/client/src/sagas/login/services/login.js
+++ b/client/src/sagas/login/services/login.js
@@ -1,19 +1,25 @@
-import { call, put } from 'redux-saga/effects';
+import { apply, call, put, select } from 'redux-saga/effects';
+import { replace } from '../../../lib/redux-router';
+import selectors from '../../../selectors';
import actions from '../../../actions';
import api from '../../../api';
+import { createOidcManager } from '../../../utils/oidc-manager';
import { setAccessToken } from '../../../utils/access-token-storage';
+import Paths from '../../../constants/Paths';
+
+export function* initializeLogin() {
+ const { item: config } = yield call(api.getConfig); // TODO: handle error
+
+ yield put(actions.initializeLogin(config));
+}
export function* authenticate(data) {
yield put(actions.authenticate(data));
- let accessToken = data.access_token;
+ let accessToken;
try {
- if (accessToken) {
- ({ item: accessToken } = yield call(api.exchangeOidcToken, accessToken));
- } else {
- ({ item: accessToken } = yield call(api.createAccessToken, data));
- }
+ ({ item: accessToken } = yield call(api.createAccessToken, data));
} catch (error) {
yield put(actions.authenticate.failure(error));
return;
@@ -23,11 +29,50 @@ export function* authenticate(data) {
yield put(actions.authenticate.success(accessToken));
}
+export function* authenticateWithOidc() {
+ const oidcConfig = yield select(selectors.selectOidcConfig);
+ const oidcManager = createOidcManager(oidcConfig);
+
+ yield apply(oidcManager, oidcManager.login);
+}
+
+export function* authenticateWithOidcCallback() {
+ const oidcConfig = yield select(selectors.selectOidcConfig);
+ const oidcManager = createOidcManager(oidcConfig);
+
+ let oidcToken;
+ try {
+ ({ access_token: oidcToken } = yield apply(oidcManager, oidcManager.loginCallback));
+ } catch (error) {
+ yield put(actions.authenticateWithOidc.failure(error));
+ }
+
+ yield put(replace(Paths.LOGIN));
+
+ if (oidcToken) {
+ let accessToken;
+ try {
+ ({ item: accessToken } = yield call(api.exchangeToAccessToken, {
+ token: oidcToken,
+ }));
+ } catch (error) {
+ yield put(actions.authenticateWithOidc.failure(error));
+ return;
+ }
+
+ yield call(setAccessToken, accessToken);
+ yield put(actions.authenticateWithOidc.success(accessToken));
+ }
+}
+
export function* clearAuthenticateError() {
yield put(actions.clearAuthenticateError());
}
export default {
+ initializeLogin,
authenticate,
+ authenticateWithOidc,
+ authenticateWithOidcCallback,
clearAuthenticateError,
};
diff --git a/client/src/sagas/login/services/router.js b/client/src/sagas/login/services/router.js
index 18ecd813..45656728 100644
--- a/client/src/sagas/login/services/router.js
+++ b/client/src/sagas/login/services/router.js
@@ -1,7 +1,9 @@
-import { call, put, select } from 'redux-saga/effects';
+import { call, put, select, take } from 'redux-saga/effects';
import { push } from '../../../lib/redux-router';
+import { authenticateWithOidcCallback } from './login';
import selectors from '../../../selectors';
+import ActionTypes from '../../../constants/ActionTypes';
import Paths from '../../../constants/Paths';
export function* goToLogin() {
@@ -27,6 +29,19 @@ export function* handleLocationChange() {
yield call(goToLogin);
break;
+ case Paths.OIDC_CALLBACK: {
+ const isInitializing = yield select(selectors.selectIsInitializing);
+
+ if (isInitializing) {
+ yield take(ActionTypes.LOGIN_INITIALIZE);
+ }
+
+ // TODO: check if OIDC is enabled
+
+ yield call(authenticateWithOidcCallback);
+
+ break;
+ }
default:
}
}
diff --git a/client/src/sagas/login/watchers/login.js b/client/src/sagas/login/watchers/login.js
index 9741b981..84aef934 100644
--- a/client/src/sagas/login/watchers/login.js
+++ b/client/src/sagas/login/watchers/login.js
@@ -8,6 +8,7 @@ export default function* loginWatchers() {
takeEvery(EntryActionTypes.AUTHENTICATE, ({ payload: { data } }) =>
services.authenticate(data),
),
+ takeEvery(EntryActionTypes.WITH_OIDC_AUTHENTICATE, () => services.authenticateWithOidc()),
takeEvery(EntryActionTypes.AUTHENTICATE_ERROR_CLEAR, () => services.clearAuthenticateError()),
]);
}
diff --git a/client/src/selectors/core.js b/client/src/selectors/core.js
index 0ae4663b..df7d42d5 100755
--- a/client/src/selectors/core.js
+++ b/client/src/selectors/core.js
@@ -6,8 +6,6 @@ import Config from '../constants/Config';
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
-export const selectIsCoreInitializing = ({ core: { isInitializing } }) => isInitializing;
-
export const selectIsLogouting = ({ core: { isLogouting } }) => isLogouting;
const nextPosition = (items, index, excludedId) => {
@@ -115,7 +113,6 @@ export const selectNextTaskPosition = createSelector(
export default {
selectAccessToken,
- selectIsCoreInitializing,
selectIsLogouting,
selectNextBoardPosition,
selectNextLabelPosition,
diff --git a/client/src/selectors/index.js b/client/src/selectors/index.js
index 7cfb1891..a674f062 100755
--- a/client/src/selectors/index.js
+++ b/client/src/selectors/index.js
@@ -1,5 +1,6 @@
import router from './router';
import socket from './socket';
+import root from './root';
import core from './core';
import modals from './modals';
import users from './users';
@@ -16,6 +17,7 @@ import attachments from './attachments';
export default {
...router,
...socket,
+ ...root,
...core,
...modals,
...users,
diff --git a/client/src/selectors/root.js b/client/src/selectors/root.js
new file mode 100644
index 00000000..2e485b49
--- /dev/null
+++ b/client/src/selectors/root.js
@@ -0,0 +1,11 @@
+export const selectIsInitializing = ({ root: { isInitializing } }) => isInitializing;
+
+export const selectConfig = ({ root: { config } }) => config;
+
+export const selectOidcConfig = (state) => selectConfig(state).oidc;
+
+export default {
+ selectIsInitializing,
+ selectConfig,
+ selectOidcConfig,
+};
diff --git a/client/src/utils/oidc-manager.js b/client/src/utils/oidc-manager.js
new file mode 100644
index 00000000..c7812052
--- /dev/null
+++ b/client/src/utils/oidc-manager.js
@@ -0,0 +1,27 @@
+import { UserManager } from 'oidc-client-ts';
+
+export default class OidcManager {
+ constructor(config) {
+ this.userManager = new UserManager({
+ ...config,
+ authority: config.issuer,
+ client_id: config.clientId,
+ redirect_uri: config.redirectUri,
+ scope: config.scopes,
+ });
+ }
+
+ login() {
+ return this.userManager.signinRedirect();
+ }
+
+ loginCallback() {
+ return this.userManager.signinRedirectCallback();
+ }
+
+ logout() {
+ return this.userManager.signoutSilent();
+ }
+}
+
+export const createOidcManager = (config) => new OidcManager(config);
diff --git a/docker-compose.yml b/docker-compose.yml
index c77790de..e0d030a5 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -37,6 +37,16 @@ services:
# Configure knex to accept SSL certificates
# - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
+
+ # - OIDC_ISSUER=
+ # - OIDC_CLIENT_ID=
+ # - OIDC_REDIRECT_URI=http://localhost:3000/oidc-callback
+ # - OIDC_SCOPES=openid email profile
+ # - OIDC_JWKS_URI=
+ # - OIDC_AUDIENCE=
+ # - OIDC_ADMIN_ROLES=admin
+ # - OIDC_ROLES_ATTRIBUTE=groups
+ # - OIDC_SKIP_USER_INFO=true
depends_on:
- postgres
diff --git a/server/.env.sample b/server/.env.sample
index 36de1985..76b30480 100644
--- a/server/.env.sample
+++ b/server/.env.sample
@@ -24,6 +24,16 @@ DEFAULT_ADMIN_USERNAME=demo
# Configure knex to accept SSL certificates
# KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
+# OIDC_ISSUER=
+# OIDC_CLIENT_ID=
+# OIDC_REDIRECT_URI=http://localhost:1337/oidc-callback
+# OIDC_SCOPES=openid email profile
+# OIDC_JWKS_URI=
+# OIDC_AUDIENCE=
+# OIDC_ADMIN_ROLES=admin
+# OIDC_ROLES_ATTRIBUTE=groups
+# OIDC_SKIP_USER_INFO=true
+
## Do not edit this
TZ=UTC
diff --git a/server/api/controllers/access-tokens/create.js b/server/api/controllers/access-tokens/create.js
index f98add91..0136a6c1 100755
--- a/server/api/controllers/access-tokens/create.js
+++ b/server/api/controllers/access-tokens/create.js
@@ -10,6 +10,9 @@ const Errors = {
INVALID_PASSWORD: {
invalidPassword: 'Invalid password',
},
+ USE_SINGLE_SIGN_ON: {
+ useSingleSignOn: 'Use single sign-on',
+ },
};
const emailOrUsernameValidator = (value) =>
@@ -37,6 +40,9 @@ module.exports = {
invalidPassword: {
responseType: 'unauthorized',
},
+ useSingleSignOn: {
+ responseType: 'forbidden',
+ },
},
async fn(inputs) {
@@ -44,6 +50,10 @@ module.exports = {
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
+ if (user.isSso) {
+ throw Errors.USE_SINGLE_SIGN_ON;
+ }
+
if (!user) {
sails.log.warn(
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${remoteAddress})`,
diff --git a/server/api/controllers/access-tokens/exchange.js b/server/api/controllers/access-tokens/exchange.js
index 04ba58d8..bd77c2cb 100644
--- a/server/api/controllers/access-tokens/exchange.js
+++ b/server/api/controllers/access-tokens/exchange.js
@@ -1,88 +1,20 @@
-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',
+ invalidToken: 'Invalid token',
+ },
+ EMAIL_ALREADY_IN_USE: {
+ emailAlreadyInUse: 'Email already in use',
+ },
+ USERNAME_ALREADY_IN_USE: {
+ usernameAlreadyInUse: 'Username already in use',
},
MISSING_VALUES: {
- missingValues:
- 'Unable to retrieve required values. Verify the access token or UserInfo endpoint has email, username and name claims',
+ missingValues: 'Unable to retrieve required values (email, name)',
},
};
-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: {
@@ -95,83 +27,41 @@ module.exports = {
invalidToken: {
responseType: 'unauthorized',
},
+ emailAlreadyInUse: {
+ responseType: 'conflict',
+ },
+ usernameAlreadyInUse: {
+ responseType: 'conflict',
+ },
missingValues: {
- responseType: 'unauthorized',
+ responseType: 'unprocessableEntity',
},
},
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);
+
+ const user = await sails.helpers.users
+ .getOrCreateOneByOidcToken(inputs.token)
+ .intercept('invalidToken', () => {
+ sails.log.warn(`Invalid token! (IP: ${remoteAddress})`);
+ return Errors.INVALID_TOKEN;
+ })
+ .intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE)
+ .intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE)
+ .intercept('missingValues', () => Errors.MISSING_VALUES);
+
+ const accessToken = sails.helpers.utils.createToken(user.id);
+
await Session.create({
- accessToken: plankaToken,
+ accessToken,
remoteAddress,
userId: user.id,
userAgent: this.req.headers['user-agent'],
});
return {
- item: plankaToken,
+ item: accessToken,
};
},
};
diff --git a/server/api/controllers/appconfig/index.js b/server/api/controllers/appconfig/index.js
deleted file mode 100644
index eef7be68..00000000
--- a/server/api/controllers/appconfig/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-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/controllers/show-config.js b/server/api/controllers/show-config.js
new file mode 100644
index 00000000..0273f93b
--- /dev/null
+++ b/server/api/controllers/show-config.js
@@ -0,0 +1,16 @@
+module.exports = {
+ fn() {
+ return {
+ item: {
+ oidc: sails.config.custom.oidcIssuer
+ ? {
+ issuer: sails.config.custom.oidcIssuer,
+ clientId: sails.config.custom.oidcClientId,
+ redirectUri: sails.config.custom.oidcRedirectUri,
+ scopes: sails.config.custom.oidcScopes,
+ }
+ : null,
+ },
+ };
+ },
+};
diff --git a/server/api/controllers/users/delete.js b/server/api/controllers/users/delete.js
index 46edb042..56a42f8b 100755
--- a/server/api/controllers/users/delete.js
+++ b/server/api/controllers/users/delete.js
@@ -1,4 +1,7 @@
const Errors = {
+ NOT_ENOUGH_RIGHTS: {
+ notEnoughRights: 'Not enough rights',
+ },
USER_NOT_FOUND: {
userNotFound: 'User not found',
},
@@ -14,6 +17,9 @@ module.exports = {
},
exits: {
+ notEnoughRights: {
+ responseType: 'forbidden',
+ },
userNotFound: {
responseType: 'notFound',
},
@@ -27,7 +33,7 @@ module.exports = {
}
if (user.email === sails.config.custom.defaultAdminEmail) {
- throw Errors.USER_NOT_FOUND; // Forbidden
+ throw Errors.NOT_ENOUGH_RIGHTS;
}
user = await sails.helpers.users.deleteOne.with({
diff --git a/server/api/controllers/users/update-email.js b/server/api/controllers/users/update-email.js
index 182e0c10..f067e004 100644
--- a/server/api/controllers/users/update-email.js
+++ b/server/api/controllers/users/update-email.js
@@ -1,6 +1,9 @@
const bcrypt = require('bcrypt');
const Errors = {
+ NOT_ENOUGH_RIGHTS: {
+ notEnoughRights: 'Not enough rights',
+ },
USER_NOT_FOUND: {
userNotFound: 'User not found',
},
@@ -31,6 +34,9 @@ module.exports = {
},
exits: {
+ notEnoughRights: {
+ responseType: 'forbidden',
+ },
userNotFound: {
responseType: 'notFound',
},
@@ -59,8 +65,8 @@ module.exports = {
throw Errors.USER_NOT_FOUND;
}
- if (user.email === sails.config.custom.defaultAdminEmail) {
- throw Errors.USER_NOT_FOUND; // Forbidden
+ if (user.email === sails.config.custom.defaultAdminEmail || user.isSso) {
+ throw Errors.NOT_ENOUGH_RIGHTS;
}
if (
diff --git a/server/api/controllers/users/update-password.js b/server/api/controllers/users/update-password.js
index c3d9c724..107c013e 100644
--- a/server/api/controllers/users/update-password.js
+++ b/server/api/controllers/users/update-password.js
@@ -4,6 +4,9 @@ const zxcvbn = require('zxcvbn');
const { getRemoteAddress } = require('../../../utils/remoteAddress');
const Errors = {
+ NOT_ENOUGH_RIGHTS: {
+ notEnoughRights: 'Not enough rights',
+ },
USER_NOT_FOUND: {
userNotFound: 'User not found',
},
@@ -33,6 +36,9 @@ module.exports = {
},
exits: {
+ notEnoughRights: {
+ responseType: 'forbidden',
+ },
userNotFound: {
responseType: 'notFound',
},
@@ -58,8 +64,8 @@ module.exports = {
throw Errors.USER_NOT_FOUND;
}
- if (user.email === sails.config.custom.defaultAdminEmail) {
- throw Errors.USER_NOT_FOUND; // Forbidden
+ if (user.email === sails.config.custom.defaultAdminEmail || user.isSso) {
+ throw Errors.NOT_ENOUGH_RIGHTS;
}
if (
diff --git a/server/api/controllers/users/update-username.js b/server/api/controllers/users/update-username.js
index 03bb1d3c..58059460 100644
--- a/server/api/controllers/users/update-username.js
+++ b/server/api/controllers/users/update-username.js
@@ -1,6 +1,9 @@
const bcrypt = require('bcrypt');
const Errors = {
+ NOT_ENOUGH_RIGHTS: {
+ notEnoughRights: 'Not enough rights',
+ },
USER_NOT_FOUND: {
userNotFound: 'User not found',
},
@@ -33,6 +36,9 @@ module.exports = {
},
exits: {
+ notEnoughRights: {
+ responseType: 'forbidden',
+ },
userNotFound: {
responseType: 'notFound',
},
@@ -61,8 +67,8 @@ module.exports = {
throw Errors.USER_NOT_FOUND;
}
- if (user.email === sails.config.custom.defaultAdminEmail) {
- throw Errors.USER_NOT_FOUND; // Forbidden
+ if (user.email === sails.config.custom.defaultAdminEmail || user.isSso) {
+ throw Errors.NOT_ENOUGH_RIGHTS;
}
if (
diff --git a/server/api/controllers/users/update.js b/server/api/controllers/users/update.js
index 7de8a730..c876efbd 100755
--- a/server/api/controllers/users/update.js
+++ b/server/api/controllers/users/update.js
@@ -72,6 +72,8 @@ module.exports = {
delete inputs.isAdmin;
delete inputs.name;
/* eslint-enable no-param-reassign */
+ } else if (user.isSso) {
+ delete inputs.name; // eslint-disable-line no-param-reassign
}
const values = {
diff --git a/server/api/helpers/users/create-one.js b/server/api/helpers/users/create-one.js
index 8df1d146..ddd07b94 100644
--- a/server/api/helpers/users/create-one.js
+++ b/server/api/helpers/users/create-one.js
@@ -9,7 +9,7 @@ const valuesValidator = (value) => {
return false;
}
- if (!_.isString(value.password)) {
+ if (!_.isNil(value.password) && !_.isString(value.password)) {
return false;
}
@@ -40,6 +40,10 @@ module.exports = {
async fn(inputs) {
const { values } = inputs;
+ if (values.password) {
+ values.password = bcrypt.hashSync(values.password, 10);
+ }
+
if (values.username) {
values.username = values.username.toLowerCase();
}
@@ -47,7 +51,6 @@ module.exports = {
const user = await User.create({
...values,
email: values.email.toLowerCase(),
- password: bcrypt.hashSync(values.password, 10),
})
.intercept(
{
diff --git a/server/api/helpers/users/delete-one.js b/server/api/helpers/users/delete-one.js
index 94bceeb7..6e2e6a58 100644
--- a/server/api/helpers/users/delete-one.js
+++ b/server/api/helpers/users/delete-one.js
@@ -10,6 +10,10 @@ module.exports = {
},
async fn(inputs) {
+ await IdentityProviderUser.destroy({
+ userId: inputs.record.id,
+ });
+
await ProjectManager.destroy({
userId: inputs.record.id,
});
diff --git a/server/api/helpers/users/get-or-create-one-by-oidc-token.js b/server/api/helpers/users/get-or-create-one-by-oidc-token.js
new file mode 100644
index 00000000..c9e38e07
--- /dev/null
+++ b/server/api/helpers/users/get-or-create-one-by-oidc-token.js
@@ -0,0 +1,159 @@
+const jwt = require('jsonwebtoken');
+const jwksClient = require('jwks-rsa');
+const openidClient = require('openid-client');
+
+const jwks = jwksClient({
+ jwksUri: sails.config.custom.oidcJwksUri,
+});
+
+const verifyOidcToken = async (oidcToken) => {
+ const signingKeys = await jwks.getSigningKeys();
+
+ const options = {
+ issuer: sails.config.custom.oidcIssuer,
+ };
+
+ if (sails.config.custom.oidcAudience) {
+ options.audience = sails.config.custom.oidcAudience;
+ }
+
+ let payload = null;
+ signingKeys.some((signingKey) => {
+ try {
+ const publicKey = signingKey.getPublicKey();
+ payload = jwt.verify(oidcToken, publicKey, options);
+ } catch (error) {
+ sails.log.error(error);
+ }
+
+ return !!payload;
+ });
+
+ return payload;
+};
+
+const getUserInfo = async (oidcToken) => {
+ const issuer = await openidClient.Issuer.discover(sails.config.custom.oidcIssuer);
+
+ const client = new issuer.Client({
+ client_id: 'irrelevant',
+ });
+
+ return client.userinfo(oidcToken);
+};
+
+module.exports = {
+ inputs: {
+ token: {
+ type: 'string',
+ required: true,
+ },
+ },
+
+ exits: {
+ invalidToken: {},
+ missingValues: {},
+ emailAlreadyInUse: {},
+ usernameAlreadyInUse: {},
+ },
+
+ async fn(inputs) {
+ const oidcUser = await verifyOidcToken(inputs.token);
+
+ if (!oidcUser) {
+ throw 'invalidToken';
+ }
+
+ if (!sails.config.custom.oidcSkipUserInfo) {
+ const userInfo = await getUserInfo(inputs.token);
+ Object.assign(oidcUser, userInfo);
+ }
+
+ if (!oidcUser.email || !oidcUser.name) {
+ throw 'missingValues';
+ }
+
+ let isAdmin = false;
+ if (sails.config.custom.oidcAdminRoles.includes('*')) {
+ isAdmin = true;
+ } else {
+ const roles = oidcUser[sails.config.custom.oidcRolesAttribute];
+
+ if (Array.isArray(roles)) {
+ isAdmin = sails.config.custom.oidcAdminRoles.some((role) => roles.includes(role));
+ }
+ }
+
+ const values = {
+ isAdmin,
+ email: oidcUser.email,
+ isSso: true,
+ name: oidcUser.name,
+ username: oidcUser.preferred_username,
+ subscribeToOwnCards: false,
+ };
+
+ let user;
+ /* eslint-disable no-constant-condition, no-await-in-loop */
+ while (true) {
+ let identityProviderUser = await IdentityProviderUser.findOne({
+ issuer: oidcUser.iss,
+ sub: oidcUser.sub,
+ });
+
+ if (identityProviderUser) {
+ user = await sails.helpers.users.getOne(identityProviderUser.userId);
+ } else {
+ while (true) {
+ user = await sails.helpers.users.getOne({
+ email: values.email,
+ });
+
+ if (!user) {
+ user = await sails.helpers.users
+ .createOne(values)
+ .tolerate('emailAlreadyInUse')
+ .intercept('usernameAlreadyInUse', 'usernameAlreadyInUse');
+ }
+
+ if (user) {
+ break;
+ }
+ }
+
+ identityProviderUser = await IdentityProviderUser.create({
+ userId: user.id,
+ issuer: oidcUser.iss,
+ sub: oidcUser.sub,
+ }).tolerate('E_UNIQUE');
+ }
+
+ if (identityProviderUser) {
+ break;
+ }
+ }
+ /* eslint-enable no-constant-condition, no-await-in-loop */
+
+ const updateFieldKeys = ['email', 'isAdmin', 'isSso', 'name', 'username'];
+
+ const updateValues = updateFieldKeys.reduce((result, fieldKey) => {
+ if (values[fieldKey] === user[fieldKey]) {
+ return result;
+ }
+
+ return {
+ ...result,
+ [fieldKey]: values[fieldKey],
+ };
+ }, {});
+
+ if (Object.keys(updateValues).length > 0) {
+ user = await sails.helpers.users
+ .updateOne(user, updateValues, {}) // FIXME: hack for last parameter
+ .intercept('emailAlreadyInUse', 'emailAlreadyInUse')
+ .intercept('usernameAlreadyInUse', 'usernameAlreadyInUse');
+ }
+
+ return user;
+ },
+};
diff --git a/server/api/models/User.js b/server/api/models/User.js
index f63dc68f..5a16fe23 100755
--- a/server/api/models/User.js
+++ b/server/api/models/User.js
@@ -24,6 +24,11 @@ module.exports = {
defaultsTo: false,
columnName: 'is_admin',
},
+ isSso: {
+ type: 'boolean',
+ defaultsTo: false,
+ columnName: 'is_sso',
+ },
name: {
type: 'string',
required: true,
@@ -67,10 +72,6 @@ module.exports = {
type: 'ref',
columnName: 'password_changed_at',
},
- locked: {
- type: 'boolean',
- columnName: 'locked',
- },
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
@@ -109,12 +110,15 @@ module.exports = {
tableName: 'user_account',
customToJSON() {
+ const isLockedAdmin = this.email === sails.config.custom.defaultAdminEmail;
+
return {
- ..._.omit(this, ['password', 'avatar', 'passwordChangedAt']),
+ ..._.omit(this, ['password', 'isSso', 'avatar', 'passwordChangedAt']),
+ isLockedAdmin,
+ isLocked: this.isSso || isLockedAdmin,
avatarUrl:
this.avatar &&
`${sails.config.custom.userAvatarsUrl}/${this.avatar.dirname}/square-100.${this.avatar.extension}`,
- isLocked: this.email === sails.config.custom.defaultAdminEmail,
};
},
};
diff --git a/server/config/custom.js b/server/config/custom.js
index a132e5ad..53f48b5c 100644
--- a/server/config/custom.js
+++ b/server/config/custom.js
@@ -31,15 +31,15 @@ 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 ? 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',
-
defaultAdminEmail: process.env.DEFAULT_ADMIN_EMAIL,
+
+ oidcIssuer: process.env.OIDC_ISSUER,
+ oidcClientId: process.env.OIDC_CLIENT_ID,
+ oidcRedirectUri: process.env.OIDC_REDIRECT_URI,
+ oidcScopes: process.env.OIDC_SCOPES || 'openid email profile',
+ oidcJwksUri: process.env.OIDC_JWKS_URI,
+ oidcAudience: process.env.OIDC_AUDIENCE,
+ oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [],
+ oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
+ oidcSkipUserInfo: process.env.OIDC_SKIP_USER_INFO === 'true',
};
diff --git a/server/config/policies.js b/server/config/policies.js
index 272355c4..6d923403 100644
--- a/server/config/policies.js
+++ b/server/config/policies.js
@@ -23,7 +23,7 @@ module.exports.policies = {
'projects/create': ['is-authenticated', 'is-admin'],
+ 'show-config': true,
'access-tokens/create': true,
'access-tokens/exchange': true,
- 'appconfig/index': true,
};
diff --git a/server/config/routes.js b/server/config/routes.js
index 9f380655..16ae350e 100644
--- a/server/config/routes.js
+++ b/server/config/routes.js
@@ -9,7 +9,7 @@
*/
module.exports.routes = {
- 'GET /api/appconfig': 'appconfig/index',
+ 'GET /api/config': 'show-config',
'POST /api/access-tokens': 'access-tokens/create',
'POST /api/access-tokens/exchange': 'access-tokens/exchange',
diff --git a/server/db/migrations/20230809022050_create_identity_provider_user.js b/server/db/migrations/20230809022050_create_identity_provider_user.js
deleted file mode 100644
index 615cf09a..00000000
--- a/server/db/migrations/20230809022050_create_identity_provider_user.js
+++ /dev/null
@@ -1,24 +0,0 @@
-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/20230809022050_oidc_with_pkce_flow.js b/server/db/migrations/20230809022050_oidc_with_pkce_flow.js
new file mode 100644
index 00000000..d3d997bc
--- /dev/null
+++ b/server/db/migrations/20230809022050_oidc_with_pkce_flow.js
@@ -0,0 +1,44 @@
+module.exports.up = async (knex) => {
+ await knex.schema.createTable('identity_provider_user', (table) => {
+ /* Columns */
+
+ table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
+
+ table.bigInteger('user_id').notNullable();
+
+ table.text('issuer').notNullable();
+ table.text('sub').notNullable();
+
+ table.timestamp('created_at', true);
+ table.timestamp('updated_at', true);
+
+ /* Indexes */
+
+ table.unique(['issuer', 'sub']);
+ table.index('user_id');
+ });
+
+ await knex.schema.table('user_account', (table) => {
+ /* Columns */
+
+ table.boolean('is_sso').notNullable().default(false);
+
+ /* Modifications */
+
+ table.setNullable('password');
+ });
+
+ return knex.schema.alterTable('user_account', (table) => {
+ table.boolean('is_sso').notNullable().alter();
+ });
+};
+
+module.exports.down = async (knex) => {
+ await knex.schema.dropTable('identity_provider_user');
+
+ return knex.schema.table('user_account', (table) => {
+ table.dropColumn('is_sso');
+
+ table.dropNullable('password');
+ });
+};
diff --git a/server/db/migrations/20230809024146_all_null_password_field.js b/server/db/migrations/20230809024146_all_null_password_field.js
deleted file mode 100644
index c86529f1..00000000
--- a/server/db/migrations/20230809024146_all_null_password_field.js
+++ /dev/null
@@ -1,11 +0,0 @@
-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
deleted file mode 100644
index 07c341f1..00000000
--- a/server/db/migrations/20230809025904_add_lock_to_user_account.js
+++ /dev/null
@@ -1,11 +0,0 @@
-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/db/seeds/default.js b/server/db/seeds/default.js
index 8fa4cdc2..20624e50 100644
--- a/server/db/seeds/default.js
+++ b/server/db/seeds/default.js
@@ -3,6 +3,7 @@ const bcrypt = require('bcrypt');
const buildData = () => {
const data = {
isAdmin: true,
+ isSso: false,
};
if (process.env.DEFAULT_ADMIN_PASSWORD) {