| 88 | * short-circuits Fastify on 401 (the route handler never runs). |
| 89 | */ |
| 90 | export function createAuthHook( |
| 91 | authTokenService: IAuthTokenService, |
| 92 | opts?: AuthHookOptions, |
| 93 | ): (req: FastifyRequest, reply: FastifyReply) => Promise<FastifyReply | void> { |
| 94 | const isBypassed = opts?.isBypassed ?? defaultIsBypassed; |
| 95 | |
| 96 | return async (req, reply) => { |
| 97 | // Rate-limit check (ROADMAP M6.4): a banned source is rejected before any |
| 98 | // auth work — even a valid token cannot bypass an active ban. Loopback |
| 99 | // binds pass no limiter, so this branch is a no-op there. |
| 100 | if (opts?.limiter?.isBanned(req.ip) === true) { |
| 101 | return reply |
| 102 | .code(429) |
| 103 | .send(errEnvelope(AUTH_RATE_LIMIT_CODE, AUTH_RATE_LIMIT_MSG, req.id)); |
| 104 | } |
| 105 | |
| 106 | const header = req.headers.authorization; |
| 107 | const token = extractBearer(header); |
| 108 | |
| 109 | // Redact the header view BEFORE the rest of the pipeline logs the request. |
| 110 | // Auth has already consumed the raw value above, so this only affects the |
| 111 | // downstream log view of the request. |
| 112 | if (header !== undefined) { |
| 113 | req.headers.authorization = REDACTED; |
| 114 | } |
| 115 | |
| 116 | if (isBypassed(req)) { |
| 117 | return; |
| 118 | } |
| 119 | |
| 120 | if (token === null) { |
| 121 | opts?.limiter?.recordFailure(req.ip); |
| 122 | return reply |
| 123 | .code(401) |
| 124 | .send(errEnvelope(AUTH_ERROR_CODE, AUTH_ERROR_MSG, req.id)); |
| 125 | } |
| 126 | |
| 127 | if (!(await authTokenService.isValid(token))) { |
| 128 | opts?.limiter?.recordFailure(req.ip); |
| 129 | return reply |
| 130 | .code(401) |
| 131 | .send(errEnvelope(AUTH_ERROR_CODE, AUTH_ERROR_MSG, req.id)); |
| 132 | } |
| 133 | }; |
| 134 | } |