diff --git a/src/backend/src/routers/auth/oidc.js b/src/backend/src/routers/auth/oidc.js index 2e1fca2dc..b1a8343f8 100644 --- a/src/backend/src/routers/auth/oidc.js +++ b/src/backend/src/routers/auth/oidc.js @@ -37,6 +37,32 @@ const OIDC_CALLBACK_ERROR_RESPONSES = { [COULD_NOT_GET_USER_INFO]: { status: 401, message: 'Could not get user info.' }, }; +const OIDC_ERROR_REDIRECT_MAP = { + login: { + account_not_found: 'signup', + other: 'login', + }, + signup: { + account_already_exists: 'login', + other: 'signup', + }, +}; + +/** + * The error redirect URL is the origin with a query parameter included to + * display an error message on the login or signup page. + * @param {string} sourceFlow - 'login' or 'signup' + * @param {string} errorCondition - string that identifies the error message + * @param {string} message - default error message (before i18n) + * @returns {string} URL to redirect to + */ +function buildOIDCErrorRedirectUrl (sourceFlow, errorCondition, message) { + const targetFlow = OIDC_ERROR_REDIRECT_MAP[sourceFlow]?.[errorCondition] ?? sourceFlow; + const origin = (config.origin || '').replace(/\/$/, '') || '/'; + const params = new URLSearchParams({ action: targetFlow, auth_error: '1', message: message || 'Something went wrong.' }); + return `${origin}/?${params.toString()}`; +} + /** Returns { session_token, target } for the caller to set cookie and redirect. */ const finishOidcSuccess_ = async (req, res, user, stateDecoded) => { const svc_auth = req.services.get('auth'); @@ -135,16 +161,16 @@ router.get('/auth/oidc/callback/login', async (req, res) => { const callbackRedirectUri = svc_oidc.getCallbackUrlForFlow('login'); const result = await processOIDCCallbackRequest_(req, callbackRedirectUri); if ( result.error ) { - const { status, message } = OIDC_CALLBACK_ERROR_RESPONSES[result.error]; - return res.status(status).send(message); + const { message } = OIDC_CALLBACK_ERROR_RESPONSES[result.error]; + return res.redirect(302, buildOIDCErrorRedirectUrl('login', 'other', message)); } const { provider, userinfo, stateDecoded } = result; const user = await svc_oidc.findUserByProviderSub(provider, userinfo.sub); if ( ! user ) { - return res.status(400).send('No account found. Sign up first.'); + return res.redirect(302, buildOIDCErrorRedirectUrl('login', 'account_not_found', 'No account found. Sign up first.')); } if ( user.suspended ) { - return res.status(401).send('This account is suspended.'); + return res.redirect(302, buildOIDCErrorRedirectUrl('login', 'other', 'This account is suspended.')); } const { session_token, target } = await finishOidcSuccess_(req, res, user, stateDecoded); res.cookie(config.cookie_name, session_token, { @@ -168,17 +194,17 @@ router.get('/auth/oidc/callback/signup', async (req, res) => { const callbackRedirectUri = svc_oidc.getCallbackUrlForFlow('signup'); const result = await processOIDCCallbackRequest_(req, callbackRedirectUri); if ( result.error ) { - const { status, message } = OIDC_CALLBACK_ERROR_RESPONSES[result.error]; - return res.status(status).send(message); + const { message } = OIDC_CALLBACK_ERROR_RESPONSES[result.error]; + return res.redirect(302, buildOIDCErrorRedirectUrl('signup', 'other', message)); } const { provider, userinfo, stateDecoded } = result; const existingUser = await svc_oidc.findUserByProviderSub(provider, userinfo.sub); if ( existingUser ) { - return res.status(400).send('Account already exists. Log in instead.'); + return res.redirect(302, buildOIDCErrorRedirectUrl('signup', 'account_already_exists', 'Account already exists. Log in instead.')); } const outcome = await svc_oidc.createUserFromOIDC(provider, userinfo); if ( outcome.failed ) { - return res.status(400).send(outcome.userMessage); + return res.redirect(302, buildOIDCErrorRedirectUrl('signup', 'other', outcome.userMessage)); } const user = await get_user({ id: outcome.infoObject.user_id }); const { session_token, target } = await finishOidcSuccess_(req, res, user, stateDecoded); @@ -217,9 +243,11 @@ router.get('/auth/oidc/callback/revalidate', async (req, res) => { if ( user.id !== stateDecoded.user_id ) { return res.status(403).send('Wrong account. Sign in with the account linked to this session.'); } - const token = jwt.sign({ user_id: user.id, purpose: 'revalidate' }, - config.jwt_secret, - { expiresIn: REVALIDATION_EXPIRY_SEC }); + const token = jwt.sign( + { user_id: user.id, purpose: 'revalidate' }, + config.jwt_secret, + { expiresIn: REVALIDATION_EXPIRY_SEC }, + ); res.cookie(REVALIDATION_COOKIE_NAME, token, { sameSite: 'lax', secure: true, diff --git a/src/gui/src/UI/UIWindowLogin.js b/src/gui/src/UI/UIWindowLogin.js index c944c4b8c..f7359f722 100644 --- a/src/gui/src/UI/UIWindowLogin.js +++ b/src/gui/src/UI/UIWindowLogin.js @@ -124,6 +124,9 @@ async function UIWindowLogin (options) { resolve(false); }, onAppend: function (this_window) { + if ( options.authError ) { + $(this_window).find('.login-error-msg').html(options.authError).fadeIn(); + } $(this_window).find('.email_or_username').get(0).focus({ preventScroll: true }); }, window_class: 'window-login', @@ -243,7 +246,7 @@ async function UIWindowLogin (options) { }), new CodeEntryView({ _ref: me => code_entry = me, - async ['property.value'] (value, { component }) { + async 'property.value' (value, { component }) { let error_i18n_key = 'something_went_wrong'; if ( ! value ) return; try { @@ -293,7 +296,7 @@ async function UIWindowLogin (options) { }, }), ], - ['event.focus'] () { + 'event.focus' () { code_entry.focus(); }, }); @@ -311,7 +314,7 @@ async function UIWindowLogin (options) { }), new RecoveryCodeEntryView({ _ref: me => recovery_entry = me, - async ['property.value'] (value, { component }) { + async 'property.value' (value, { component }) { let error_i18n_key = 'something_went_wrong'; if ( ! value ) return; try { diff --git a/src/gui/src/UI/UIWindowSignup.js b/src/gui/src/UI/UIWindowSignup.js index 61be05e5e..a24eef24b 100644 --- a/src/gui/src/UI/UIWindowSignup.js +++ b/src/gui/src/UI/UIWindowSignup.js @@ -130,6 +130,9 @@ function UIWindowSignup (options) { dominant: true, center: true, onAppend: function (el_window) { + if ( options.authError ) { + $(el_window).find('.signup-error-msg').html(options.authError).fadeIn(); + } $(el_window).find('.username').get(0).focus({ preventScroll: true }); // Initialize Turnstile widget with callback to capture token @@ -404,8 +407,9 @@ function UIWindowSignup (options) { let isPasswordVisible = inputField.attr('type') === 'text'; inputField.attr('type', isPasswordVisible ? 'password' : 'text'); $(this).find('.toggle-show-password-icon').attr( - 'src', - isPasswordVisible ? window.icons['eye-open.svg'] : window.icons['eye-closed.svg']); + 'src', + isPasswordVisible ? window.icons['eye-open.svg'] : window.icons['eye-closed.svg'], + ); }); //remove login window diff --git a/src/gui/src/initgui.js b/src/gui/src/initgui.js index b634e37a4..e314f9b20 100644 --- a/src/gui/src/initgui.js +++ b/src/gui/src/initgui.js @@ -493,6 +493,14 @@ window.initgui = async function (options) { } } + //-------------------------------------------------------------------------------------- + // Desktop background (early) + // Set before action=login/signup so OIDC error redirects show the background behind the form. + // ------------------------------------------------------------------------------------- + if ( !window.is_fullpage_mode && !window.embedded_in_popup ) { + window.refresh_desktop_background(); + } + //-------------------------------------------------------------------------------------- // Action: Request Permission //-------------------------------------------------------------------------------------- @@ -535,13 +543,23 @@ window.initgui = async function (options) { // Action: Login //-------------------------------------------------------------------------------------- else if ( action === 'login' ) { - await UIWindowLogin(); + const authError = window.url_query_params.get('message') || null; + const opts = window.url_query_params.has('auth_error') ? { authError } : {}; + if ( ! window.is_auth() ) { + opts.window_options = { has_head: false }; + } + await UIWindowLogin(Object.keys(opts).length ? opts : undefined); } //-------------------------------------------------------------------------------------- // Action: Signup //-------------------------------------------------------------------------------------- else if ( action === 'signup' ) { - await UIWindowSignup(); + const authError = window.url_query_params.get('message') || null; + const opts = window.url_query_params.has('auth_error') ? { authError } : {}; + if ( ! window.is_auth() ) { + opts.window_options = { has_head: false }; + } + await UIWindowSignup(Object.keys(opts).length ? opts : undefined); } // ------------------------------------------------------------------------------------- // If in embedded in a popup, it is important to check whether the opener app has a relationship with the user @@ -867,12 +885,14 @@ window.initgui = async function (options) { } // upload try { - const res = await puter.fs.write(target_path, - file_to_upload, - { - dedupeName: false, - overwrite: overwrite, - }); + const res = await puter.fs.write( + target_path, + file_to_upload, + { + dedupeName: false, + overwrite: overwrite, + }, + ); let file_signature = await puter.fs.sign(app_uid, { uid: res.uid, action: 'write' }); file_signature = file_signature.items; @@ -1454,12 +1474,14 @@ window.initgui = async function (options) { } // upload try { - const res = await puter.fs.write(target_path, - file_to_upload, - { - dedupeName: false, - overwrite: overwrite, - }); + const res = await puter.fs.write( + target_path, + file_to_upload, + { + dedupeName: false, + overwrite: overwrite, + }, + ); let file_signature = await puter.fs.sign(app_uid, { uid: res.uid, action: 'write' }); file_signature = file_signature.items;