Authentication
TrickBook uses JWT-based authentication with Google SSO and Apple Sign-In.
Authentication Methods
1. Email/Password
Traditional email and password authentication with bcrypt hashing.
// Registration - password hashing
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// Login - password verification
const validPassword = await bcrypt.compare(password, user.password);
2. Google SSO
OAuth2 authentication using Google Identity Services. Supports web, iOS, and Android clients.
const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
// Verify Google ID token (accepts multiple client IDs)
const ticket = await client.verifyIdToken({
idToken: googleIdToken,
audience: [
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_IOS_CLIENT_ID,
process.env.GOOGLE_ANDROID_CLIENT_ID,
]
});
const { email, name, picture } = ticket.getPayload();
- Auto-creates user if email doesn't exist
- Updates profile on subsequent logins
3. Apple Sign-In
Apple identity token verification using apple-signin-auth.
const appleSignin = require('apple-signin-auth');
// Verify Apple identity token
const appleUser = await appleSignin.verifyIdToken(identityToken);
const { email, sub: appleUserId } = appleUser;
// Link Apple ID to existing email or create new user
// Fallback name: "Apple User" (Apple only sends name on first auth)
- Links
appleUserIdto existing email accounts - Supports first-time and returning Apple auth flows
JWT Token Structure
Tokens are generated using the jsonwebtoken package:
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{
_id: user._id,
name: user.name,
email: user.email,
imageUri: user.imageUri,
role: user.role
},
'jwtPrivateKey' // Should be env variable!
);
The JWT secret is currently hardcoded as "jwtPrivateKey". This must be changed to use process.env.JWT_SECRET.
Token Payload
{
"_id": "user_object_id",
"name": "John Doe",
"email": "john@example.com",
"imageUri": "https://s3.amazonaws.com/...",
"role": "admin" // or null for regular users
}
Tokens currently have no expiration. Should add:
jwt.sign(payload, secret, { expiresIn: '1h' });
Middleware
Basic Auth (middleware/auth.js)
Validates JWT token on protected routes.
const auth = (req, res, next) => {
const token = req.header("x-auth-token");
if (!token) {
return res.status(401).send("Access denied. No token provided.");
}
try {
const decoded = jwt.verify(token, "jwtPrivateKey");
req.user = decoded;
next();
} catch (error) {
res.status(400).send("Invalid token.");
}
};
Usage:
router.get('/protected', auth, (req, res) => {
// req.user contains decoded token
res.json({ userId: req.user._id });
});
Admin Auth (middleware/authAdmin.js)
Extends basic auth to require admin role.
const authAdmin = async (req, res, next) => {
const token = req.header("x-auth-token");
if (!token) {
return res.status(401).send("Access denied.");
}
try {
const decoded = jwt.verify(token, "jwtPrivateKey");
// Check admin role in database
const user = await db.collection("users").findOne({
_id: new ObjectId(decoded._id)
});
if (user.role !== "admin") {
return res.status(403).send("Access denied. Admin only.");
}
req.user = decoded;
next();
} catch (error) {
res.status(400).send("Invalid token.");
}
};
Admin check queries the database on every request. Consider caching or including role in JWT payload with short expiration.
Account or Admin (middleware/authAccountOrAdmin.js)
Allows access if user is the account owner OR an admin.
const authAccountOrAdmin = async (req, res, next) => {
// Uses Authorization: Bearer token
const authHeader = req.header("Authorization");
const token = authHeader?.replace("Bearer ", "");
const decoded = jwt.verify(token, "jwtPrivateKey");
// Allow if admin
const user = await db.collection("users").findOne({...});
if (user.role === "admin") {
return next();
}
// Allow if account owner
if (decoded.email === req.body.email ||
decoded._id === req.params.id) {
return next();
}
return res.status(403).send("Access denied.");
};
Subscription Check (middleware/subscription.js)
Enforces freemium limits on certain endpoints.
const checkSubscription = async (req, res, next) => {
const user = await db.collection("users").findOne({
_id: new ObjectId(req.user._id)
});
// Premium users bypass all limits
if (user.subscription?.plan === "premium" &&
user.subscription?.status === "active") {
return next();
}
// Check limits for free users
const spotListCount = await db.collection("spotlists")
.countDocuments({ userId: req.user._id });
if (spotListCount >= 3) {
return res.status(403).json({
error: "Upgrade to premium for unlimited spot lists"
});
}
next();
};
Token Storage (Mobile App)
The mobile app stores tokens securely using Expo Secure Store:
// app/auth/storage.js
import * as SecureStore from 'expo-secure-store';
const key = 'authToken';
const storeToken = async (authToken) => {
await SecureStore.setItemAsync(key, authToken);
};
const getToken = async () => {
return await SecureStore.getItemAsync(key);
};
const removeToken = async () => {
await SecureStore.deleteItemAsync(key);
};
Request Flow
Client Server
│ │
│─── POST /api/auth ───────────────▶│
│ {email, password} │
│ │
│ ┌────────┴────────┐
│ │ Verify password │
│ │ Generate JWT │
│ └────────┬────────┘
│ │
│◀──── {token: "eyJ..."} ───────────│
│ │
│ Store in Secure Store │
│ │
│─── GET /api/listings ────────────▶│
│ x-auth-token: eyJ... │
│ │
│ ┌────────┴────────┐
│ │ auth middleware │
│ │ Verify JWT │
│ │ Set req.user │
│ └────────┬────────┘
│ │
│◀──── [list data] ─────────────────│
Role-Based Access Control
Current roles:
null- Regular user"admin"- Full access to admin endpoints
Admin-only endpoints:
POST /api/trickipedia- Create tricksPUT /api/trickipedia/:id- Update tricksDELETE /api/trickipedia/:id- Delete tricksPOST /api/blog- Create blog postsPATCH /api/blog/:id- Update blog postsDELETE /api/blog/:id- Delete blog posts
Security Recommendations
-
Use environment variable for JWT secret
jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' }); -
Add token expiration
{ expiresIn: '1h' } // or '7d' with refresh tokens -
Implement refresh tokens for better security
-
Add rate limiting on auth endpoints
const rateLimit = require('express-rate-limit');
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5 // 5 attempts
});
router.post('/auth', authLimiter, loginHandler); -
Hash sensitive data in JWT payload or use opaque tokens