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
+1 -14
View File
@@ -3,12 +3,11 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { apply, fork, select, take } from 'redux-saga/effects';
import { apply, fork, take } from 'redux-saga/effects';
import watchers from './watchers';
import services from './services';
import runWatchers from '../run-watchers';
import selectors from '../../selectors';
import { socket } from '../../api';
import ActionTypes from '../../constants/ActionTypes';
import Paths from '../../constants/Paths';
@@ -21,17 +20,5 @@ export default function* coreSaga() {
yield take(ActionTypes.LOGOUT);
const oidcBootstrap = yield select(selectors.selectOidcBootstrap);
if (oidcBootstrap && oidcBootstrap.endSessionUrl !== null) {
const currentUser = yield select(selectors.selectCurrentUser);
if (!currentUser || currentUser.isSsoUser) {
// Redirect the user to the IDP to log out.
window.location.href = oidcBootstrap.endSessionUrl;
return;
}
}
window.location.href = Paths.LOGIN;
}
-1
View File
@@ -53,7 +53,6 @@ export function* handleLocationChange() {
switch (pathsMatch.pattern.path) {
case Paths.LOGIN:
case Paths.OIDC_CALLBACK:
yield call(goToRoot);
break;
+1 -5
View File
@@ -15,11 +15,7 @@ export default function* loginSaga() {
yield fork(services.initializeLogin);
yield take([
ActionTypes.AUTHENTICATE__SUCCESS,
ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS,
ActionTypes.TERMS_ACCEPT__SUCCESS,
]);
yield take([ActionTypes.AUTHENTICATE__SUCCESS, ActionTypes.TERMS_ACCEPT__SUCCESS]);
yield cancel(watcherTasks);
yield call(services.goToRoot);
-104
View File
@@ -3,16 +3,13 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { nanoid } from 'nanoid';
import { 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 i18n from '../../../i18n';
import { setAccessToken } from '../../../utils/access-token-storage';
import Paths from '../../../constants/Paths';
import AccessTokenSteps from '../../../constants/AccessTokenSteps';
export function* initializeLogin() {
@@ -41,105 +38,6 @@ export function* authenticate(data) {
yield put(actions.authenticate.success(accessToken));
}
export function* authenticateWithOidc() {
const oidcBootstrap = yield select(selectors.selectOidcBootstrap);
const state = nanoid();
window.localStorage.setItem('oidc-state', state);
const nonce = nanoid();
window.localStorage.setItem('oidc-nonce', nonce);
let redirectUrl = `${oidcBootstrap.authorizationUrl}`;
redirectUrl += `&state=${encodeURIComponent(state)}`;
redirectUrl += `&nonce=${encodeURIComponent(nonce)}`;
window.location.href = redirectUrl;
}
export function* authenticateWithOidcCallback() {
// https://github.com/plankanban/planka/issues/511#issuecomment-1771385639
const params = new URLSearchParams(window.location.hash.substring(1) || window.location.search);
const state = window.localStorage.getItem('oidc-state');
window.localStorage.removeItem('oidc-state');
const nonce = window.localStorage.getItem('oidc-nonce');
window.localStorage.removeItem('oidc-nonce');
yield put(replace(Paths.LOGIN));
if (params.get('error') !== null) {
yield put(
actions.authenticateWithOidc.failure(
new Error(
`OIDC Authorization error: ${params.get('error')}: ${params.get('error_description')}`,
),
),
);
return;
}
const code = params.get('code');
if (code === null) {
yield put(
actions.authenticateWithOidc.failure(new Error('Invalid OIDC response: no code parameter')),
);
return;
}
if (params.get('state') !== state) {
yield put(
actions.authenticateWithOidc.failure(
new Error('Unable to process OIDC response: state mismatch'),
),
);
return;
}
if (nonce === null) {
yield put(
actions.authenticateWithOidc.failure(
new Error('Unable to process OIDC response: no nonce issued'),
),
);
return;
}
const oidcBootstrap = yield select(selectors.selectOidcBootstrap);
if (oidcBootstrap?.debug) {
const {
included: { logs },
} = yield call(api.debugOidc, {
code,
nonce,
});
yield put(actions.authenticateWithOidc.debug(logs));
return;
}
let accessToken;
try {
({ item: accessToken } = yield call(api.exchangeForAccessTokenWithOidc, {
code,
nonce,
}));
} catch (error) {
let terms;
if (error.step === AccessTokenSteps.ACCEPT_TERMS) {
({ item: terms } = yield call(api.getTerms, i18n.resolvedLanguage));
}
yield put(actions.authenticateWithOidc.failure(error, terms));
return;
}
yield call(setAccessToken, accessToken);
yield put(actions.authenticateWithOidc.success(accessToken));
}
export function* clearAuthenticateError() {
yield put(actions.clearAuthenticateError());
}
@@ -199,8 +97,6 @@ export function* updateTermsLanguage(value) {
export default {
initializeLogin,
authenticate,
authenticateWithOidc,
authenticateWithOidcCallback,
clearAuthenticateError,
acceptTerms,
cancelTerms,
-18
View File
@@ -6,7 +6,6 @@
import { call, put, select, take } from 'redux-saga/effects';
import { push } from '../../../lib/redux-router';
import { authenticateWithOidc, authenticateWithOidcCallback } from './login';
import selectors from '../../../selectors';
import ActionTypes from '../../../constants/ActionTypes';
import Paths from '../../../constants/Paths';
@@ -48,23 +47,6 @@ export function* handleLocationChange() {
}
switch (pathsMatch.pattern.path) {
case Paths.LOGIN: {
const oidcBootstrap = yield select(selectors.selectOidcBootstrap);
if (oidcBootstrap) {
const params = new URLSearchParams(window.location.search);
if (params.has('authenticateWithOidc')) {
yield call(authenticateWithOidc);
}
}
break;
}
case Paths.OIDC_CALLBACK:
yield call(authenticateWithOidcCallback);
break;
default:
}
}
-1
View File
@@ -13,7 +13,6 @@ 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()),
takeEvery(EntryActionTypes.TERMS_ACCEPT, ({ payload: { signature } }) =>
services.acceptTerms(signature),