Skip to main content

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 appleUserId to 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!
);
Security Issue

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
}
Missing Expiration

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.");
}
};
Performance Issue

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 tricks
  • PUT /api/trickipedia/:id - Update tricks
  • DELETE /api/trickipedia/:id - Delete tricks
  • POST /api/blog - Create blog posts
  • PATCH /api/blog/:id - Update blog posts
  • DELETE /api/blog/:id - Delete blog posts

Security Recommendations

  1. Use environment variable for JWT secret

    jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });
  2. Add token expiration

    { expiresIn: '1h' }  // or '7d' with refresh tokens
  3. Implement refresh tokens for better security

  4. 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);
  5. Hash sensitive data in JWT payload or use opaque tokens