Is there an expiration of the email verification token?

I need to determine whether the email verification token expires. If it does, I plan to create a dedicated "Account Confirmation" page that indicates when the token has expired and displays a "Resend activation link" button. Additionally, I want to know the duration (in hours) for which the token is valid and how to capture an error if the token has expired.
Solution:
if the token is invalid, it just returns 401 instead
Jump to solution
26 Replies
craftzcode
craftzcodeOP•2w ago
hello sir @bekacru can you help me?
bekacru
bekacru•2w ago
yes. it does expire in 1 hour. you can configure it under emailVerification.expiresIn
craftzcode
craftzcodeOP•2w ago
how about the error, how can I display the error, where can I get the error? I want to display the error if the token is invalid or it's expired
bekacru
bekacru•2w ago
better auth will add error query param with token_expired
craftzcode
craftzcodeOP•2w ago
is this already added?
bekacru
bekacru•2w ago
yes
craftzcode
craftzcodeOP•2w ago
sir @bekacru this is a very confusing to me, there's a error query param on the forgot password to handle the invalid token, but I can access also the onError inside of authClient.resetPassword so the error query param is useless now?
bekacru
bekacru•2w ago
the token_expired is jsut for when the users tries to verify their email
craftzcode
craftzcodeOP•2w ago
hello sir @bekacru this is how I handle the email verification manually
const searchParams = useSearchParams()
const token = searchParams.get('token')
const callbackURL = searchParams.get('callbackURL')

const handleVerifyEmail = async () => {
await verifyEmail(
{
query: { token: token ?? '', callbackURL: callbackURL ?? '/' }
},
{
onRequest: () => {
setIsLoading(true)
},
onSuccess: () => {
toast.success('Email verified successfully')
},
onError: ctx => {
toast.error(ctx.error.message)
}
}
)
setIsLoading(false)
}
const searchParams = useSearchParams()
const token = searchParams.get('token')
const callbackURL = searchParams.get('callbackURL')

const handleVerifyEmail = async () => {
await verifyEmail(
{
query: { token: token ?? '', callbackURL: callbackURL ?? '/' }
},
{
onRequest: () => {
setIsLoading(true)
},
onSuccess: () => {
toast.success('Email verified successfully')
},
onError: ctx => {
toast.error(ctx.error.message)
}
}
)
setIsLoading(false)
}
but whenever the email verification is successfull it's not redirecting to the callbackURL? also I can't get the error all I can see is the ?error=token_expired and it is 200 status on my network tab and it's not adding that query params on the url.
Ping
Ping•2w ago
Is this being called in on the client or server (eg server actions)? Also, is the verifyEmail coming from auth.api?
craftzcode
craftzcodeOP•2w ago
I called it on the client side this is from authClient.verifyEmail, I don't understand the callbackURL is this will only trigger if there's something wrong on the validation? what I mean the only purpose of the callbackURL is to pass the query params on that route?
Ping
Ping•2w ago
If the user clicks the provided verification URL, their email is automatically verified, and they are redirected to the callbackURL. Could it be session token expired? 🤔 Could you try re-signing in, then verify email, and try again? Just to test
craftzcode
craftzcodeOP•2w ago
I tried to click the verification URL that has been already expired, and I see this on my network tab, as you can see on the picture I got a successfully message even the token has already expired, also on my network tab there's a request invetory?error=tokent_expired this is my another route but I'm not redirecting on that route? it's just pass only the query params
No description
craftzcode
craftzcodeOP•2w ago
that's why I thought the callbackURL or redirectTo is not really going to the route it's just only passing the query params on that callbackURL or redirectTo here's how I setup the sendVerificationEmail function in the auth server instance
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
expiresIn: 60,
sendVerificationEmail: async ({ user, token, url }) => {
const verificationURL = `${process.env.NEXT_PUBLIC_APP_URL}/verify-email?token=${token}&callbackURL=${process.env.BETTER_EMAIL_VERIFICATION_CALLBACK_URL}`
await sendEmail({
to: user.email,
subject: 'Verify your email',
template: VerifyEmail({
username: user.email,
url: verificationURL
})
})
}
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
expiresIn: 60,
sendVerificationEmail: async ({ user, token, url }) => {
const verificationURL = `${process.env.NEXT_PUBLIC_APP_URL}/verify-email?token=${token}&callbackURL=${process.env.BETTER_EMAIL_VERIFICATION_CALLBACK_URL}`
await sendEmail({
to: user.email,
subject: 'Verify your email',
template: VerifyEmail({
username: user.email,
url: verificationURL
})
})
}
},
@Ping @bekacru This is what I did to see where the problem is. First, I tried visiting the default URL of sendVerificationEmail in the auth.ts, which is this: http://localhost:3000/api/auth/verify-email?token=[TOKEN]&callbackURL=/verify-email. When I verify my email directly through this URL, the callbackURL works; it redirects me to that route whether or not the query parameters are present. But when I use the manual authClient.verifyEmail on the client side, the redirect for callbackURL does not work. It sends a request in the network tab, but it does not redirect.
Ping
Ping•2w ago
In your onSuccess, just redirect them using code.
craftzcode
craftzcodeOP•2w ago
how about the error? it's not passing the query params at all, or it will pass now if I use router.push on the onSuccess @Ping @bekacru You should just make your email verification flow similar to your reset password flow. In reset password, the error is caught in onError when you manually call authClient.resetPassword. Unlike the manual call to authClient.verifyEmail, the HTTP status is still success even if there’s an error in token validation. For me, I find it a bit challenging to handle the error regarding the query params in manual verify email; it also doesn’t redirect even if there’s a callbackURL, so the query params never get passed. That’s why I prefer handling the error in reset password — it doesn’t need a redirect or query params. The error is immediately caught in onError and it makes it easier to customize the page.
bekacru
bekacru•2w ago
But when I use the manual authClient.verifyEmail on the client side, the redirect for callbackURL does not work. It sends a request in the network tab, but it does not redirect.
you sohuld redirect them yourself since you're calling it directly you're in contorl people call verifyEmail from native mobile and desktop apps. we don't want to return 302 or "redirect" them since that wouldn't make sense
craftzcode
craftzcodeOP•2w ago
I see, I didn't think about that this is also for multiplatform 😅 @bekacru so it's okay that I will use router.push or router.refresh so the error query params will pass on the url? And why does resetPassword return an error in onError when verifyEmail has the same function to handle token validation? Why is it that for verifyEmail you don’t return the error in onError—if you are considering mobile apps and desktop apps. Also, does the flow for callbackURL or redirectTo only work when you directly call api/auth/reset-password or api/auth/verify-email? Because that’s what I noticed: when I use the URL from the auth instance itself, the callbackURL and redirectTo work; but when I create a custom URL to handle token validation on a custom page and then manually call authClient.resetPassword or authClient.verifyEmail, the callbackURL or redirectTo don’t work. @Ping @bekacru I can't get the error on authClient.verifyEmail even I use router.refresh() the url is not changing, is not passing the query params of error. I'm using a custom url on sendEmailVerification like this:
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
expiresIn: 60,
sendVerificationEmail: async ({ user, token }) => {
const verificationURL = `${process.env.NEXT_PUBLIC_APP_URL}/verify-email?token=${token}&callbackURL=${process.env.BETTER_EMAIL_VERIFICATION_CALLBACK_URL}`
await sendEmail({
to: user.email,
subject: 'Verify your email',
template: VerifyEmail({
username: user.email,
url: verificationURL
})
})
}
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
expiresIn: 60,
sendVerificationEmail: async ({ user, token }) => {
const verificationURL = `${process.env.NEXT_PUBLIC_APP_URL}/verify-email?token=${token}&callbackURL=${process.env.BETTER_EMAIL_VERIFICATION_CALLBACK_URL}`
await sendEmail({
to: user.email,
subject: 'Verify your email',
template: VerifyEmail({
username: user.email,
url: verificationURL
})
})
}
},
Instead the user click the email verification direct to http://localhost:3000/api/auth/verify-email?token, on this custom url the user will redirect to the custom page to verify manually using authClient.verifyEmail button. I think this is a bad DX 🤔 it's better to return an error instead of status 200 even if there's a query params for error
bekacru
bekacru•2w ago
if you call verifyEmail manually, it's not going to be added as a query param
Solution
bekacru
bekacru•2w ago
if the token is invalid, it just returns 401 instead
craftzcode
craftzcodeOP•2w ago
it's not returning as 401, it's returning as 200 that's why I can't handle the error
bekacru
bekacru•2w ago
when there is an error?
craftzcode
craftzcodeOP•2w ago
I tried to access the error from onError but I didn't get the error even the toke has expired
craftzcode
craftzcodeOP•2w ago
as you can see here I always got the onSuccess
No description
craftzcode
craftzcodeOP•2w ago
but in the network tab it said 302? or because of the callbackURL? oh I see 😂 it's coming from the callbackURL, so when calling the manual verifyEmail I don't need to pass the callbackURL anymore
craftzcode
craftzcodeOP•2w ago
I just confused by the documentation on Email the verifyEmail has callbackURL when triggering the manual verify email

Did you find this page helpful?