Project Auth

Authentication and Authorization in the Project

Bookshelf uses the bearer-token scheme of JSON Web Tokens with one change that matters for a public catalog: the token reader never refuses. readToken sets req.user when a valid token arrives and otherwise calls next(), so GET /books stays public and still knows who is asking. Refusing is the job of two guards mounted per route:

src/middleware/auth.js — read, then requireJavaScript
const CLAIMS = { issuer: 'api.example.com', audience: 'bookshelf-web' };
export function readToken(req, res, next) {
  const [scheme, token] = (req.get('authorization') ?? '').split(' ');
  if (scheme !== 'Bearer' || !token) return next();           // anonymous is not an error
  try {
    const c = jwt.verify(token, config.ACCESS_SECRET, { algorithms: ['HS256'], ...CLAIMS });
    req.user = { id: c.sub, roles: c.roles ?? [] };
    return next();
  } catch (err) {
    return next(new AppError(err.message, { status: 401, code: 'invalid_token', cause: err }));
  }
}
export function requireAuth(req, res, next) {
  if (req.user) return next();
  res.set('WWW-Authenticate', 'Bearer');
  next(new AppError('Authentication required', { status: 401, code: 'not_authenticated' }));
}
export const requireRole = (...allowed) => (req, res, next) => {
  if (!req.user) return requireAuth(req, res, next);          // 401 before 403
  if (allowed.some((r) => req.user.roles.includes(r))) return next();
  next(new AppError(`Requires one of: ${allowed.join(', ')}`, { status: 403,
    code: 'forbidden' }));
};

issueAccess is the jwt.sign call of JSON Web Tokens with subject: user.id, expiresIn: config.ACCESS_TTL and the same CLAIMS. A present but broken token is still a 401: ignoring a bad signature is how an expired session becomes a mysterious empty page instead of a prompt to sign in. Login compares a bcrypt hash, against a fixed dummy hash when the email is unknown, so a wrong address costs what a wrong password does.

Output of 95
$ curl -s -X POST .../auth/login -d '{"email":"ada@example.com","password":"correct horse..."}'
{"data":{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJyZWFkZXIiXSwi...",
 "tokenType":"Bearer","user":{"id":"u_1","email":"ada@example.com","roles":["reader"]}}}
$ curl -s -X POST .../auth/login -d '{"email":"ada@example.com","password":"hunter2hunter2"}'
{"error":{"code":"bad_credentials","message":"Email or password is wrong"},...}    # 401
$ curl -si -X POST .../books -d '{}'    # 401 + WWW-Authenticate: Bearer, not_authenticated
$ curl -s -X POST .../books -H "authorization: Bearer $READER" -d '...'   # ada is a reader
{"error":{"code":"forbidden","message":"Requires one of: editor"},...}            # 403
$ curl -s .../auth/me -H "authorization: Bearer ${READER:0:-3}AAA"
{"error":{"code":"invalid_token","message":"invalid signature"},...}   # roles edited: 401
$ curl -s -X DELETE .../books/bk_3/reviews/rv_2 -H "authorization: Bearer $READER"
{"error":{"code":"not_owner","message":"You can only delete your own review"},...} # 403

Roles cover books, where "editors may write" is the whole rule. They cannot express reviews, because that rule is your own review, so the ownership check lives in the review service — the only layer that sees both caller and record: if (review.userId !== user.id && !user.roles.includes('editor')).