Callback hell (?) #2401
Replies: 9 comments
-
Please add a complete reproduction, otherwise, we cannot help you! If I have to guess, you are calling your |
Beta Was this translation helpful? Give feedback.
-
I'm not sure what exactly to share, because I haven't done any huge customization. Basically, when sign in fails, it goes back to the site it was requested in with And no, I'm not calling signIn early... just when a button is pressed. this is probably the weird part in my next auth options: pages: {
signIn: '/',
signOut: '/',
}, |
Beta Was this translation helpful? Give feedback.
-
Would you please share the full |
Beta Was this translation helpful? Give feedback.
-
next auth config/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/ban-ts-comment */
import { NextApiRequest, NextApiResponse } from 'next';
import NextAuth, { NextAuthOptions, User } from 'next-auth';
import Providers, { DefaultProviders, Provider } from 'next-auth/providers';
import { NextApiFunc } from '~/types/index';
import { buildBackendUrl } from '~/utils/backend-url';
import { get, post } from '~/utils/http';
import { checkIfProApp } from '~/utils/isProApp';
import { notNullOrUndefined } from '~/utils/notNull';
import { ResponseType } from '~/utils/responses-handler';
interface LinkedInAuthResponse extends ResponseType {
elements?: [
{
'handle~'?: {
emailAddress?: string;
};
},
];
profilePicture?: {
'displayImage~'?: {
elements?: [
{
identifiers?: [
{
identifier?: string;
},
];
},
];
};
};
}
interface SynaptikoSignInResponse extends ResponseType {
id?: string;
token?: string;
}
interface SynaptikoUserResponse extends ResponseType {
id?: string;
email?: string;
name?: string;
fullName?: string;
image?: string;
}
type AuthProvider =
| Provider
| ReturnType<DefaultProviders[keyof DefaultProviders]>;
const getLinkedInEmail = async (accessToken: string) => {
try {
const projectionUrl = `https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))&oauth2_access_token=${accessToken}`;
return get<LinkedInAuthResponse>(projectionUrl)
.then((response) => {
return response?.elements?.[0]?.['handle~']?.emailAddress;
})
.catch(() => {
return null;
});
} catch (e) {
return null;
}
};
const getLinkedInPhoto = async (accessToken: string) => {
try {
const projectionUrl = `https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))&oauth2_access_token=${accessToken}`;
return get<LinkedInAuthResponse>(projectionUrl)
.then((response) => {
return (
response?.profilePicture?.['displayImage~']?.elements?.[0]
?.identifiers?.[0]?.identifier ?? ''
);
})
.catch(() => {
return null;
});
} catch (e) {
return null;
}
};
const linkedinProvider: AuthProvider = Providers.LinkedIn({
clientId: process.env.LINKEDIN_CLIENT_ID || '',
clientSecret: process.env.LINKEDIN_CLIENT_SECRET || '',
scope: 'r_liteprofile r_emailaddress',
profileUrl:
'https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))',
// @ts-ignore
profile: (profileData: {
profilePicture?: unknown;
id?: string;
localizedFirstName?: string;
localizedLastName?: string;
}) => {
const profileImage =
// @ts-ignore
profileData?.profilePicture?.['displayImage~']?.elements[0]
?.identifiers?.[0]?.identifier ?? '';
return {
id: profileData.id,
name: `${profileData.localizedFirstName} ${profileData.localizedLastName}`,
email: null,
image: profileImage,
};
},
});
const discordProvider: AuthProvider = Providers.Discord({
clientId: process.env.DISCORD_CLIENT_ID || '',
clientSecret: process.env.DISCORD_CLIENT_SECRET || '',
});
const credentialsProvider: AuthProvider = Providers.Credentials({
name: 'Synaptikö',
credentials: {
email: {
label: 'Correo electrónico',
type: 'email',
placeholder: '[email protected]',
},
password: { label: 'Contraseña', type: 'password' },
},
async authorize(credentials): Promise<User | null> {
try {
const isPro = checkIfProApp();
const rolePath = isPro ? 'professionals' : 'users';
const path = buildBackendUrl('/auth/login');
// @ts-ignore
const user = post<SynaptikoSignInResponse>(
path,
{
email: credentials.email,
password: credentials.password,
},
{ headers: { accept: '*/*' } },
)
.then(async (user) => {
// @ts-ignore
const { id, token } = user;
if (!id || !token) return null;
return get<SynaptikoUserResponse>(
buildBackendUrl(`/${rolePath}/${id}`),
)
.then((userData) => {
const validUser: User = {
name: userData?.fullName ?? userData?.name ?? '',
email: userData?.email || '',
image: `https://unavatar.now.sh/${userData?.email || ''}`,
};
if (!validUser?.email?.length) return null;
return validUser;
})
.catch((e) => {
return null;
});
})
.catch((e) => {
const errorMessage = e?.message ?? '';
// eslint-disable-next-line
throw `/?error=${encodeURI(errorMessage)}&email=${encodeURI(
credentials.email || '',
)}`;
});
return user;
} catch (e) {
return null;
}
},
});
const providers: Array<AuthProvider> = [
checkIfProApp() ? linkedinProvider : undefined,
checkIfProApp() ? undefined : discordProvider,
credentialsProvider,
].filter(notNullOrUndefined);
const options: NextAuthOptions = {
providers,
database: checkIfProApp()
? process.env.DATABASE_URL
: process.env.PRO_DATABASE_URL,
secret: process.env.AUTH_SECRET,
session: {
jwt: true,
// Seconds - How long until an idle session expires and is no longer valid.
maxAge: 30 * 24 * 60 * 60, // 30 days
updateAge: 24 * 60 * 60, // 24 hours
},
jwt: {
encryption: true,
secret: process.env.JWT_SECRET,
},
debug: (process.env.NODE_ENV || '').includes('dev'),
pages: {
signIn: '/',
signOut: '/',
},
callbacks: {
// @ts-ignore
async jwt(token, user, account, profile, isNewUser) {
const newToken = { ...token };
// Add access_token to the token right after signin
if (account?.accessToken) {
newToken.accessToken = account.accessToken;
}
newToken.provider = token?.provider ?? account?.provider ?? '';
if (newToken.provider === 'linkedin') {
// newToken.isPro = true;
if (!newToken.email) {
// @ts-ignore
newToken.email = await getLinkedInEmail(newToken.accessToken);
}
if (!newToken.image) {
// @ts-ignore
newToken.image = await getLinkedInPhoto(newToken.accessToken);
}
} else {
// Comment if you need to test as a user
newToken.isPro = (process.env.NODE_ENV || '').includes('dev');
}
if (!newToken.image) {
newToken.image = newToken.picture || null;
}
return { ...newToken };
},
// @ts-ignore
async session(session, token) {
// @ts-ignore
const { accessToken, iat, exp, ...rest } = token;
session.user = { ...rest };
return session;
},
},
};
export default (req: NextApiRequest, res: NextApiResponse): NextApiFunc =>
NextAuth(req, res, options); login pageimport {
Flex,
FormControl,
FormLabel,
IconButton,
Input,
InputGroup,
InputRightElement,
Stack,
useColorModeValue as mode,
useToast,
} from '@chakra-ui/react';
import { mdiEyeOffOutline, mdiEyeOutline } from '@mdi/js';
import { signIn } from 'next-auth/client';
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';
import { Button } from '~/elements/button';
import { Component, ComponentProps } from '~/elements/fc';
import { Icon } from '~/elements/icon';
import { Link } from '~/elements/link';
import { post } from '~/utils/http';
interface AuthFormProps extends ComponentProps {
isForSignIn?: boolean;
}
export const AuthForm: Component<AuthFormProps> = (props) => {
const { isForSignIn } = props;
const linkColor = mode('blue.600', 'blue.200');
const toast = useToast();
const router = useRouter();
const [name, setName] = useState('');
const [email, setEmail] = useState(router?.query?.email || '');
const [password, setPassword] = useState('');
const [passwordConfirmation, setPasswordConfirmation] = useState('');
const [isPasswordVisible, setPasswordVisible] = useState(false);
const [isPasswordConfVisible, setPasswordConfVisible] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (isForSignIn) {
signIn('credentials', {
email,
password,
callbackUrl: `${window.location.origin}/`,
});
} else {
// register users
if (
!name.length ||
!email.length ||
!password.length ||
!passwordConfirmation.length
) {
toast({
title: 'Advertencia',
description: 'Hay campos vacios',
status: 'warning',
duration: 5000,
isClosable: true,
});
} else if (password !== passwordConfirmation) {
toast({
title: 'Error',
description: 'Las contraseñas no coinciden',
status: 'error',
duration: 5000,
isClosable: true,
});
} else {
await post('/api/internal/auth/register', { name, email, password })
.then((data) => {
const { success, error } = data;
if (success) {
setPassword('');
setPasswordConfirmation('');
toast({
title: 'Registro completo',
description: 'Ahora puedes iniciar sesión!',
status: 'success',
duration: 5000,
isClosable: true,
});
} else {
toast({
title: 'Error',
description: error ?? 'Error inesperado',
status: 'error',
duration: 5000,
isClosable: true,
});
}
})
.catch((error) => {
toast({
title: `Error: ${error.name}`.trim(),
description: error?.message ?? 'Error inesperado',
status: 'error',
duration: 5000,
isClosable: true,
});
});
}
}
};
useEffect(() => {
const error = router?.query?.error ?? '';
if (error && error.length) {
toast({
title: 'Error',
description: error,
status: 'error',
duration: 5000,
isClosable: true,
});
}
}, [router, toast]);
return (
<form
onSubmit={(e) => {
e.preventDefault();
// your submit logic here
}}
>
<Stack spacing={6}>
{!isForSignIn && (
<FormControl id={'name'}>
<FormLabel mb={1}>Nombre completo</FormLabel>
<Input
autoComplete={'name'}
value={name}
onChange={(e) => {
setName(e.target.value);
}}
/>
</FormControl>
)}
<FormControl id={'email'}>
<FormLabel mb={1}>Correo electrónico</FormLabel>
<Input
type={'email'}
autoComplete={'email'}
value={email}
onChange={(e) => {
setEmail(e.target.value);
}}
/>
</FormControl>
<FormControl id={'password'}>
<Flex align={'baseline'} justify={'space-between'}>
<FormLabel mb={1}>Contraseña</FormLabel>
{isForSignIn && (
<Link href={'#'} fontSize={'sm'} color={linkColor}>
Olvidaste la contraseña?
</Link>
)}
</Flex>
<InputGroup>
<Input
type={isPasswordVisible ? 'text' : 'password'}
autoComplete={isForSignIn ? 'current-password' : 'off'}
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
<InputRightElement mr={2}>
<IconButton
variant={'ghost'}
h={'2rem'}
size={'sm'}
onClick={() => {
setPasswordVisible(!isPasswordVisible);
}}
aria-label={
isPasswordVisible ? 'Hide password' : 'Show password'
}
icon={
<Icon
path={isPasswordVisible ? mdiEyeOffOutline : mdiEyeOutline}
/>
}
/>
</InputRightElement>
</InputGroup>
</FormControl>
{!isForSignIn && (
<FormControl id={'password-confirmation'}>
<FormLabel mb={1}>Confirma tu contraseña</FormLabel>
<InputGroup>
<Input
type={isPasswordConfVisible ? 'text' : 'password'}
autoComplete={isForSignIn ? 'current-password' : 'off'}
value={passwordConfirmation}
onChange={(e) => {
setPasswordConfirmation(e.target.value);
}}
/>
<InputRightElement mx={2}>
<IconButton
variant={'ghost'}
h={'2rem'}
size={'sm'}
onClick={() => {
setPasswordConfVisible(!isPasswordConfVisible);
}}
aria-label={
isPasswordConfVisible
? 'Hide password confirmation'
: 'Show password confirmation'
}
icon={
<Icon
path={
isPasswordConfVisible ? mdiEyeOffOutline : mdiEyeOutline
}
/>
}
/>
</InputRightElement>
</InputGroup>
</FormControl>
)}
<Button
type={'button'}
colorScheme={'brand'}
size={'lg'}
fontSize={'md'}
onClick={handleSubmit}
>
{isForSignIn ? 'Iniciar sesión' : 'Crear mi cuenta'}
</Button>
</Stack>
</form>
);
}; |
Beta Was this translation helpful? Give feedback.
-
Not a solution (I'm experiencing the same issue with query params) but a viable walk around.
It's documented here await signIn() here is somewhat equivalent to this
hope it helps |
Beta Was this translation helpful? Give feedback.
-
@jahirfiquitiva were you able to find a solution for this? |
Beta Was this translation helpful? Give feedback.
-
@balazsorban44 no, I haven't 😕 |
Beta Was this translation helpful? Give feedback.
-
So it's a bit hard to debug without a runnable reproduction, but your issue might be here: const handleSubmit = async (e) => {
e.preventDefault();
if (isForSignIn) {
signIn('credentials', {
email,
password,
callbackUrl: `${window.location.origin}/`, // <--
});
} You don't really check if there is already a I think there should be an error parameter, and so you should handle those cases appropriately since you defined a custom sign-in page. See the docs for possible error codes: https://next-auth.js.org/configuration/pages#sign-in-page And here is the default sign-in page and error messages for reference: next-auth/src/server/pages/signin.js Lines 23 to 36 in bcb9383 (See the whole file for inspiration) In any case, I am certain it is not a bug in |
Beta Was this translation helpful? Give feedback.
-
Hi next-auth team, http://localhost:5000/signin?callbackUrl=http://localhost:5000/signin&error=Callback#_=_ thank you in advance |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
When auth fails, it goes back to the site it was called from, and so we end up having something like
http://localhost:3000/?callbackUrl=http://localhost:3000/?callbackUrl=http://localhost:3000/
and it will keep adding the
callbackUrl
again and again...would you mind doing something to prevent this from happening?
thanks in advance
Beta Was this translation helpful? Give feedback.
All reactions