feat: Remove OIDC and SSO support

Existing SSO accounts have no local password, so the migration
deactivates them before dropping is_sso_user and the
identity_provider_user table.
This commit is contained in:
Daniel Hiller
2026-08-07 19:15:43 +02:00
parent a1f0a2b3fa
commit 36aa732fec
128 changed files with 121 additions and 1993 deletions
@@ -32,7 +32,6 @@ const StepTypes = {
EDIT_PASSWORD: 'EDIT_PASSWORD',
EDIT_ROLE: 'EDIT_ROLE',
API_KEY: 'API_KEY',
UNLINK_SSO: 'UNLINK_SSO',
ACTIVATE: 'ACTIVATE',
DEACTIVATE: 'DEACTIVATE',
DELETE: 'DELETE',
@@ -61,16 +60,6 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
[userId, dispatch],
);
const handleUnlinkSsoConfirm = useCallback(() => {
dispatch(
entryActions.updateUser(userId, {
isSsoUser: false,
}),
);
onClose();
}, [userId, onClose, dispatch]);
const handleActivateConfirm = useCallback(() => {
dispatch(
entryActions.updateUser(userId, {
@@ -123,10 +112,6 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
openStep(StepTypes.API_KEY);
}, [openStep]);
const handleUnlinkSsoClick = useCallback(() => {
openStep(StepTypes.UNLINK_SSO);
}, [openStep]);
const handleActivateClick = useCallback(() => {
openStep(StepTypes.ACTIVATE);
}, [openStep]);
@@ -165,16 +150,6 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
);
case StepTypes.API_KEY:
return <ApiKeyStep userId={userId} onBack={handleBack} onClose={onClose} />;
case StepTypes.UNLINK_SSO:
return (
<ConfirmationStep
title="common.unlinkSso"
content="common.areYouSureYouWantToUnlinkSsoFromThisUser"
buttonContent="action.unlinkSso"
onConfirm={handleUnlinkSsoConfirm}
onBack={handleBack}
/>
);
case StepTypes.ACTIVATE:
return (
<ConfirmationStep
@@ -271,14 +246,6 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
context: 'title',
})}
</Menu.Item>
{user.isSsoUser && !user.lockedFieldNames.includes('isSsoUser') && !isCurrentUser && (
<Menu.Item className={styles.menuItem} onClick={handleUnlinkSsoClick}>
<Icon name="unlink" className={styles.menuItemIcon} />
{t('action.unlinkSso', {
context: 'title',
})}
</Menu.Item>
)}
{!isCurrentUser && (
<>
<Menu.Item
@@ -21,11 +21,6 @@ const UsersPane = React.memo(() => {
const activeUsersTotal = useSelector(selectors.selectActiveUsersTotal);
const users = useSelector(selectors.selectUsers);
const canAdd = useSelector((state) => {
const oidcBootstrap = selectors.selectOidcBootstrap(state);
return !oidcBootstrap || !oidcBootstrap.isEnforced;
});
const [t] = useTranslation();
const [search, handleSearchChange] = useField('');
@@ -102,22 +97,20 @@ const UsersPane = React.memo(() => {
className={styles.toggleDeactivatedButton}
onClick={handleToggleDeactivatedClick}
/>
{canAdd && (
<AddPopup>
<Button
positive
disabled={activeUsersLimit !== null && activeUsersTotal >= activeUsersLimit}
className={styles.addButton}
>
{t('action.addUser')}
{activeUsersLimit !== null && (
<span className={styles.addButtonCounter}>
{activeUsersTotal}/{activeUsersLimit}
</span>
)}
</Button>
</AddPopup>
)}
<AddPopup>
<Button
positive
disabled={activeUsersLimit !== null && activeUsersTotal >= activeUsersLimit}
className={styles.addButton}
>
{t('action.addUser')}
{activeUsersLimit !== null && (
<span className={styles.addButtonCounter}>
{activeUsersTotal}/{activeUsersLimit}
</span>
)}
</Button>
</AddPopup>
</div>
</Tab.Pane>
);
+41 -89
View File
@@ -8,8 +8,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation, Trans } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Divider, Form, Grid, Header, Message, TextArea } from 'semantic-ui-react';
import { Form, Grid, Header, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { Input } from '../../../lib/custom-ui';
@@ -24,7 +23,7 @@ import logo from '../../../assets/images/logo.png';
import styles from './Content.module.scss';
const createMessage = (error, isDebug) => {
const createMessage = (error) => {
if (!error) {
return error;
}
@@ -45,11 +44,6 @@ const createMessage = (error, isDebug) => {
type: 'error',
content: 'common.invalidPassword',
};
case 'Use single sign-on':
return {
type: 'error',
content: 'common.useSingleSignOn',
};
case 'Admin login required to initialize instance':
return {
type: 'error',
@@ -83,7 +77,7 @@ const createMessage = (error, isDebug) => {
default:
return {
type: 'warning',
content: isDebug ? error.message : 'common.unknownError',
content: 'common.unknownError',
};
}
};
@@ -94,9 +88,7 @@ const Content = React.memo(() => {
const {
data: defaultData,
isSubmitting,
isSubmittingWithOidc,
error,
debugLogs,
step,
} = useSelector(selectors.selectAuthenticateForm);
@@ -126,11 +118,7 @@ const Content = React.memo(() => {
return initialData;
});
const withOidc = !!bootstrap.oidc;
const isOidcEnforced = withOidc && bootstrap.oidc.isEnforced;
const isOidcDebug = withOidc && bootstrap.oidc.debug;
const message = useMemo(() => createMessage(error, isOidcDebug), [error, isOidcDebug]);
const message = useMemo(() => createMessage(error), [error]);
const [focusPasswordFieldState, focusPasswordField] = useToggle();
const [emailOrUsernameFieldRef, handleEmailOrUsernameFieldRef] = useNestedRef('inputRef');
@@ -155,19 +143,13 @@ const Content = React.memo(() => {
dispatch(entryActions.authenticate(cleanData));
}, [dispatch, data, emailOrUsernameFieldRef, passwordFieldRef]);
const handleAuthenticateWithOidcClick = useCallback(() => {
dispatch(entryActions.authenticateWithOidc());
}, [dispatch]);
const handleMessageDismiss = useCallback(() => {
dispatch(entryActions.clearAuthenticateError());
}, [dispatch]);
useEffect(() => {
if (!isOidcEnforced) {
emailOrUsernameFieldRef.current.focus();
}
}, [isOidcEnforced, emailOrUsernameFieldRef]);
emailOrUsernameFieldRef.current.focus();
}, [emailOrUsernameFieldRef]);
useDidUpdate(() => {
if (wasSubmitting && !isSubmitting && error) {
@@ -227,73 +209,43 @@ const Content = React.memo(() => {
onDismiss={handleMessageDismiss}
/>
)}
{!isOidcEnforced && (
<>
<Form size="large" onSubmit={handleSubmit}>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.emailOrUsername')}</div>
<Input
fluid
ref={handleEmailOrUsernameFieldRef}
name="emailOrUsername"
value={data.emailOrUsername}
maxLength={256}
readOnly={isSubmitting}
className={styles.input}
onChange={handleFieldChange}
/>
</div>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.password')}</div>
<Input.Password
fluid
ref={handlePasswordFieldRef}
name="password"
value={data.password}
maxLength={256}
readOnly={isSubmitting}
className={styles.input}
onChange={handleFieldChange}
/>
</div>
<Form.Button
fluid
primary
icon="right arrow"
labelPosition="right"
content={t('action.logIn')}
loading={isSubmitting}
disabled={isSubmitting || isSubmittingWithOidc}
/>
</Form>
{withOidc && (
<Divider horizontal content={t('common.or')} className={styles.divider} />
)}
</>
)}
{withOidc && (
<>
<Button
<Form size="large" onSubmit={handleSubmit}>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.emailOrUsername')}</div>
<Input
fluid
primary={isOidcDebug ? undefined : isOidcEnforced}
color={isOidcDebug ? 'orange' : undefined}
icon={isOidcEnforced ? 'right arrow' : undefined}
labelPosition={isOidcEnforced ? 'right' : undefined}
content={isOidcDebug ? t('action.debugSso') : t('action.logInWithSso')}
loading={isSubmittingWithOidc}
disabled={isSubmitting || isSubmittingWithOidc}
onClick={handleAuthenticateWithOidcClick}
ref={handleEmailOrUsernameFieldRef}
name="emailOrUsername"
value={data.emailOrUsername}
maxLength={256}
readOnly={isSubmitting}
className={styles.input}
onChange={handleFieldChange}
/>
{debugLogs && (
<TextArea
readOnly
as={TextareaAutosize}
value={debugLogs.join('\n')}
className={styles.debugLog}
/>
)}
</>
)}
</div>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.password')}</div>
<Input.Password
fluid
ref={handlePasswordFieldRef}
name="password"
value={data.password}
maxLength={256}
readOnly={isSubmitting}
className={styles.input}
onChange={handleFieldChange}
/>
</div>
<Form.Button
fluid
primary
icon="right arrow"
labelPosition="right"
content={t('action.logIn')}
loading={isSubmitting}
disabled={isSubmitting}
/>
</Form>
</div>
<div className={styles.poweredBy}>
<p className={styles.poweredByText}>
-1
View File
@@ -32,7 +32,6 @@ function Root({ store, history }) {
<ToasterProvider toaster={toaster}>
<Routes>
<Route path={Paths.LOGIN} element={<Login />} />
<Route path={Paths.OIDC_CALLBACK} element={<Login />} />
<Route path={Paths.ROOT} element={<Core />} />
<Route path={Paths.PROJECTS} element={<Core />} />
<Route path={Paths.BOARDS} element={<Core />} />
@@ -47,12 +47,11 @@ const EditUserEmailStep = React.memo(({ id, onBack, onClose }) => {
const {
email,
isSsoUser,
emailUpdateForm: { data: defaultData, isSubmitting, error },
} = useSelector((state) => selectUserById(state, id));
const withPasswordConfirmation = useSelector(
(state) => id === selectors.selectCurrentUserId(state) && !isSsoUser,
(state) => id === selectors.selectCurrentUserId(state),
);
const dispatch = useDispatch();
@@ -42,12 +42,11 @@ const EditUserPasswordStep = React.memo(({ id, onBack, onClose }) => {
const selectUserById = useMemo(() => selectors.makeSelectUserById(), []);
const {
isSsoUser,
passwordUpdateForm: { data: defaultData, isSubmitting, error },
} = useSelector((state) => selectUserById(state, id));
const withPasswordConfirmation = useSelector(
(state) => id === selectors.selectCurrentUserId(state) && !isSsoUser,
(state) => id === selectors.selectCurrentUserId(state),
);
const dispatch = useDispatch();
@@ -47,12 +47,11 @@ const EditUserUsernameStep = React.memo(({ id, onBack, onClose }) => {
const {
username,
isSsoUser,
usernameUpdateForm: { data: defaultData, isSubmitting, error },
} = useSelector((state) => selectUserById(state, id));
const withPasswordConfirmation = useSelector(
(state) => id === selectors.selectCurrentUserId(state) && !isSsoUser,
(state) => id === selectors.selectCurrentUserId(state),
);
const dispatch = useDispatch();