From 36aa732fec851e6dfeebc5bd0b43576dafd3283a Mon Sep 17 00:00:00 2001 From: Daniel Hiller Date: Fri, 7 Aug 2026 19:15:43 +0200 Subject: [PATCH] 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. --- README.md | 1 - charts/planka/README.md | 42 --- charts/planka/templates/deployment.yaml | 27 -- charts/planka/templates/secret-oidc.yaml | 17 -- charts/planka/values.yaml | 79 ----- client/src/actions/login.js | 28 -- client/src/api/access-tokens.js | 7 - .../UsersPane/ActionsStep.jsx | 33 --- .../UsersPane/UsersPane.jsx | 35 +-- .../src/components/common/Login/Content.jsx | 130 +++------ client/src/components/common/Root.jsx | 1 - .../EditUserEmailStep/EditUserEmailStep.jsx | 3 +- .../EditUserPasswordStep.jsx | 3 +- .../EditUserUsernameStep.jsx | 3 +- client/src/constants/ActionTypes.js | 4 - client/src/constants/EntryActionTypes.js | 1 - client/src/constants/Paths.js | 2 - client/src/entry-actions/login.js | 6 - client/src/locales/ar-YE/core.js | 5 - client/src/locales/ar-YE/login.js | 3 - client/src/locales/bg-BG/core.js | 5 - client/src/locales/bg-BG/login.js | 3 - client/src/locales/ca-ES/core.js | 5 - client/src/locales/ca-ES/login.js | 3 - client/src/locales/cs-CZ/core.js | 5 - client/src/locales/cs-CZ/login.js | 3 - client/src/locales/da-DK/core.js | 5 - client/src/locales/da-DK/login.js | 3 - client/src/locales/de-DE/core.js | 5 - client/src/locales/de-DE/login.js | 3 - client/src/locales/el-GR/core.js | 5 - client/src/locales/el-GR/login.js | 3 - client/src/locales/en-GB/core.js | 5 - client/src/locales/en-GB/login.js | 3 - client/src/locales/en-US/core.js | 5 - client/src/locales/en-US/login.js | 3 - client/src/locales/es-ES/core.js | 5 - client/src/locales/es-ES/login.js | 3 - client/src/locales/et-EE/core.js | 5 - client/src/locales/et-EE/login.js | 3 - client/src/locales/fa-IR/core.js | 5 - client/src/locales/fa-IR/login.js | 3 - client/src/locales/fi-FI/core.js | 5 - client/src/locales/fi-FI/login.js | 3 - client/src/locales/fr-FR/core.js | 5 - client/src/locales/fr-FR/login.js | 3 - client/src/locales/hu-HU/core.js | 5 - client/src/locales/hu-HU/login.js | 3 - client/src/locales/id-ID/core.js | 5 - client/src/locales/id-ID/login.js | 3 - client/src/locales/it-IT/core.js | 5 - client/src/locales/it-IT/login.js | 3 - client/src/locales/ja-JP/core.js | 5 - client/src/locales/ja-JP/login.js | 3 - client/src/locales/ko-KR/core.js | 5 - client/src/locales/ko-KR/login.js | 3 - client/src/locales/nl-NL/core.js | 5 - client/src/locales/nl-NL/login.js | 3 - client/src/locales/pl-PL/core.js | 5 - client/src/locales/pl-PL/login.js | 3 - client/src/locales/pt-BR/core.js | 5 - client/src/locales/pt-BR/login.js | 3 - client/src/locales/pt-PT/core.js | 5 - client/src/locales/pt-PT/login.js | 3 - client/src/locales/ro-RO/core.js | 5 - client/src/locales/ro-RO/login.js | 3 - client/src/locales/ru-RU/core.js | 5 - client/src/locales/ru-RU/login.js | 3 - client/src/locales/sk-SK/core.js | 5 - client/src/locales/sk-SK/login.js | 3 - client/src/locales/sr-Cyrl-RS/core.js | 5 - client/src/locales/sr-Cyrl-RS/login.js | 3 - client/src/locales/sr-Latn-RS/core.js | 5 - client/src/locales/sr-Latn-RS/login.js | 3 - client/src/locales/sv-SE/core.js | 5 - client/src/locales/sv-SE/login.js | 3 - client/src/locales/tr-TR/core.js | 5 - client/src/locales/tr-TR/login.js | 3 - client/src/locales/uk-UA/core.js | 5 - client/src/locales/uk-UA/login.js | 3 - client/src/locales/uz-UZ/core.js | 5 - client/src/locales/uz-UZ/login.js | 3 - client/src/locales/vi-VN/core.js | 5 - client/src/locales/vi-VN/login.js | 3 - client/src/locales/zh-CN/core.js | 5 - client/src/locales/zh-CN/login.js | 3 - client/src/locales/zh-TW/core.js | 5 - client/src/locales/zh-TW/login.js | 3 - client/src/models/User.js | 1 - client/src/reducers/auth.js | 1 - client/src/reducers/common.js | 1 - client/src/reducers/ui/authenticate-form.js | 40 --- client/src/sagas/core/index.js | 15 +- client/src/sagas/core/services/router.js | 1 - client/src/sagas/login/index.js | 6 +- client/src/sagas/login/services/login.js | 104 ------- client/src/sagas/login/services/router.js | 18 -- client/src/sagas/login/watchers/login.js | 1 - client/src/selectors/common.js | 3 - docker-compose-dev.yml | 23 -- docker-compose.yml | 25 -- server/.env.sample | 23 -- .../api/controllers/access-tokens/create.js | 17 +- .../controllers/access-tokens/debug-oidc.js | 223 -------------- .../access-tokens/exchange-with-oidc.js | 271 ------------------ server/api/controllers/bootstrap/show.js | 28 +- server/api/controllers/users/create.js | 4 - server/api/controllers/users/update-email.js | 6 +- .../api/controllers/users/update-password.js | 6 +- .../api/controllers/users/update-username.js | 8 +- server/api/controllers/users/update.js | 20 +- server/api/helpers/bootstrap/present-one.js | 4 - server/api/helpers/users/delete-related.js | 4 - .../users/get-or-create-one-with-oidc.js | 200 ------------- server/api/helpers/users/present-one.js | 19 +- server/api/hooks/oidc/index.js | 114 -------- .../models/IdentityProviderUser.js | 23 -- server/api/models/IdentityProviderUser.js | 46 --- server/api/models/User.js | 18 +- server/config/custom.js | 28 -- server/config/policies.js | 2 - server/config/routes.js | 2 - ...807120000_remove_single_sign_on_support.js | 51 ++++ server/db/seeds/default.js | 1 - server/db/upgrade.js | 10 - server/package-lock.json | 43 --- server/package.json | 1 - server/start.sh | 1 - 128 files changed, 121 insertions(+), 1993 deletions(-) delete mode 100644 charts/planka/templates/secret-oidc.yaml delete mode 100644 server/api/controllers/access-tokens/debug-oidc.js delete mode 100644 server/api/controllers/access-tokens/exchange-with-oidc.js delete mode 100644 server/api/helpers/users/get-or-create-one-with-oidc.js delete mode 100644 server/api/hooks/oidc/index.js delete mode 100644 server/api/hooks/query-methods/models/IdentityProviderUser.js delete mode 100644 server/api/models/IdentityProviderUser.js create mode 100644 server/db/migrations/20260807120000_remove_single_sign_on_support.js diff --git a/README.md b/README.md index a80bbc36..fd62c6a4 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ - **Real-Time Updates:** Instant syncing across all users, no refresh needed - **Rich Markdown Support:** Write beautifully formatted card descriptions with a powerful markdown editor - **Flexible Notifications:** Get alerts through 100+ providers, fully customizable to your workflow -- **Seamless Authentication:** Single sign-on with OpenID Connect integration - **Multilingual & Easy to Translate:** Full internationalization support for a global audience ## How to Deploy diff --git a/charts/planka/README.md b/charts/planka/README.md index 142a3f9b..eb3c1c4f 100644 --- a/charts/planka/README.md +++ b/charts/planka/README.md @@ -116,7 +116,6 @@ Any questions or concerns, [raise an issue](https://github.com/Chris-Greaves/pla The Helm chart supports mounting arbitrary ConfigMaps, Secrets, and Volumes to the PLANKA deployment using the `extraMounts` configuration. This is especially useful for scenarios like: -- Mounting custom CA certificates for OIDC with self-hosted identity providers - Adding custom configuration files - Mounting TLS certificates from existing secrets - Adding temporary or persistent storage volumes @@ -173,44 +172,6 @@ extraMounts: path: /exports/planka ``` -### OIDC with Self-Hosted Keycloak - -A common use case is configuring OIDC with a self-hosted Keycloak instance that uses custom CA certificates. - -First, create the CA certificate ConfigMap: - -```bash -kubectl create configmap ca-certificates --from-file=ca.crt=/path/to/your/ca.crt -``` - -Then configure the chart: - -```yaml -# Mount custom CA certificate from existing ConfigMap -extraMounts: - - name: keycloak-ca - mountPath: /etc/ssl/certs/keycloak-ca.crt - subPath: ca.crt - readOnly: true - configMap: - name: ca-certificates - -# Configure Node.js to trust the custom CA -extraEnv: - - name: NODE_EXTRA_CA_CERTS - value: "/etc/ssl/certs/keycloak-ca.crt" - -# Enable OIDC -oidc: - enabled: true - clientId: "planka-client" - clientSecret: "your-client-secret" - issuerUrl: "https://keycloak.example.com/realms/master" - admin: - roles: - - "planka-admin" -``` - ### Environment Variables from Secrets You can reference values from existing secrets in environment variables: @@ -310,7 +271,4 @@ image: - **Reproducibility**: Makes deployments fully reproducible across environments - **Audit Trail**: Provides clear image identity in deployment manifests -### Complete Example - -See `values-example.yaml` for a comprehensive example that demonstrates all the advanced features including OIDC configuration with custom CA certificates. ```` diff --git a/charts/planka/templates/deployment.yaml b/charts/planka/templates/deployment.yaml index 63fdecb5..6fa10be9 100644 --- a/charts/planka/templates/deployment.yaml +++ b/charts/planka/templates/deployment.yaml @@ -173,33 +173,6 @@ spec: - name: {{ $k | quote }} value: {{ $v | quote }} {{- end }} - {{- if .Values.oidc.enabled }} - {{- $secretName := default (printf "%s-oidc" (include "planka.fullname" .)) .Values.oidc.existingSecret }} - - name: OIDC_CLIENT_ID - valueFrom: - secretKeyRef: - key: clientId - name: {{ $secretName }} - - name: OIDC_CLIENT_SECRET - valueFrom: - secretKeyRef: - key: clientSecret - name: {{ $secretName }} - - name: OIDC_ISSUER - value: {{ required "issuerUrl is required when configuring OIDC" .Values.oidc.issuerUrl | quote }} - - name: OIDC_SCOPES - value: {{ join " " .Values.oidc.scopes | default "openid profile email" | quote }} - {{- if .Values.oidc.admin.roles }} - - name: OIDC_ADMIN_ROLES - value: {{ join "," .Values.oidc.admin.roles | quote }} - {{- end }} - - name: OIDC_ROLES_ATTRIBUTE - value: {{ .Values.oidc.admin.rolesAttribute | default "groups" | quote }} - {{- if .Values.oidc.admin.ignoreRoles }} - - name: OIDC_IGNORE_ROLES - value: {{ .Values.oidc.admin.ignoreRoles | quote }} - {{- end }} - {{- end }} {{- if .Values.extraContainers -}} {{ toYaml .Values.extraContainers | nindent 8 }} {{- end }} diff --git a/charts/planka/templates/secret-oidc.yaml b/charts/planka/templates/secret-oidc.yaml deleted file mode 100644 index 7d5293b6..00000000 --- a/charts/planka/templates/secret-oidc.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if .Values.oidc.enabled }} -{{- if eq (and (not (empty .Values.oidc.clientId)) (not (empty .Values.oidc.clientSecret))) (not (empty .Values.oidc.existingSecret)) -}} - {{- fail "Either specify inline `clientId` and `clientSecret` or refer to them via `existingSecret`" -}} -{{- end }} -{{- if (and (and (not (empty .Values.oidc.clientId)) (not (empty .Values.oidc.clientSecret))) (empty .Values.oidc.existingSecret)) -}} -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "planka.fullname" . }}-oidc - labels: - {{- include "planka.labels" . | nindent 4 }} -type: Opaque -data: - clientId: {{ .Values.oidc.clientId | b64enc | quote }} - clientSecret: {{ .Values.oidc.clientSecret | b64enc | quote }} -{{- end }} -{{- end }} diff --git a/charts/planka/values.yaml b/charts/planka/values.yaml index 9cc38696..18a27396 100644 --- a/charts/planka/values.yaml +++ b/charts/planka/values.yaml @@ -151,71 +151,6 @@ persistence: accessMode: ReadWriteOnce size: 10Gi -## OpenID Identity Management configuration -## -## Example: -## --------------- -## oidc: -## enabled: true -## clientId: sxxaAIAxVXlCxTmc1YLHBbQr8NL8MqLI2DUbt42d -## clientSecret: om4RTMRVHRszU7bqxB7RZNkHIzA8e4sGYWxeCwIMYQXPwEBWe4SY5a0wwCe9ltB3zrq5f0dnFnp34cEHD7QSMHsKvV9AiV5Z7eqDraMnv0I8IFivmuV5wovAECAYreSI -## issuerUrl: https://auth.local/application/o/planka/ -## admin: -## roles: -## - planka-admin -## -## --------------- -## NOTE: A minimal configuration requires setting `clientId`, `clientSecret` and `issuerUrl`. (plus `admin.roles` for administrators) -## ref: https://docs.planka.cloud/docs/configuration/oidc/ -## -oidc: - ## @param oidc.enabled Enable single sign-on (SSO) with OpenID Connect (OIDC) - ## - enabled: false - - ## OIDC credentials - ## @param oidc.clientId A string unique to the provider that identifies your app. - ## @param oidc.clientSecret A secret string that the provider uses to confirm ownership of a client ID. - ## - ## NOTE: Either specify inline `clientId` and `clientSecret` or refer to them via `existingSecret` - ## - clientId: "" - clientSecret: "" - - ## @param oidc.existingSecret Name of an existing secret containing OIDC credentials - ## NOTE: Must contain key `clientId` and `clientSecret` - ## NOTE: When it's set, the `clientId` and `clientSecret` parameters are ignored - ## - existingSecret: "" - - ## @param oidc.issuerUrl The OpenID connect metadata document endpoint - ## - issuerUrl: "" - - ## @param oidc.scopes A list of scopes required for OIDC client. - ## If empty will default to `openid`, `profile` and `email` - ## NOTE: PLANKA needs the email and name claims - ## - scopes: [] - - ## Admin permissions configuration - admin: - ## @param oidc.admin.ignoreRoles If set to true, the admin roles will be ignored. - ## It is useful if you want to use OIDC for authentication but not for authorization. - ## If empty will default to `false` - ## - ignoreRoles: false - - ## @param oidc.admin.rolesAttribute The name of a custom group claim that you have configured in your OIDC provider - ## If empty will default to `groups` - ## - rolesAttribute: groups - - ## @param oidc.admin.roles The names of the admin groups - ## - roles: [] - # - planka-admin - ## Extra environment variables for planka deployment ## Supports hard coded and getting values from a k8s secret ## - name: test @@ -293,20 +228,6 @@ extraMounts: [] ## emptyDir: ## sizeLimit: 1Gi -## Example configuration for OIDC with self-hosted Keycloak using custom CA -## (Requires pre-existing ConfigMap "ca-certificates") -## extraMounts: -## - name: keycloak-ca -## mountPath: /etc/ssl/certs/keycloak-ca.crt -## subPath: ca.crt -## readOnly: true -## configMap: -## name: ca-certificates -## -## extraEnv: -## - name: NODE_EXTRA_CA_CERTS -## value: "/etc/ssl/certs/keycloak-ca.crt" - extraContainers: [] ## Extra sidecar containers ## Add additional containers to the PLANKA pod diff --git a/client/src/actions/login.js b/client/src/actions/login.js index 7cd14692..dee8c258 100644 --- a/client/src/actions/login.js +++ b/client/src/actions/login.js @@ -34,33 +34,6 @@ authenticate.failure = (error, terms) => ({ }, }); -const authenticateWithOidc = () => ({ - type: ActionTypes.WITH_OIDC_AUTHENTICATE, - payload: {}, -}); - -authenticateWithOidc.success = (accessToken) => ({ - type: ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS, - payload: { - accessToken, - }, -}); - -authenticateWithOidc.failure = (error, terms) => ({ - type: ActionTypes.WITH_OIDC_AUTHENTICATE__FAILURE, - payload: { - error, - terms, - }, -}); - -authenticateWithOidc.debug = (logs) => ({ - type: ActionTypes.WITH_OIDC_AUTHENTICATE__DEBUG, - payload: { - logs, - }, -}); - const clearAuthenticateError = () => ({ type: ActionTypes.AUTHENTICATE_ERROR_CLEAR, payload: {}, @@ -128,7 +101,6 @@ updateTermsLanguage.failure = (error) => ({ export default { initializeLogin, authenticate, - authenticateWithOidc, clearAuthenticateError, acceptTerms, cancelTerms, diff --git a/client/src/api/access-tokens.js b/client/src/api/access-tokens.js index 426c0501..8d770db2 100755 --- a/client/src/api/access-tokens.js +++ b/client/src/api/access-tokens.js @@ -10,11 +10,6 @@ import http from './http'; const createAccessToken = (data, headers) => http.post('/access-tokens?withHttpOnlyToken=true', data, headers); -const exchangeForAccessTokenWithOidc = (data, headers) => - http.post('/access-tokens/exchange-with-oidc?withHttpOnlyToken=true', data, headers); - -const debugOidc = (data, headers) => http.post('/access-tokens/debug-oidc', data, headers); - // TODO: rename? const acceptTerms = (data, headers) => http.post('/access-tokens/accept-terms', data, headers); @@ -25,8 +20,6 @@ const deleteCurrentAccessToken = (headers) => http.delete('/access-tokens/me', u export default { createAccessToken, - exchangeForAccessTokenWithOidc, - debugOidc, acceptTerms, revokePendingToken, deleteCurrentAccessToken, diff --git a/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx b/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx index 9fbad0ed..487f19f8 100644 --- a/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx +++ b/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx @@ -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 ; - case StepTypes.UNLINK_SSO: - return ( - - ); case StepTypes.ACTIVATE: return ( { context: 'title', })} - {user.isSsoUser && !user.lockedFieldNames.includes('isSsoUser') && !isCurrentUser && ( - - - {t('action.unlinkSso', { - context: 'title', - })} - - )} {!isCurrentUser && ( <> { 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 && ( - - - - )} + + + ); diff --git a/client/src/components/common/Login/Content.jsx b/client/src/components/common/Login/Content.jsx index 03904133..2fc3e395 100644 --- a/client/src/components/common/Login/Content.jsx +++ b/client/src/components/common/Login/Content.jsx @@ -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 && ( - <> -
-
-
{t('common.emailOrUsername')}
- -
-
-
{t('common.password')}
- -
- - - {withOidc && ( - - )} - - )} - {withOidc && ( - <> -