mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-04 08:30:39 +00:00
dev(oidc): redirect to login/signup on error (#2531)
Redirect to the login or signup page when there is an error signing in or creating an account using OIDC, instead of displaying the error on a new page. Alter the flow in cases where the suggested action is not the same as the initial action taken by the user (based on the error case).
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+36
-14
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user