feat: Add OIDC debug mode

This commit is contained in:
Maksim Eltyshev
2026-01-27 22:34:08 +01:00
parent 2c4369159b
commit d688a64e36
52 changed files with 349 additions and 24 deletions
+7
View File
@@ -54,6 +54,13 @@ authenticateWithOidc.failure = (error, terms) => ({
}, },
}); });
authenticateWithOidc.debug = (logs) => ({
type: ActionTypes.WITH_OIDC_AUTHENTICATE__DEBUG,
payload: {
logs,
},
});
const clearAuthenticateError = () => ({ const clearAuthenticateError = () => ({
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR, type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
payload: {}, payload: {},
+3
View File
@@ -13,6 +13,8 @@ const createAccessToken = (data, headers) =>
const exchangeForAccessTokenWithOidc = (data, headers) => const exchangeForAccessTokenWithOidc = (data, headers) =>
http.post('/access-tokens/exchange-with-oidc?withHttpOnlyToken=true', 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? // TODO: rename?
const acceptTerms = (data, headers) => http.post('/access-tokens/accept-terms', data, headers); const acceptTerms = (data, headers) => http.post('/access-tokens/accept-terms', data, headers);
@@ -24,6 +26,7 @@ const deleteCurrentAccessToken = (headers) => http.delete('/access-tokens/me', u
export default { export default {
createAccessToken, createAccessToken,
exchangeForAccessTokenWithOidc, exchangeForAccessTokenWithOidc,
debugOidc,
acceptTerms, acceptTerms,
revokePendingToken, revokePendingToken,
deleteCurrentAccessToken, deleteCurrentAccessToken,
+24 -10
View File
@@ -8,7 +8,8 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import { Button, Divider, Form, Grid, Header, Message } from 'semantic-ui-react'; import TextareaAutosize from 'react-textarea-autosize';
import { Button, Divider, Form, Grid, Header, Message, TextArea } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks'; import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { Input } from '../../../lib/custom-ui'; import { Input } from '../../../lib/custom-ui';
@@ -23,7 +24,7 @@ import logo from '../../../assets/images/logo.png';
import styles from './Content.module.scss'; import styles from './Content.module.scss';
const createMessage = (error) => { const createMessage = (error, isDebug) => {
if (!error) { if (!error) {
return error; return error;
} }
@@ -82,7 +83,7 @@ const createMessage = (error) => {
default: default:
return { return {
type: 'warning', type: 'warning',
content: 'common.unknownError', content: isDebug ? error.message : 'common.unknownError',
}; };
} }
}; };
@@ -95,6 +96,7 @@ const Content = React.memo(() => {
isSubmitting, isSubmitting,
isSubmittingWithOidc, isSubmittingWithOidc,
error, error,
debugLogs,
step, step,
} = useSelector(selectors.selectAuthenticateForm); } = useSelector(selectors.selectAuthenticateForm);
@@ -124,7 +126,11 @@ const Content = React.memo(() => {
return initialData; return initialData;
}); });
const message = useMemo(() => createMessage(error), [error]); 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 [focusPasswordFieldState, focusPasswordField] = useToggle(); const [focusPasswordFieldState, focusPasswordField] = useToggle();
const [emailOrUsernameFieldRef, handleEmailOrUsernameFieldRef] = useNestedRef('inputRef'); const [emailOrUsernameFieldRef, handleEmailOrUsernameFieldRef] = useNestedRef('inputRef');
@@ -157,14 +163,11 @@ const Content = React.memo(() => {
dispatch(entryActions.clearAuthenticateError()); dispatch(entryActions.clearAuthenticateError());
}, [dispatch]); }, [dispatch]);
const withOidc = !!bootstrap.oidc;
const isOidcEnforced = withOidc && bootstrap.oidc.isEnforced;
useEffect(() => { useEffect(() => {
if (!isOidcEnforced) { if (!isOidcEnforced) {
emailOrUsernameFieldRef.current.focus(); emailOrUsernameFieldRef.current.focus();
} }
}, [emailOrUsernameFieldRef, isOidcEnforced]); }, [isOidcEnforced, emailOrUsernameFieldRef]);
useDidUpdate(() => { useDidUpdate(() => {
if (wasSubmitting && !isSubmitting && error) { if (wasSubmitting && !isSubmitting && error) {
@@ -269,16 +272,27 @@ const Content = React.memo(() => {
</> </>
)} )}
{withOidc && ( {withOidc && (
<>
<Button <Button
fluid fluid
primary={isOidcEnforced} primary={isOidcDebug ? undefined : isOidcEnforced}
color={isOidcDebug ? 'orange' : undefined}
icon={isOidcEnforced ? 'right arrow' : undefined} icon={isOidcEnforced ? 'right arrow' : undefined}
labelPosition={isOidcEnforced ? 'right' : undefined} labelPosition={isOidcEnforced ? 'right' : undefined}
content={t('action.logInWithSso')} content={isOidcDebug ? t('action.debugSso') : t('action.logInWithSso')}
loading={isSubmittingWithOidc} loading={isSubmittingWithOidc}
disabled={isSubmitting || isSubmittingWithOidc} disabled={isSubmitting || isSubmittingWithOidc}
onClick={handleAuthenticateWithOidcClick} onClick={handleAuthenticateWithOidcClick}
/> />
{debugLogs && (
<TextArea
readOnly
as={TextareaAutosize}
value={debugLogs.join('\n')}
className={styles.debugLog}
/>
)}
</>
)} )}
</div> </div>
<div className={styles.poweredBy}> <div className={styles.poweredBy}>
@@ -19,6 +19,16 @@
width: 100%; width: 100%;
} }
.debugLog {
border: 1px solid rgba(9, 30, 66, 0.13);
border-radius: 3px;
color: #333;
line-height: 1.4;
margin-top: 16px;
padding: 8px 12px;
width: 100%;
}
.divider { .divider {
font-weight: normal; font-weight: normal;
} }
+1
View File
@@ -29,6 +29,7 @@ export default {
WITH_OIDC_AUTHENTICATE: 'WITH_OIDC_AUTHENTICATE', WITH_OIDC_AUTHENTICATE: 'WITH_OIDC_AUTHENTICATE',
WITH_OIDC_AUTHENTICATE__SUCCESS: 'WITH_OIDC_AUTHENTICATE__SUCCESS', WITH_OIDC_AUTHENTICATE__SUCCESS: 'WITH_OIDC_AUTHENTICATE__SUCCESS',
WITH_OIDC_AUTHENTICATE__FAILURE: 'WITH_OIDC_AUTHENTICATE__FAILURE', WITH_OIDC_AUTHENTICATE__FAILURE: 'WITH_OIDC_AUTHENTICATE__FAILURE',
WITH_OIDC_AUTHENTICATE__DEBUG: 'WITH_OIDC_AUTHENTICATE__DEBUG',
AUTHENTICATE_ERROR_CLEAR: 'AUTHENTICATE_ERROR_CLEAR', AUTHENTICATE_ERROR_CLEAR: 'AUTHENTICATE_ERROR_CLEAR',
TERMS_ACCEPT: 'TERMS_ACCEPT', TERMS_ACCEPT: 'TERMS_ACCEPT',
TERMS_ACCEPT__SUCCESS: 'TERMS_ACCEPT__SUCCESS', TERMS_ACCEPT__SUCCESS: 'TERMS_ACCEPT__SUCCESS',
+1
View File
@@ -25,6 +25,7 @@ export default {
action: { action: {
cancelAndClose: 'إلغاء وإغلاق', cancelAndClose: 'إلغاء وإغلاق',
continue: 'متابعة', continue: 'متابعة',
debugSso: 'تصحيح أخطاء تسجيل الدخول الموحد',
goBack: 'العودة', goBack: 'العودة',
goHome: 'الذهاب للرئيسية', goHome: 'الذهاب للرئيسية',
logIn: 'تسجيل الدخول', logIn: 'تسجيل الدخول',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Отказ и затваряне', cancelAndClose: 'Отказ и затваряне',
continue: 'Продължи', continue: 'Продължи',
debugSso: 'Дебъгване на SSO',
goBack: 'Назад', goBack: 'Назад',
goHome: 'Към началото', goHome: 'Към началото',
logIn: 'Вход', logIn: 'Вход',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Cancel·lar i tancar', cancelAndClose: 'Cancel·lar i tancar',
continue: 'Continuar', continue: 'Continuar',
debugSso: 'Depurar SSO',
goBack: 'Tornar', goBack: 'Tornar',
goHome: "Anar a l'inici", goHome: "Anar a l'inici",
logIn: 'Iniciar sessió', logIn: 'Iniciar sessió',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Zrušit a zavřít', cancelAndClose: 'Zrušit a zavřít',
continue: 'Pokračovat', continue: 'Pokračovat',
debugSso: 'Ladit SSO',
goBack: 'Zpět', goBack: 'Zpět',
goHome: 'Domů', goHome: 'Domů',
logIn: 'Přihlásit se', logIn: 'Přihlásit se',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Annuller og luk', cancelAndClose: 'Annuller og luk',
continue: 'Fortsæt', continue: 'Fortsæt',
debugSso: 'Fejlfind SSO',
goBack: 'Gå tilbage', goBack: 'Gå tilbage',
goHome: 'Gå hjem', goHome: 'Gå hjem',
logIn: 'Log på', logIn: 'Log på',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Abbrechen und schließen', cancelAndClose: 'Abbrechen und schließen',
continue: 'Fortfahren', continue: 'Fortfahren',
debugSso: 'SSO debuggen',
goBack: 'Zurück gehen', goBack: 'Zurück gehen',
goHome: 'Zur Startseite', goHome: 'Zur Startseite',
logIn: 'Einloggen', logIn: 'Einloggen',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Ακύρωση και κλείσιμο', cancelAndClose: 'Ακύρωση και κλείσιμο',
continue: 'Συνέχεια', continue: 'Συνέχεια',
debugSso: 'Αποσφαλμάτωση SSO',
goBack: 'Επιστροφή', goBack: 'Επιστροφή',
goHome: 'Αρχική σελίδα', goHome: 'Αρχική σελίδα',
logIn: 'Σύνδεση', logIn: 'Σύνδεση',
+1
View File
@@ -25,6 +25,7 @@ export default {
action: { action: {
cancelAndClose: 'Cancel and close', cancelAndClose: 'Cancel and close',
continue: 'Continue', continue: 'Continue',
debugSso: 'Debug SSO',
goBack: 'Go back', goBack: 'Go back',
goHome: 'Go home', goHome: 'Go home',
logIn: 'Log in', logIn: 'Log in',
+1
View File
@@ -25,6 +25,7 @@ export default {
action: { action: {
cancelAndClose: 'Cancel and close', cancelAndClose: 'Cancel and close',
continue: 'Continue', continue: 'Continue',
debugSso: 'Debug SSO',
goBack: 'Go back', goBack: 'Go back',
goHome: 'Go home', goHome: 'Go home',
logIn: 'Log in', logIn: 'Log in',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Cancelar y cerrar', cancelAndClose: 'Cancelar y cerrar',
continue: 'Continuar', continue: 'Continuar',
debugSso: 'Depurar SSO',
goBack: 'Volver', goBack: 'Volver',
goHome: 'Ir al inicio', goHome: 'Ir al inicio',
logIn: 'Iniciar sesión', logIn: 'Iniciar sesión',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Tühista ja sulge', cancelAndClose: 'Tühista ja sulge',
continue: 'Jätka', continue: 'Jätka',
debugSso: 'Siluda SSO',
goBack: 'Tagasi', goBack: 'Tagasi',
goHome: 'Koju', goHome: 'Koju',
logIn: 'Logi sisse', logIn: 'Logi sisse',
+1
View File
@@ -25,6 +25,7 @@ export default {
action: { action: {
cancelAndClose: 'لغو و بستن', cancelAndClose: 'لغو و بستن',
continue: 'ادامه', continue: 'ادامه',
debugSso: 'اشکال‌زدایی SSO',
goBack: 'بازگشت', goBack: 'بازگشت',
goHome: 'رفتن به خانه', goHome: 'رفتن به خانه',
logIn: 'ورود', logIn: 'ورود',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Peruuta ja sulje', cancelAndClose: 'Peruuta ja sulje',
continue: 'Jatka', continue: 'Jatka',
debugSso: 'Korjaa SSO-virheitä',
goBack: 'Takaisin', goBack: 'Takaisin',
goHome: 'Kotiin', goHome: 'Kotiin',
logIn: 'Kirjaudu sisään', logIn: 'Kirjaudu sisään',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Annuler et fermer', cancelAndClose: 'Annuler et fermer',
continue: 'Continuer', continue: 'Continuer',
debugSso: 'Déboguer le SSO',
goBack: 'Retour', goBack: 'Retour',
goHome: "Aller à l'accueil", goHome: "Aller à l'accueil",
logIn: 'Se connecter', logIn: 'Se connecter',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Mégse és bezárás', cancelAndClose: 'Mégse és bezárás',
continue: 'Folytatás', continue: 'Folytatás',
debugSso: 'SSO hibakeresése',
goBack: 'Vissza', goBack: 'Vissza',
goHome: 'Kezdőlapra', goHome: 'Kezdőlapra',
logIn: 'Belépés', logIn: 'Belépés',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Batal dan tutup', cancelAndClose: 'Batal dan tutup',
continue: 'Lanjutkan', continue: 'Lanjutkan',
debugSso: 'Debug SSO',
goBack: 'Kembali', goBack: 'Kembali',
goHome: 'Ke beranda', goHome: 'Ke beranda',
logIn: 'Masuk', logIn: 'Masuk',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Annulla e chiudi', cancelAndClose: 'Annulla e chiudi',
continue: 'Continua', continue: 'Continua',
debugSso: 'Debug SSO',
goBack: 'Torna indietro', goBack: 'Torna indietro',
goHome: 'Vai alla home', goHome: 'Vai alla home',
logIn: 'Accedi', logIn: 'Accedi',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'キャンセルして閉じる', cancelAndClose: 'キャンセルして閉じる',
continue: '続行', continue: '続行',
debugSso: 'SSOをデバッグ',
goBack: '戻る', goBack: '戻る',
goHome: 'ホームへ', goHome: 'ホームへ',
logIn: 'ログイン', logIn: 'ログイン',
+1
View File
@@ -25,6 +25,7 @@ export default {
action: { action: {
cancelAndClose: '취소 후 닫기', cancelAndClose: '취소 후 닫기',
continue: '계속', continue: '계속',
debugSso: 'SSO 디버그',
goBack: '뒤로 가기', goBack: '뒤로 가기',
goHome: '홈으로 가기', goHome: '홈으로 가기',
logIn: '로그인', logIn: '로그인',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Annuleren en sluiten', cancelAndClose: 'Annuleren en sluiten',
continue: 'Doorgaan', continue: 'Doorgaan',
debugSso: 'SSO debuggen',
goBack: 'Terug gaan', goBack: 'Terug gaan',
goHome: 'Naar startpagina', goHome: 'Naar startpagina',
logIn: 'Inloggen', logIn: 'Inloggen',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Anuluj i zamknij', cancelAndClose: 'Anuluj i zamknij',
continue: 'Kontynuuj', continue: 'Kontynuuj',
debugSso: 'Debuguj SSO',
goBack: 'Wróć', goBack: 'Wróć',
goHome: 'Idź do domu', goHome: 'Idź do domu',
logIn: 'Zaloguj', logIn: 'Zaloguj',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Cancelar e fechar', cancelAndClose: 'Cancelar e fechar',
continue: 'Continuar', continue: 'Continuar',
debugSso: 'Depurar SSO',
goBack: 'Voltar', goBack: 'Voltar',
goHome: 'Ir para início', goHome: 'Ir para início',
logIn: 'Entrar', logIn: 'Entrar',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Cancelar e fechar', cancelAndClose: 'Cancelar e fechar',
continue: 'Continuar', continue: 'Continuar',
debugSso: 'Depurar SSO',
goBack: 'Voltar', goBack: 'Voltar',
goHome: 'Ir para início', goHome: 'Ir para início',
logIn: 'Iniciar sessão', logIn: 'Iniciar sessão',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Anulează și închide', cancelAndClose: 'Anulează și închide',
continue: 'Continuă', continue: 'Continuă',
debugSso: 'Depanează SSO',
goBack: 'Înapoi', goBack: 'Înapoi',
goHome: 'Acasă', goHome: 'Acasă',
logIn: 'Autentificarea', logIn: 'Autentificarea',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Отменить и закрыть', cancelAndClose: 'Отменить и закрыть',
continue: 'Продолжить', continue: 'Продолжить',
debugSso: 'Отладить SSO',
goBack: 'Назад', goBack: 'Назад',
goHome: 'На главную', goHome: 'На главную',
logIn: 'Войти', logIn: 'Войти',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Zrušiť a zavrieť', cancelAndClose: 'Zrušiť a zavrieť',
continue: 'Pokračovať', continue: 'Pokračovať',
debugSso: 'Ladiť SSO',
goBack: 'Ísť späť', goBack: 'Ísť späť',
goHome: 'Ísť domov', goHome: 'Ísť domov',
logIn: 'Prihlásiť sa', logIn: 'Prihlásiť sa',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Откажи и затвори', cancelAndClose: 'Откажи и затвори',
continue: 'Настави', continue: 'Настави',
debugSso: 'Дебагуј SSO',
goBack: 'Назад', goBack: 'Назад',
goHome: 'Иди кући', goHome: 'Иди кући',
logIn: 'Пријава', logIn: 'Пријава',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Otkaži i zatvori', cancelAndClose: 'Otkaži i zatvori',
continue: 'Nastavi', continue: 'Nastavi',
debugSso: 'Debaguj SSO',
goBack: 'Nazad', goBack: 'Nazad',
goHome: 'Idi kući', goHome: 'Idi kući',
logIn: 'Prijava', logIn: 'Prijava',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Avbryt och stäng', cancelAndClose: 'Avbryt och stäng',
continue: 'Fortsätt', continue: 'Fortsätt',
debugSso: 'Felsök SSO',
goBack: 'Gå tillbaka', goBack: 'Gå tillbaka',
goHome: 'Gå hem', goHome: 'Gå hem',
logIn: 'Logga in', logIn: 'Logga in',
+1
View File
@@ -25,6 +25,7 @@ export default {
action: { action: {
cancelAndClose: 'İptal et ve kapat', cancelAndClose: 'İptal et ve kapat',
continue: 'Devam et', continue: 'Devam et',
debugSso: 'SSO hatalarını ayıkla',
goBack: 'Geri dön', goBack: 'Geri dön',
goHome: 'Ana sayfaya git', goHome: 'Ana sayfaya git',
logIn: 'Giriş yap', logIn: 'Giriş yap',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Скасувати та закрити', cancelAndClose: 'Скасувати та закрити',
continue: 'Продовжити', continue: 'Продовжити',
debugSso: 'Відлагодити SSO',
goBack: 'Назад', goBack: 'Назад',
goHome: 'На головну', goHome: 'На головну',
logIn: 'Увійти', logIn: 'Увійти',
+1
View File
@@ -26,6 +26,7 @@ export default {
action: { action: {
cancelAndClose: 'Bekor qilish va yopish', cancelAndClose: 'Bekor qilish va yopish',
continue: 'Davom etish', continue: 'Davom etish',
debugSso: 'SSO ni tuzatish',
goBack: 'Orqaga', goBack: 'Orqaga',
goHome: 'Bosh sahifaga', goHome: 'Bosh sahifaga',
logIn: 'Kirish', logIn: 'Kirish',
+1
View File
@@ -25,6 +25,7 @@ export default {
action: { action: {
cancelAndClose: 'Hủy và đóng', cancelAndClose: 'Hủy và đóng',
continue: 'Tiếp tục', continue: 'Tiếp tục',
debugSso: 'Gỡ lỗi SSO',
goBack: 'Quay lại', goBack: 'Quay lại',
goHome: 'Về trang chủ', goHome: 'Về trang chủ',
logIn: 'Đăng nhập', logIn: 'Đăng nhập',
+1
View File
@@ -25,6 +25,7 @@ export default {
action: { action: {
cancelAndClose: '取消并关闭', cancelAndClose: '取消并关闭',
continue: '继续', continue: '继续',
debugSso: '调试SSO',
goBack: '返回', goBack: '返回',
goHome: '回到首页', goHome: '回到首页',
logIn: '登录', logIn: '登录',
+1
View File
@@ -25,6 +25,7 @@ export default {
action: { action: {
cancelAndClose: '取消並關閉', cancelAndClose: '取消並關閉',
continue: '繼續', continue: '繼續',
debugSso: '偵錯SSO',
goBack: '返回', goBack: '返回',
goHome: '回到首頁', goHome: '回到首頁',
logIn: '登入', logIn: '登入',
@@ -16,6 +16,7 @@ const initialState = {
isSubmitting: false, isSubmitting: false,
isSubmittingWithOidc: false, isSubmittingWithOidc: false,
error: null, error: null,
debugLogs: null,
pendingToken: null, pendingToken: null,
step: null, step: null,
termsForm: { termsForm: {
@@ -91,6 +92,12 @@ export default (state = initialState, { type, payload }) => {
isSubmittingWithOidc: false, isSubmittingWithOidc: false,
error: payload.error, error: payload.error,
}; };
case ActionTypes.WITH_OIDC_AUTHENTICATE__DEBUG:
return {
...state,
isSubmittingWithOidc: false,
debugLogs: payload.logs,
};
case ActionTypes.AUTHENTICATE_ERROR_CLEAR: case ActionTypes.AUTHENTICATE_ERROR_CLEAR:
return { return {
...state, ...state,
+14
View File
@@ -106,6 +106,20 @@ export function* authenticateWithOidcCallback() {
return; 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; let accessToken;
try { try {
({ item: accessToken } = yield call(api.exchangeForAccessTokenWithOidc, { ({ item: accessToken } = yield call(api.exchangeForAccessTokenWithOidc, {
+1
View File
@@ -77,6 +77,7 @@ services:
# - OIDC_IGNORE_USERNAME=true # - OIDC_IGNORE_USERNAME=true
# - OIDC_IGNORE_ROLES=true # - OIDC_IGNORE_ROLES=true
# - OIDC_ENFORCED=true # - OIDC_ENFORCED=true
# - OIDC_DEBUG=true
# Email Notifications (https://nodemailer.com/smtp/) # Email Notifications (https://nodemailer.com/smtp/)
# These values override and disable configuration in the UI if set. # These values override and disable configuration in the UI if set.
+1
View File
@@ -95,6 +95,7 @@ services:
# - OIDC_IGNORE_USERNAME=true # - OIDC_IGNORE_USERNAME=true
# - OIDC_IGNORE_ROLES=true # - OIDC_IGNORE_ROLES=true
# - OIDC_ENFORCED=true # - OIDC_ENFORCED=true
# - OIDC_DEBUG=true
# Email Notifications (https://nodemailer.com/smtp/) # Email Notifications (https://nodemailer.com/smtp/)
# These values override and disable configuration in the UI if set. # These values override and disable configuration in the UI if set.
+1
View File
@@ -68,6 +68,7 @@ SECRET_KEY=notsecretkey
# OIDC_IGNORE_USERNAME=true # OIDC_IGNORE_USERNAME=true
# OIDC_IGNORE_ROLES=true # OIDC_IGNORE_ROLES=true
# OIDC_ENFORCED=true # OIDC_ENFORCED=true
# OIDC_DEBUG=true
# Email Notifications (https://nodemailer.com/smtp/) # Email Notifications (https://nodemailer.com/smtp/)
# These values override and disable configuration in the UI if set. # These values override and disable configuration in the UI if set.
@@ -0,0 +1,223 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
};
module.exports = {
inputs: {
code: {
type: 'string',
maxLength: 2048,
required: true,
},
nonce: {
type: 'string',
maxLength: 1024,
required: true,
},
},
exits: {
notEnoughRights: {
responseType: 'forbidden',
},
},
async fn(inputs) {
if (!sails.config.custom.oidcDebug) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const logs = ['🔐 Starting OIDC debug flow...', ''];
const client = await sails.hooks.oidc.getClient();
if (!client) {
logs.push('❌ OIDC client is not initialized.');
logs.push('💡 Hint: Check your OIDC issuer and client configuration.');
return {
item: null,
included: {
logs,
},
};
}
let tokenSet;
try {
logs.push('🔄 Exchanging authorization code...');
if (sails.config.custom.oidcUseOauthCallback) {
tokenSet = await client.oauthCallback(
sails.config.custom.oidcRedirectUri,
{
iss: sails.config.custom.oidcIssuer,
code: inputs.code,
},
{ nonce: inputs.nonce },
);
} else {
tokenSet = await client.callback(
sails.config.custom.oidcRedirectUri,
{
iss: sails.config.custom.oidcIssuer,
code: inputs.code,
},
{ nonce: inputs.nonce },
);
}
logs.push('✅ Authorization code exchanged successfully.', '');
} catch (error) {
logs.push('❌ Failed to exchange authorization code.');
logs.push(`💬 Reason: ${error.message || error.toString()}`);
logs.push('💡 Hint: Check redirect URI, client secret, and nonce handling.');
return {
item: null,
included: {
logs,
},
};
}
if (sails.config.custom.oidcClaimsSource === 'id_token') {
logs.push('📥 Extracting claims from ID token...');
try {
claims = tokenSet.claims();
logs.push('✅ Claims extracted successfully.', '');
} catch (error) {
logs.push('❌ Failed to extract user claims.');
logs.push(`💬 Reason: ${error.message || error.toString()}`);
return {
item: null,
included: {
logs,
},
};
}
} else {
logs.push('📥 Fetching claims from userinfo endpoint...');
try {
claims = await client.userinfo(tokenSet);
logs.push('✅ Claims fetched successfully.', '');
} catch (error) {
logs.push('❌ Failed to fetch user claims.');
if (error instanceof SyntaxError && error.message.includes('Unexpected token e in JSON')) {
logs.push('💬 Reason: Userinfo response is signed or not JSON.');
logs.push(
'💡 Hint: Try configuring userinfo signed response algorithm or switch to ID token claims.',
);
} else {
logs.push(`💬 Reason: ${error.message || error.toString()}`);
}
return {
item: null,
included: {
logs,
},
};
}
}
logs.push('📦 Raw claims received:', JSON.stringify(claims, null, 2), '');
logs.push('🧩 Evaluating claim mappings...', '');
const mappings = {
email: {
attribute: sails.config.custom.oidcEmailAttribute,
value: _.get(claims, sails.config.custom.oidcEmailAttribute),
},
name: {
attribute: sails.config.custom.oidcNameAttribute,
value: _.get(claims, sails.config.custom.oidcNameAttribute),
},
username: sails.config.custom.oidcIgnoreUsername
? undefined
: {
attribute: sails.config.custom.oidcUsernameAttribute,
value: _.get(claims, sails.config.custom.oidcUsernameAttribute),
},
roles: sails.config.custom.oidcIgnoreRoles
? undefined
: {
attribute: sails.config.custom.oidcRolesAttribute,
value: _.get(claims, sails.config.custom.oidcRolesAttribute),
},
};
logs.push('📋 Mapping result:', JSON.stringify(mappings, null, 2), '');
if (!mappings.email.value) {
logs.push('❌ Email not resolved.');
logs.push('💡 Hint: Check email attribute mapping.', '');
}
if (!mappings.name.value) {
logs.push('❌ Name not resolved.');
logs.push('💡 Hint: Check name attribute mapping.', '');
}
if (!sails.config.custom.oidcIgnoreUsername) {
if (!mappings.username.value) {
logs.push('⚠️ Username not resolved.');
logs.push('💡 Hint: Check username attribute mapping.', '');
}
}
if (!sails.config.custom.oidcIgnoreRoles) {
if (!Array.isArray(mappings.roles.value) || mappings.roles.value.length === 0) {
logs.push('⚠️ Roles not resolved or empty.');
logs.push('💡 Hint: Check roles attribute mapping or IdP role configuration.', '');
} else {
logs.push('🎭 Resolving user role from OIDC roles...');
// Use a Set here to avoid quadratic time complexity
const claimsRolesSet = new Set(mappings.roles.value);
const foundRole = [User.Roles.ADMIN, User.Roles.PROJECT_OWNER, User.Roles.BOARD_USER].find(
(roleItem) => {
const configRoles = sails.config.custom[`oidc${_.upperFirst(roleItem)}Roles`];
if (configRoles.includes('*')) {
return true;
}
return configRoles.some((configRole) => claimsRolesSet.has(configRole));
},
);
if (foundRole) {
logs.push(`✅ Matched user role → ${_.lowerCase(foundRole)}`, '');
} else {
logs.push('⚠️ No user role matched configured OIDC roles.');
logs.push('💡 Hint: Check role matching settings.', '');
}
}
}
if (mappings.email.value && mappings.name.value) {
logs.push('🎉 OIDC debug completed successfully.');
} else {
logs.push('🛑 OIDC debug detected missing required attributes.');
}
return {
item: null,
included: {
logs,
},
};
},
};
+4 -5
View File
@@ -61,9 +61,8 @@ module.exports = {
return Errors.NOT_AVAILABLE; return Errors.NOT_AVAILABLE;
} }
const logs = []; const logs = ['📧 Sending test email...'];
try { try {
logs.push('📧 Sending test email...');
/* eslint-disable no-underscore-dangle */ /* eslint-disable no-underscore-dangle */
const info = await transporter.sendMail({ const info = await transporter.sendMail({
to: currentUser.email, to: currentUser.email,
@@ -72,16 +71,16 @@ module.exports = {
html: this.req.i18n.__('This is a <i>test</i> <b>html</b> <code>message</code>!'), html: this.req.i18n.__('This is a <i>test</i> <b>html</b> <code>message</code>!'),
}); });
/* eslint-enable no-underscore-dangle */ /* eslint-enable no-underscore-dangle */
logs.push('✅ Email sent successfully!', ''); logs.push('✅ Email sent successfully.', '');
logs.push(`📬 Message ID: ${info.messageId}`); logs.push(`📬 Message ID: ${info.messageId}`);
if (info.response) { if (info.response) {
logs.push(`📤 Server response: ${info.response.trim()}`); logs.push(`📤 Server response: ${info.response.trim()}`);
} }
logs.push('', '🎉 Your configuration is working correctly!'); logs.push('', '🎉 Your configuration is working correctly.');
} catch (error) { } catch (error) {
logs.push('❌ Failed to send email!', ''); logs.push('❌ Failed to send email.', '');
if (error.code) { if (error.code) {
logs.push(`⚠️ Error code: ${error.code}`); logs.push(`⚠️ Error code: ${error.code}`);
@@ -106,6 +106,7 @@ module.exports = {
if (configRoles.includes('*')) { if (configRoles.includes('*')) {
return true; return true;
} }
return configRoles.some((configRole) => claimsRolesSet.has(configRole)); return configRoles.some((configRole) => claimsRolesSet.has(configRole));
}, },
); );
+6 -1
View File
@@ -89,11 +89,16 @@ module.exports = function defineOidcHook(sails) {
authorizationUrlParams.response_mode = sails.config.custom.oidcResponseMode; authorizationUrlParams.response_mode = sails.config.custom.oidcResponseMode;
} }
return { const bootstrap = {
authorizationUrl: instance.authorizationUrl(authorizationUrlParams), authorizationUrl: instance.authorizationUrl(authorizationUrlParams),
endSessionUrl: instance.issuer.end_session_endpoint ? instance.endSessionUrl({}) : null, endSessionUrl: instance.issuer.end_session_endpoint ? instance.endSessionUrl({}) : null,
isEnforced: sails.config.custom.oidcEnforced, isEnforced: sails.config.custom.oidcEnforced,
}; };
if (sails.config.custom.oidcDebug) {
bootstrap.debug = true;
}
return bootstrap;
}, },
isEnabled() { isEnabled() {
+1
View File
@@ -94,6 +94,7 @@ module.exports.custom = {
oidcIgnoreUsername: process.env.OIDC_IGNORE_USERNAME === 'true', oidcIgnoreUsername: process.env.OIDC_IGNORE_USERNAME === 'true',
oidcIgnoreRoles: process.env.OIDC_IGNORE_ROLES === 'true', oidcIgnoreRoles: process.env.OIDC_IGNORE_ROLES === 'true',
oidcEnforced: process.env.OIDC_ENFORCED === 'true', oidcEnforced: process.env.OIDC_ENFORCED === 'true',
oidcDebug: process.env.OIDC_DEBUG === 'true',
// TODO: move client base url to environment variable? // TODO: move client base url to environment variable?
oidcRedirectUri: `${ oidcRedirectUri: `${
+1
View File
@@ -48,6 +48,7 @@ module.exports.policies = {
'terms/show': true, 'terms/show': true,
'access-tokens/create': true, 'access-tokens/create': true,
'access-tokens/exchange-with-oidc': true, 'access-tokens/exchange-with-oidc': true,
'access-tokens/debug-oidc': true,
'access-tokens/accept-terms': true, 'access-tokens/accept-terms': true,
'access-tokens/revoke-pending-token': true, 'access-tokens/revoke-pending-token': true,
}; };
+1
View File
@@ -81,6 +81,7 @@ module.exports.routes = {
'POST /api/access-tokens': 'access-tokens/create', 'POST /api/access-tokens': 'access-tokens/create',
'POST /api/access-tokens/exchange-with-oidc': 'access-tokens/exchange-with-oidc', 'POST /api/access-tokens/exchange-with-oidc': 'access-tokens/exchange-with-oidc',
'POST /api/access-tokens/debug-oidc': 'access-tokens/debug-oidc',
'POST /api/access-tokens/accept-terms': 'access-tokens/accept-terms', 'POST /api/access-tokens/accept-terms': 'access-tokens/accept-terms',
'POST /api/access-tokens/revoke-pending-token': 'access-tokens/revoke-pending-token', 'POST /api/access-tokens/revoke-pending-token': 'access-tokens/revoke-pending-token',
'DELETE /api/access-tokens/me': 'access-tokens/delete', 'DELETE /api/access-tokens/me': 'access-tokens/delete',