Skip to content

Instantly share code, notes, and snippets.

@andyjessop
Last active March 7, 2024 20:30
Show Gist options
  • Save andyjessop/6d92cf0576b2b3a02e1d896203e3048d to your computer and use it in GitHub Desktop.
Save andyjessop/6d92cf0576b2b3a02e1d896203e3048d to your computer and use it in GitHub Desktop.
Cloudflare Worker Auth
import { Hono } from 'hono';
import { jwt } from 'hono/jwt';
import { D1 } from '@cloudflare/workers-types';
import { hash, compare } from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
import { sendEmail } from './email';
import { isEmail, sanitize } from 'validator';
import { createHash } from 'crypto';
type Env = {
DB: D1;
JWT_SECRET: string;
REFRESH_TOKEN_SECRET: string;
EMAIL_FROM: string;
EMAIL_SERVICE: string;
EMAIL_USER: string;
EMAIL_PASSWORD: string;
};
const app = new Hono<{ Bindings: Env }>();
// JWT middleware
app.use('/api/*', jwt({ secret: app.env.JWT_SECRET }));
// Create user route
app.post('/api/register', async (c) => {
const { email, password } = await c.req.json();
// Validate and sanitize input
if (!isEmail(email)) {
return c.json({ error: 'Invalid email address' }, 400);
}
const sanitizedEmail = sanitize(email).trim();
const sanitizedPassword = sanitize(password).trim();
if (sanitizedPassword.length < 8) {
return c.json({ error: 'Password must be at least 8 characters long' }, 400);
}
// Check if user already exists
const existingUser = await c.env.DB.prepare(`
SELECT * FROM users WHERE email = ?
`).bind(sanitizedEmail).first();
if (existingUser) {
return c.json({ error: 'Email already exists' }, 400);
}
const salt = await bcrypt.genSalt(10);
const hashedPassword = await hash(sanitizedPassword, salt);
const activationToken = uuidv4();
await c.env.DB.prepare(`
INSERT INTO users (id, email, password, salt, activation_token)
VALUES (?, ?, ?, ?, ?)
`).bind(uuidv4(), sanitizedEmail, hashedPassword, salt, activationToken).run();
// Send activation email
const activationLink = `https://your-app.com/activate?token=${activationToken}`;
await sendEmail(c.env, {
to: sanitizedEmail,
subject: 'Activate Your Account',
text: `Please click the following link to activate your account: ${activationLink}`,
html: `<p>Please click the following link to activate your account: <a href="${activationLink}">${activationLink}</a></p>`,
});
return c.json({ message: 'User created successfully. Please check your email to activate your account.' });
});
// Activate account route
app.get('/api/activate', async (c) => {
const { token } = c.req.query();
const user = await c.env.DB.prepare(`
SELECT * FROM users WHERE activation_token = ?
`).bind(token).first();
if (!user) {
return c.json({ error: 'Invalid or expired activation token' }, 400);
}
await c.env.DB.prepare(`
UPDATE users SET activation_token = NULL, is_active = true WHERE id = ?
`).bind(user.id).run();
return c.json({ message: 'Account activated successfully. You can now log in.' });
});
// Login route
app.post('/api/login', async (c) => {
const { email, password } = await c.req.json();
// Validate and sanitize input
if (!isEmail(email)) {
return c.json({ error: 'Invalid email address' }, 400);
}
const sanitizedEmail = sanitize(email).trim();
const sanitizedPassword = sanitize(password).trim();
const user = await c.env.DB.prepare(`
SELECT * FROM users WHERE email = ?
`).bind(sanitizedEmail).first();
if (!user) {
return c.json({ error: 'Invalid email or password' }, 401);
}
if (!user.is_active) {
return c.json({ error: 'Account is not activated. Please check your email to activate your account.' }, 401);
}
const isPasswordValid = await compare(sanitizedPassword, user.password);
if (!isPasswordValid) {
return c.json({ error: 'Invalid email or password' }, 401);
}
const accessToken = await c.jwt.sign({ userId: user.id }, { expiresIn: '15m' });
const refreshToken = await c.jwt.sign({ userId: user.id }, { secret: c.env.REFRESH_TOKEN_SECRET, expiresIn: '7d' });
const hashedRefreshToken = createHash('sha256').update(refreshToken).digest('hex');
await c.env.DB.prepare(`
UPDATE users SET refresh_token = ?, refresh_token_expires_at = DATE_ADD(NOW(), INTERVAL 7 DAY) WHERE id = ?
`).bind(hashedRefreshToken, user.id).run();
return c.json({ accessToken, refreshToken });
});
// Refresh token route
app.post('/api/refresh-token', async (c) => {
const { refreshToken } = await c.req.json();
try {
const { userId } = await c.jwt.verify(refreshToken, { secret: c.env.REFRESH_TOKEN_SECRET });
const hashedRefreshToken = createHash('sha256').update(refreshToken).digest('hex');
const user = await c.env.DB.prepare(`
SELECT * FROM users WHERE id = ? AND refresh_token = ? AND refresh_token_expires_at > NOW()
`).bind(userId, hashedRefreshToken).first();
if (!user) {
return c.json({ error: 'Invalid or expired refresh token' }, 401);
}
const accessToken = await c.jwt.sign({ userId: user.id }, { expiresIn: '15m' });
return c.json({ accessToken });
} catch (error) {
return c.json({ error: 'Invalid refresh token' }, 401);
}
});
// Logout route
app.post('/api/logout', async (c) => {
const { userId } = c.req.user;
await c.env.DB.prepare(`
UPDATE users SET refresh_token = NULL, refresh_token_expires_at = NULL WHERE id = ?
`).bind(userId).run();
return c.json({ message: 'Logged out successfully' });
});
// Forgot password route
app.post('/api/forgot-password', async (c) => {
const { email } = await c.req.json();
// Validate and sanitize input
if (!isEmail(email)) {
return c.json({ error: 'Invalid email address' }, 400);
}
const sanitizedEmail = sanitize(email).trim();
const user = await c.env.DB.prepare(`
SELECT * FROM users WHERE email = ?
`).bind(sanitizedEmail).first();
if (!user) {
return c.json({ error: 'User not found' }, 404);
}
const resetToken = uuidv4();
await c.env.DB.prepare(`
UPDATE users SET reset_token = ?, reset_token_expires_at = DATE_ADD(NOW(), INTERVAL 1 HOUR) WHERE id = ?
`).bind(resetToken, user.id).run();
// Send password reset email
const resetLink = `https://your-app.com/reset-password?token=${resetToken}`;
await sendEmail(c.env, {
to: sanitizedEmail,
subject: 'Reset Your Password',
text: `Please click the following link to reset your password: ${resetLink}`,
html: `<p>Please click the following link to reset your password: <a href="${resetLink}">${resetLink}</a></p>`,
});
return c.json({ message: 'Password reset email sent' });
});
// Reset password route
app.post('/api/reset-password', async (c) => {
const { token, password } = await c.req.json();
// Validate and sanitize input
const sanitizedPassword = sanitize(password).trim();
if (sanitizedPassword.length < 8) {
return c.json({ error: 'Password must be at least 8 characters long' }, 400);
}
const user = await c.env.DB.prepare(`
SELECT * FROM users WHERE reset_token = ? AND reset_token_expires_at > NOW()
`).bind(token).first();
if (!user) {
return c.json({ error: 'Invalid or expired reset token' }, 400);
}
const salt = await bcrypt.genSalt(10);
const hashedPassword = await hash(sanitizedPassword, salt);
await c.env.DB.prepare(`
UPDATE users SET password = ?, salt = ?, reset_token = NULL, reset_token_expires_at = NULL WHERE id = ?
`).bind(hashedPassword, salt, user.id).run();
return c.json({ message: 'Password reset successfully' });
});
// Change password route
app.post('/api/change-password', async (c) => {
const { userId } = c.req.user;
const { currentPassword, newPassword } = await c.req.json();
// Validate and sanitize input
const sanitizedCurrentPassword = sanitize(currentPassword).trim();
const sanitizedNewPassword = sanitize(newPassword).trim();
if (sanitizedNewPassword.length < 8) {
return c.json({ error: 'New password must be at least 8 characters long' }, 400);
}
const user = await c.env.DB.prepare(`
SELECT * FROM users WHERE id = ?
`).bind(userId).first();
const isPasswordValid = await compare(sanitizedCurrentPassword, user.password);
if (!isPasswordValid) {
return c.json({ error: 'Invalid current password' }, 401);
}
const salt = await bcrypt.genSalt(10);
const hashedPassword = await hash(sanitizedNewPassword, salt);
await c.env.DB.prepare(`
UPDATE users SET password = ?, salt = ? WHERE id = ?
`).bind(hashedPassword, salt, userId).run();
return c.json({ message: 'Password changed successfully' });
});
// Protected route example
app.get('/api/protected', (c) => {
return c.json({ message: 'Access granted to protected route' });
});
export default app;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment