Authentication
Complete guide for authentication in Laju framework.
Overview
Laju provides a complete authentication system:
- Password hashing with PBKDF2 (100,000 iterations)
- Session-based auth with secure cookies
- Google OAuth integration
- Password reset via email
- Email verification
Authenticate Service
Configuration
typescript
// app/services/Authenticate.ts
const ITERATIONS = 100000; // 100,000 iterations (OWASP recommended)
const KEYLEN = 64; // 64-byte key
const DIGEST = 'sha512'; // SHA-512 hashing
const SALT_SIZE = 16; // 16-byte random saltMethods
typescript
import Authenticate from "app/services/Authenticate";
// Hash password
const hashedPassword = await Authenticate.hash("mypassword123");
// Returns: "salt:hash" format
// Verify password
const isValid = await Authenticate.compare("mypassword123", hashedPassword);
// Returns: true or false
// Create session (login)
await Authenticate.process(user, request, response);
// - Generates UUID session token
// - Stores in sessions table
// - Sets auth_id cookie (60-day expiration)
// - Redirects to /home
// Destroy session (logout)
await Authenticate.logout(request, response);
// - Deletes session from database
// - Clears auth_id cookie
// - Redirects to /loginRegistration Example
typescript
// app/controllers/RegisterController.ts
public async processRegister(request: Request, response: Response) {
const { name, email, password } = await request.json();
// Hash password
const hashedPassword = await Authenticate.hash(password);
// Create user
const user = {
id: randomUUID(),
name,
email: email.toLowerCase(),
password: hashedPassword,
created_at: Date.now(),
updated_at: Date.now()
};
try {
await DB.insertInto("users").values(user).execute();
return Authenticate.process(user, request, response);
} catch (error) {
// Handle duplicate email
return response
.cookie("error", "Email already registered", 3000)
.redirect("/register");
}
}Login Example
typescript
// app/controllers/LoginController.ts
public async processLogin(request: Request, response: Response) {
const { email, password } = await request.json();
// Find user
const user = await DB.selectFrom("users")
.selectAll()
.where("email", "=", email.toLowerCase())
.executeTakeFirst();
if (!user) {
return response
.cookie("error", "Email not registered", 3000)
.redirect("/login");
}
// Verify password
const valid = await Authenticate.compare(password, user.password);
if (!valid) {
return response
.cookie("error", "Invalid password", 3000)
.redirect("/login");
}
// Create session
return Authenticate.process(user, request, response);
}Auth Middleware
How It Works
typescript
// app/middlewares/auth.ts
import SQLite from "../services/SQLite";
import { Request, Response } from "../../type";
export default async (request: Request, response: Response) => {
if (request.cookies.auth_id) {
// Validate session and load user
const user = SQLite.get(`
SELECT u.id, u.name, u.email, u.phone, u.is_admin, u.is_verified
FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.id = ?
`, [request.cookies.auth_id]);
if (user) {
// Convert SQLite 0/1 to boolean
user.is_admin = !!user.is_admin;
user.is_verified = !!user.is_verified;
request.user = user;
// No return = continue to handler
} else {
return response.cookie("auth_id", "", 0).redirect("/login");
}
} else {
return response.redirect("/login");
}
}Usage in Routes
typescript
// routes/web.ts
import Auth from "../app/middlewares/auth";
// Public routes
Route.get("/", HomeController.index);
Route.get("/login", LoginController.loginPage);
// Protected routes
Route.get("/home", [Auth], ProfileController.homePage);
Route.get("/profile", [Auth], ProfileController.profilePage);
Route.post("/posts", [Auth], PostController.store);Access User in Controller
typescript
public async store(request: Request, response: Response) {
// Access authenticated user
const userId = request.user.id;
const userName = request.user.name;
const isAdmin = request.user.is_admin;
await DB.insertInto("posts").values({
title: "New Post",
user_id: userId,
created_at: Date.now()
}).execute();
return response.redirect("/posts");
}Access User in Svelte
User is automatically passed to all Inertia pages:
svelte
<script>
let { user } = $props();
</script>
{#if user?.id}
<p>Welcome, {user.name}!</p>
{#if user.is_admin}
<a href="/admin">Admin Panel</a>
{/if}
{:else}
<a href="/login">Login</a>
{/if}Session Management
Sessions Table
typescript
// migrations/20230514062913_sessions.ts
import { Kysely } from "kysely";
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('sessions')
.addColumn('id', 'text', (col) => col.primaryKey()) // UUID token
.addColumn('user_id', 'text', (col) => col.references('users.id'))
.addColumn('user_agent', 'text')
.addColumn('expires_at', 'text')
.execute();
}Cookie Settings
- Name:
auth_id - Expiration: 60 days
- HttpOnly: Yes (prevents XSS)
Logout
typescript
public async logout(request: Request, response: Response) {
if (request.cookies.auth_id) {
await Authenticate.logout(request, response);
}
}Google OAuth
Configuration
env
# .env
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
GOOGLE_REDIRECT_URI=http://localhost:5555/google/callbackSetup Google Console
- Go to Google Cloud Console
- Create OAuth 2.0 credentials
- Add redirect URI:
http://localhost:5555/google/callback - Copy Client ID and Secret to
.env
OAuth Flow
typescript
// app/controllers/OAuthController.ts
import { redirectParamsURL } from "../services/GoogleAuth";
class OAuthController {
// Step 1: Redirect to Google
public async redirect(request: Request, response: Response) {
const params = redirectParamsURL();
const googleLoginUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
return response.redirect(googleLoginUrl);
}
// Step 2: Handle callback
public async googleCallback(request: Request, response: Response) {
const { code } = request.query;
// Exchange code for tokens
const { data } = await axios.post('https://oauth2.googleapis.com/token', {
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: process.env.GOOGLE_REDIRECT_URI,
grant_type: "authorization_code",
code,
});
// Get user info
const result = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${data.access_token}` }
});
const { email, name, verified_email } = result.data;
// Find or create user
let user = await DB.selectFrom("users")
.selectAll()
.where("email", "=", email.toLowerCase())
.executeTakeFirst();
if (!user) {
user = {
id: randomUUID(),
email: email.toLowerCase(),
name,
password: await Authenticate.hash(email),
is_verified: verified_email,
created_at: Date.now(),
updated_at: Date.now()
};
await DB.insertInto("users").values(user).execute();
}
return Authenticate.process(user, request, response);
}
}Routes
typescript
Route.get("/google/redirect", OAuthController.redirect);
Route.get("/google/callback", OAuthController.googleCallback);Password Reset
Flow
- User requests password reset
- Generate token, store in DB with expiry
- Send reset link via email
- User clicks link, enters new password
- Verify token, update password, delete token
Controller
typescript
// app/controllers/PasswordController.ts
public async sendResetPassword(request: Request, response: Response) {
const { email } = await request.json();
const user = await DB.selectFrom("users")
.selectAll()
.where("email", "=", email)
.executeTakeFirst();
if (!user) {
return response.status(404).send("Email not found");
}
const token = randomUUID();
await DB.insertInto("password_reset_tokens").values({
email: user.email,
token: token,
expires_at: dayjs().add(24, 'hours').toDate()
}).execute();
await MailTo({
to: email,
subject: "Reset Password",
text: `Reset your password: ${process.env.APP_URL}/reset-password/${token}`
});
return response.send("OK");
}
public async resetPassword(request: Request, response: Response) {
const { id, password } = await request.json();
const token = await DB.selectFrom("password_reset_tokens")
.selectAll()
.where("token", "=", id)
.where("expires_at", ">", new Date())
.executeTakeFirst();
if (!token) {
return response.status(404).send("Invalid or expired link");
}
const user = await DB.selectFrom("users")
.selectAll()
.where("email", "=", token.email)
.executeTakeFirst();
await DB.updateTable("users")
.set({ password: await Authenticate.hash(password) })
.where("id", "=", user.id)
.execute();
await DB.deleteFrom("password_reset_tokens").where("token", "=", id).execute();
return Authenticate.process(user, request, response);
}Email Verification
Flow
- After registration, generate verification token
- Send verification link via email
- User clicks link
- Verify token, mark user as verified
Controller
typescript
// app/controllers/VerificationController.ts
public async verify(request: Request, response: Response) {
const token = randomUUID();
await DB.deleteFrom("email_verification_tokens")
.where("user_id", "=", request.user.id)
.execute();
await DB.insertInto("email_verification_tokens").values({
user_id: request.user.id,
token: token,
expires_at: dayjs().add(24, 'hours').toDate()
}).execute();
await MailTo({
to: request.user.email,
subject: "Verify Your Email",
text: `Verify: ${process.env.APP_URL}/verify/${token}`
});
return response.redirect("/home");
}
public async verifyPage(request: Request, response: Response) {
const { id } = request.params;
const verificationToken = await DB.selectFrom("email_verification_tokens")
.selectAll()
.where("user_id", "=", request.user.id)
.where("token", "=", id)
.where("expires_at", ">", new Date())
.executeTakeFirst();
if (verificationToken) {
await DB.updateTable("users")
.set({ is_verified: true })
.where("id", "=", request.user.id)
.execute();
await DB.deleteFrom("email_verification_tokens")
.where("id", "=", verificationToken.id)
.execute();
}
return response.redirect("/home?verified=true");
}Security Best Practices
- Always hash passwords - Never store plaintext
- Use HTTPS in production - Protect cookies in transit
- Rate limit auth endpoints - Prevent brute force
- Validate email format - Before sending emails
- Short token expiry - 24 hours for reset/verification
- Delete used tokens - Prevent reuse
