Problem:
I am in the process of creating a straightforward passwordless authentication system for users to perform basic CRUD operations.
Approach
The overall login process is as follows:
- The user visits the magic link page
- If the email is valid, a verification link is sent to the user's mailbox.
- The user verifies the link.
- If the link has not expired and is valid, it is stored as a cookie.
- With the information from the cookie, I can make Guest HOC for components only visible for guests like login and User HOC for components like dashboard.
I would like to know if this approach to verifying users without passwords is secure enough.
Stack i use
I use fastify with prisma,typescript and zod for schema validation
My code so far
export const app = fastify()
declare module 'fastify' {
interface FastifyRequest {
user: User
}
}
app.register(fastifyCookie, {
secret: process.env.COOKIE_SECRET,
hook: 'onRequest',
parseOptions: {},
})
app.setErrorHandler(function (error, _request, reply) {
if (error instanceof NotFoundError) {
reply.status(404).send(error.message)
}
if (error instanceof InvalidInputError) {
reply.status(422).send(error.issues)
}
if (error instanceof TokenExpiredError) {
reply.status(403).send({ error: 'Token expired' })
}
if (error instanceof JsonWebTokenError) {
reply.status(404).send({ error: 'Invalid token' })
// Token required for the request is invalid
}
if (process.env.NODE_ENV == 'development') {
console.log(error.message)
}
reply.status(500).send('Internal server error')
})
const authTokenSchema = z.object({ email: z.string().email() })
const auth = async (request: FastifyRequest, reply: FastifyReply) => {
try {
const { authToken } = request.cookies
if (!authToken) {
throw new NotFoundError('Token')
}
const decodedToken = verify(authToken, process.env.JWT_SECRET)
const { email } = authTokenSchema.parse(decodedToken)
const user = await prisma.user.findFirst({
where: {
email,
authToken,
},
})
if (!user) {
throw new NotFoundError('User')
}
request.user = user
} catch (error) {
reply.status(500).send({ error })
}
}
app.post('/api/login', async (req, reply) => {
const { email } = req.body as {
email: string
}
try {
const authToken = sign({ email }, process.env.JWT_SECRET, { expiresIn: '30m' })
const isEmailFound = await prisma.user.findFirst({ where: { email } })
if (isEmailFound) {
await prisma.user.update({
where: { email },
data: { authToken },
})
} else {
await prisma.user.create({
data: {
email,
authToken,
},
})
}
await emailService.sendMagicLink(email, authToken)
reply.status(200).send({
success: true,
email,
})
} catch (error) {
reply.status(500).send({ error })
}
})
app.get('/api/verify', async (req, reply) => {
const { authToken } = req.query as { authToken: string }
// TODO: parse schemÄ… tokena
const data = verify(authToken, process.env.JWT_SECRET) as { email: string }
console.log(authToken)
const { email } = authTokenSchema.parse(data)
reply
.setCookie('authToken', authToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // true on production
})
.status(200)
.send({
success: true,
email,
})
})
app.get('/auth', { preHandler: [auth] }, async (_req, reply) => {
try {
reply.status(200).send({ success: true })
} catch (error) {
reply.status(403).send({ error })
}
})
const port = parseInt(process.env.PORT)
app.listen({ port }, (error, address) => {
if (error) logger.error(error)
logger.info(`Server listening on on ${address}`)
})
If someone is kind enught a have some questions
- How can I ensure the security of the authentication process in my passwordless approach when verifying users without passwords?
- Are there any potential vulnerabilities or security risks in the code implementation of my passwordless authentication system using Fastify, Prisma, and TypeScript?
- What are the best practices for handling and storing authentication tokens in a passwordless authentication system?
- Are there any improvements or modifications I should consider to enhance the security of my passwordless authentication approach using cookies and magic links?
Thanks