Custom user identities in providers #4865
Replies: 6 comments 2 replies
-
For the record, I am tracing the steps while undertanding the code, perhaps at the benefit of anyone with the same requirement. The email value sent via the form's sign in page is obtained here : If the user can sign in, this goes here next-auth/packages/next-auth/src/core/lib/email/signin.ts Lines 9 to 12 in ae834f1 At this point, we lose the body that was submitted. An option should specify extra fields to read before that call. It should also be noted that this function receives 2 arguments, Also, regarding this note next-auth/packages/next-auth/src/core/routes/signin.ts Lines 33 to 39 in ae834f1 The providers: [
EmailProvider({
name: 'Email',
...
getIdentity: ({ /*query,*/ body }) => ({ email: body?.email })
})
] so this code next-auth/packages/next-auth/src/core/routes/signin.ts Lines 40 to 58 in ae834f1 would become const identity = provider?.getIdentity?.({ query, body }) ?? { email: body?.email?.toLowerCase() }
if (!identity?.email) return { redirect: `${url}/error?error=EmailSignin` }
// Verified in `assertConfig`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { getUserByEmail } = adapter!
// If is an existing user return a user object (otherwise use placeholder)
const user: User = (getUserByEmail ? await getUserByEmail(identity.email) : null) ?? {
email: identity.email,
id: identity.email,
}
const account: Account = {
providerAccountId: identity.email,
userId: identity.email,
type: "email",
provider: provider.id,
}
...
try {
await emailSignin(identity, options)
} catch (error) {
logger.error("SIGNIN_EMAIL_ERROR", error as Error)
return { redirect: `${url}/error?error=EmailSignin` }
} The email sign in function and other code using |
Beta Was this translation helpful? Give feedback.
-
Note that there is an error in the code. The line Should be
Because the so the condition is always true, even if |
Beta Was this translation helpful? Give feedback.
-
Or, instead of callbacks: {
...
identity({ params: { query, body }, account }) {
if (account.provider === 'email') {
return { email: body?.email?.toLowerCase() };
} else if (account.provider === 'credentials') {
return { username: body?.username, password: body?.password };
} else {
return null;
}
}
...
} |
Beta Was this translation helpful? Give feedback.
-
This is how I currently go around this limitation, but it's not ideal and I would rather have the possibility to receive these values directly as arguments without wrapping anything. /** @return { import("next-auth/adapters").Adapter } */
export default function GraphQlAdapter({ req }) {
const methods = {
...
async updateUser({ id:userId, emailVerified:accessedAt }) {
// req.body should have been "augmented" by the useVerificationToken method
// as getContext returns the required HTTP headers to send to the server, and
// need "x-api-fingerprint" containing the client's fingerprint for permissions
const ctx = getContext(req, req.body);
const userData = { userId, accessedAt, provider:'email' };
const [ access, error ] = await authApi.createAuthenticationAccess(userData, ctx);
return !error && access?.user ? {
...userToAccountUser(access.user, {
accessToken: access.accessToken,
fingerprint: req.body?.fingerprint ?? ''
}),
emailVerified: access.accessedAt
} : null;
},
async createVerificationToken({ identifier, expires:expiredAt, token }) {
// NOTE : req.body contains the field "fingerprint" sent from the custom FORM, required to properly generate a JWT
const context = getContext(req, req.body);
const fingerprint = req.body?.fingerprint ?? ''; // if the value is empty, the final accessToken will be invalid
const accessTokenData = { token, identifier, fingerprint, expiredAt };
const [ authToken, error ] = await authApi.createAuthenticationToken(accessTokenData, context);
return !error ? authToken : null;
},
async useVerificationToken({ identifier, token }) {
const [ authToken, error ] = await authApi.consumeAuthenticationToken({ token, identifier }, getContext(req));
// HACK : if a fingerprint was not provided, use the one we got from the createVerificationToken request
// this is not pretty as it means modifying the request object, a local variable could be used, but
// but this seems more consistent regardless.
if (!req.body?.fingerprint) {
if (!req.body) {
req.body = {};
}
req.body.fingerprint = authToken.fingerprint;
}
return !error && authToken?.verifiedAt ? {
token: authToken.token,
identifier: authToken.identifier,
expires: authToken.expiredAt
} : null;
},
};
return methods;
}; With the above adapter, these callbacks work as intended : export default async function auth(req, res) {
return await NextAuth(req, res, getAuthOptions(req));
};
export const getAuthOptions = (req /*, res*/) => ({
..
adapter: new GraphQlAdapter({ req }),
...
callbacks: {
....
// Getting the JWT token from API response,
// The user argument is initially received by the updateUser adapter method.
async jwt({ token, user /*, account, profile, isNewUser*/ }) {
if (user) {
// if user is provided, we just logged in, save extra user information in the token
token.uname = user.username;
token.atok = user.accessToken;
token.fp = user.fingerprint;
}
return token
},
// Since the jwt callback adds the fingerprint (fp) and accessToken (atok) parameters
// at sign-in, these values are now available in the session. When performing a query,
// the client can validate the request's fingerprint with the one originally received, adding
// an extra level of validation.
async session({ session, token }) {
// restore extra information from the token to the session
session.accessToken = token.atok;
session.user.username = token.uname;
session.fingerprint = token.fp;
// make sure that the accessToken is valid. This method is using GraphQL's cache to prevent
// unnecessary network calls
const context = getContext(req, session);
const [ access ] = await authApi.checkAuthenticationAccessToken(session.accessToken, context);
// we assume that access.user.id === session.user.id
return access?.user?.active ? session : null;
},
...
},
...
}); I do not like this solution, but this is a dirty workaround that works. |
Beta Was this translation helpful? Give feedback.
-
Was there any updates since this on a better solution for capturing additional data and storing it in the verification token (when using email provider). Hitting similar issues and wonder if this was worked on since? |
Beta Was this translation helpful? Give feedback.
-
Any updates? |
Beta Was this translation helpful? Give feedback.
-
Description 📓
I need to pass some extra values when creating a verification token. Currently, the method signature from the adapter looks like this :
I need to add a browser fingerprint, calculated on the client, specified in the request through a hidden
INPUT
field. How do I access that hidden field from the adatper method?How to reproduce ☕️
Nothing to reproduce.
Contributing 🙌🏽
Yes, I am willing to help implement this feature in a PR
Beta Was this translation helpful? Give feedback.
All reactions