# Password Reset in Sveltekit with Lucia
## Problem
If a user forgets their password, they need a way to change it.
But if they don't have their password, how do we know they are who they say they are?
We can use a one-time-token to authenticate the user once. If we send that token to their email, we can be fairly certain they are indeed the user associated with that email, and have permission to change their password.
This guide shows how to implement password-reset-tokens using Lucia and MongoDB.
It does not show any front-end code, just the server-side functionality needed
## Steps
### 1. Setup Email Client
We need an email client to send the reset link to the user. This example uses `emailjs`:
- You need to have your own SMTP client
- See some examples [here](https://postmarkapp.com/blog/the-best-smtp-email-services-comparison-sheet)
- `emailjs` seems to handle any SMTP setup. I use AWS, for example
```ts
import { SMTP_PASSWORD, SMTP_USERNAME } from '$lib/_env'
import { Message, SMTPClient, } from 'emailjs';
// Use the emailjs library to set up an SMTP client using your credentials
const client = new SMTPClient({
user: SMTP_USERNAME,
password: SMTP_PASSWORD,
host: `<Host>`,
ssl: true,
});
// General function to send an email to a single address
// from your chosen `from` email
async function sendEmail({ subject, text, to, attachment }) {
const msg = new Message({
text,
from: '<Your-Email>',
to,
subject,
// attachment lets us send html
// in which case, `text` will be use as a fallback
attachment: attachment ?? [],
})
const { isValid, validationError } = msg.checkValidity()
console.assert(isValid, validationError)
try {
await client.sendAsync(msg);
console.log('sent')
} catch (error) {
console.log(error)
}
}
```
### 2. Password Reset Token Storage
We also need to store the password reset tokens (using MongoDB, in this example):
`$lib/models/password-reset-tokens.ts`
```ts
import mongoose from "mongoose";
export const PasswordResetToken = mongoose.model(
"Password Reset Token",
new mongoose.Schema({
// One token per user
user_id: { type: String, required: true, unique: true },
token: { type: String, default: () => crypto.randomUUID() },
// In 1 day from now
expires: { type: Date, default: () => new Date(Date.getTime() + 24 * 60 * 60 * 1000) },
})
)
/**
* Given a user_id and token, check if that token exists for that user, and if it's still valid
*
* Will delete an expired token without creating a new one
*/
export const canResetPassword = async (params: { user_id: string, token: string }) => {
const resetToken = await PasswordResetToken.findOne(params).lean();
// Returns a RequestHandlerOutput to use in the password-reset endpoint
if (!resetToken) return {
status: 400,
body: { error: "Invalid token or user_id", ok: false }
};
if (resetToken.expires < new Date()) {
await resetToken.deleteOne();
return {
status: 400,
body: { error: "Token expired. Please reset your password again", ok: false }
};
}
return { body: { message: "Token valid", ok: true, resetToken } };
}
````
### 3. Send Password Reset Link
Now, we can set up the route to send a password-reset email to the user:
`/routes/forgot-password.ts`
```ts
import type { RequestHandler } from "@sveltejs/kit";
import { PasswordResetToken } from "$lib/models/password-reset-tokens";
import { Users } from "$lib/models/users";
import { dev } from "$app/env";
// Send a password-reset link to a user via email
// Either send them their existing token (if it hasn't expired)
// Or generate a new one
export const POST: RequestHandler = async ({ request }) => {
try {
const { email } = await request.json() as Record<string, string>
// This example uses MongoDB
const user = await User.findOne({ email }).lean()
if (!user) return { status: 400, body: { error: 'No user found with this email' } }
const user_id = user.id
// Check if a resetToken already exists for this user
let token = await PasswordResetToken.findOne({ user_id }).lean()
if (!token) {
token = await new PasswordResetToken({ user_id }).save()
} else if (token.expires < new Date()) {
// Each token has an expiry date.
// If the token is expired, delete it and make a new one
await token.deleteOne()
token = await new PasswordResetToken({ user_id }).save()
}
// The page the user will be directed to from their email
// We send the resetToken, and the user_id along
const link = `${dev ? 'localhost:3000/' : '<Your-Site>.com/'}password-reset/${user_id}/${token.token}`
await sendEmail({
subject: 'Password Reset',
text: `Reset your password by clicking this link: ${link}`,
to: email,
attachment: [{
data: `<div>Click the link below to reset your password: <br/><br/><a href="${link}">Reset password</a></div>`,
alternative: true
}]
})
return { body: { message: 'Password reset email sent' } }
} catch (error) {
console.error(error)
return { status: 500, body: { error: 'Internal server error' } }
}
}
```
### 4. Reset User's Password
The user now has a link to reset their password using a one-time-token stored in our DB.
When they click that link, they should be shown a form to enter a new password.
- The GET endpoint validates the token for the given user so that you can show a different page if the token is invalid
- The POST endpoint also validates the token for the user so that their password can actually be reset
`/password-reset/[user_id]/[token].ts`
```ts
import { auth } from "$lib/_lucia";
import type { RequestHandler } from "@sveltejs/kit";
import { canResetPassword } from "$lib/models/password-reset-tokens";
// Checks that the token is valid for the given user
// Allows the page to be loaded
export const GET: RequestHandler = async ({ params }) => {
const { user_id, token } = params
// Using the function defined earlier in step 2
const valid = await canResetPassword({ user_id, token })
return { body: { ok: valid.body.ok } }
}
// Resets password
export const POST: RequestHandler = async ({ params, request }) => {
try {
const { user_id, token } = params;
const { newPassword } = await request.json()
const valid = await canResetPassword({ user_id, token })
if (!valid.body.ok) return valid;
await auth.resetUserPassword(user_id, newPassword)
// Delete the token
await valid.body.resetToken?.deleteOne()
return { body: { ok: true } }
} catch (error) {
console.log(error)
return { status: 500, body: { error: "Internal server error" } }
}
}
```