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:
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
Thanks
I would like to know if this approach to verifying users without passwords is secure enough.
Secure enough for what? This isn't a yes/no answer. Everything has tradeoffs.
In this case, it's only secure as the e-mail, and anyone who the user inadvertently sends it to. You have to trust all possible recipients of that message, the user, the user's browser and its extensions, etc. If you're fine with that, then it's secure enough. If not, well, then, not.
- How can I ensure the security of the authentication process in my passwordless approach when verifying users without passwords?
Testing, auditing, third-party review, etc.
Have you also considered checking out the common attacks for JWT on OWASP?
- Are there any potential vulnerabilities or security risks in the code implementation of my passwordless authentication system using Fastify, Prisma, and TypeScript?
The answer to this question is always yes... there are always potential vulnerabilities or security risks. It's up to you to understand them and decide what risks you will take for the convenience of your users.
As far as specific issues go... you haven't shown us code in main functions like verify()
so who knows. I am curious though... it appears that you're not just allowing token re-use but requiring it for your system to work?
- What are the best practices for handling and storing authentication tokens in a passwordless authentication system?
I'm not sure why you're storing the tokens to begin with. If you include the relevant information, like a session ID, and sign it, then the server can statelessly determine if the session ID was properly created. You can also encrypt the token to hide data within it that the user's browser tracks for you, but only the API server can decrypt. Either method doesn't require storing the token.