Skip to content

fix: switch auth tokens to httpOnly cookies#117

Merged
GitAddRemote merged 9 commits intomainfrom
fix/ISSUE-93-httponly-cookies
Apr 13, 2026
Merged

fix: switch auth tokens to httpOnly cookies#117
GitAddRemote merged 9 commits intomainfrom
fix/ISSUE-93-httponly-cookies

Conversation

@GitAddRemote
Copy link
Copy Markdown
Owner

Summary

  • Moves access and refresh tokens from the response body into httpOnly cookies to protect against XSS-based token theft
  • JwtStrategy now extracts the access token from req.cookies.access_token
  • RefreshTokenAuthGuard rewritten as a simple CanActivate guard reading refresh_token from cookies — removes the passport-http-bearer dependency entirely
  • login sets both cookies (access: 15m, refresh: 7d); refresh rotates both; logout clears both
  • cookie-parser registered in main.ts; CORS configured with credentials: true

Test plan

  • POST /auth/login — response body no longer contains tokens; cookies access_token and refresh_token are set with httpOnly flag
  • Protected routes work using the access_token cookie (no Authorization header needed)
  • POST /auth/refresh — rotates both cookies; old refresh token is revoked
  • POST /auth/logout — both cookies are cleared; refresh token revoked in DB
  • pnpm typecheck passes with zero errors
  • pnpm lint passes clean

Closes #93

Copilot AI review requested due to automatic review settings April 11, 2026 22:24
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates authentication from returning JWTs in JSON responses to using httpOnly cookies, updates JWT extraction to read from cookies, and simplifies refresh-token handling by removing the bearer strategy dependency.

Changes:

  • Set access_token and refresh_token as httpOnly cookies on login/refresh; clear cookies on logout.
  • Update JwtStrategy to extract the access token from req.cookies.access_token; replace refresh-token Passport strategy/guard with a cookie-reading CanActivate guard.
  • Register cookie-parser and enable CORS with credentials: true; remove passport-http-bearer dependency.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds cookie-parser (+ types) and removes passport-http-bearer from the lockfile.
backend/package.json Adds cookie-parser (+ types) and removes passport-http-bearer (+ types).
backend/src/main.ts Registers cookie-parser, configures credentialed CORS, and updates Swagger auth configuration (partially).
backend/src/modules/auth/jwt.strategy.ts Switches JWT extraction from bearer header to cookie extractor.
backend/src/modules/auth/refresh-token-auth.guard.ts Rewrites refresh guard to read refresh_token from cookies and attach it to request.user.
backend/src/modules/auth/auth.controller.ts Sets/rotates/clears auth cookies in login/refresh/logout handlers.
backend/src/modules/auth/auth.service.ts Renames returned token fields to { accessToken, refreshToken } for internal cookie-setting use.
backend/src/modules/auth/auth.module.ts Removes the deleted refresh-token strategy from providers.
backend/src/modules/auth/refresh-token.strategy.ts Deletes the old passport-http-bearer refresh token strategy.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/src/modules/auth/auth.controller.ts Outdated
Comment thread backend/src/modules/auth/auth.controller.ts Outdated
Comment thread backend/src/modules/auth/auth.controller.ts Outdated
Comment thread backend/src/modules/auth/auth.controller.ts
Comment thread backend/src/modules/auth/auth.controller.ts Outdated
Comment thread backend/src/modules/auth/jwt.strategy.ts
Comment thread backend/src/main.ts Outdated
Comment thread backend/src/modules/auth/auth.controller.ts Outdated
Comment thread backend/src/modules/auth/auth.controller.ts
Comment thread backend/src/modules/auth/auth.controller.ts
Moves access and refresh tokens out of the response body and
into httpOnly cookies to prevent XSS-based token theft.

Changes:
- Register cookie-parser middleware in main.ts
- CORS configured with credentials: true for cross-origin cookie support
- JwtStrategy extracts access_token from req.cookies
- login sets access_token (15m) and refresh_token (7d) as httpOnly cookies
- refresh rotates both cookies; logout clears them
- RefreshTokenAuthGuard rewritten as CanActivate reading refresh_token cookie
- Remove passport-http-bearer dependency (no longer needed)

Closes #93
@GitAddRemote GitAddRemote self-assigned this Apr 12, 2026
@GitAddRemote GitAddRemote force-pushed the fix/ISSUE-93-httponly-cookies branch from 83afdec to 616c33c Compare April 13, 2026 00:03
Controller:
- Extract cookie options into private cookieOptions() to avoid duplication
- Switch sameSite from 'lax' to 'strict' to minimize CSRF surface
- clearCookie now passes full options (httpOnly, secure, sameSite) to
  ensure consistent removal across deployments
- login returns { message, username } so clients know who is logged in

JwtStrategy:
- Add Authorization: Bearer as fallback extractor so Swagger and API
  clients continue to work alongside cookie-based browser clients

E2E tests:
- Register cookie-parser middleware in all test app instances
- Extract access_token from Set-Cookie response header after login
- Replace Authorization: Bearer with Cookie header across all specs
Copilot AI review requested due to automatic review settings April 13, 2026 02:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/test/user-organization-roles.e2e-spec.ts Outdated
Comment thread backend/src/main.ts
supertest types headers as { [key: string]: string } so a direct
cast to string[] fails TS2352. Going through unknown is the correct
escape hatch for this known library type gap.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/src/modules/auth/auth.controller.ts Outdated
Comment thread backend/test/user-organization-roles.e2e-spec.ts Outdated
- Derive clearCookie options from cookieOptions() helper by destructuring
  out maxAge, eliminating duplicated security-critical flags in logout
- Align @ApiBearerAuth decorator with Swagger scheme name 'access-token'
  so Swagger UI sends the Authorization header on secured endpoints
- Capture userId from /auth/register response instead of login body since
  login no longer returns userId (tokens moved to httpOnly cookies)
@GitAddRemote GitAddRemote requested a review from Copilot April 13, 2026 04:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/src/main.ts Outdated
Comment thread backend/test/roles.e2e-spec.ts Outdated
Comment thread backend/test/organizations.e2e-spec.ts Outdated
Comment thread backend/test/user-organization-roles.e2e-spec.ts Outdated
Comment thread backend/test/user-organization-roles.e2e-spec.ts
Comment thread backend/test/user-organization-roles.e2e-spec.ts
- Read PORT as number via configService.get<number>() so app.listen()
  receives a proper TCP port; Joi schema already coerces it to a number
- Add .expect(201) to login calls in roles and organizations e2e tests
  so cookie-extraction failures are caught at the right step
- Add .expect(201) + expect(id).toBeDefined() to register call in
  user-organization-roles e2e test to catch registration failures early
- Add .expect(201) to org and role creation in beforeAll so auth
  regressions surface at the setup step rather than as undefined IDs
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

backend/src/modules/auth/auth.controller.ts:80

  • This PR is marked as closing #93, but the acceptance criteria for #93 includes adding a GET /auth/me endpoint and removing frontend localStorage token usage. I couldn’t find an /auth/me handler in this controller, and the frontend still references localStorage.getItem('access_token') (e.g., frontend/src/components/ProtectedRoute.tsx). Either implement the remaining parts or update the PR description/linked issue status to reflect the actual scope.
  @ApiOperation({ summary: 'Register new user' })
  @ApiResponse({ status: 201, description: 'User successfully registered' })
  @ApiResponse({ status: 400, description: 'Invalid input data' })
  // Registration Route: No guards required
  @Post('register')

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/src/modules/auth/auth.controller.ts
Comment thread backend/test/organizations.e2e-spec.ts Outdated
Comment thread backend/test/roles.e2e-spec.ts Outdated
GitAddRemote added a commit that referenced this pull request Apr 13, 2026
- Configure Helmet with explicit CSP directives that allow Swagger UI's
  inline scripts and styles (unsafe-inline on scriptSrc/styleSrc);
  default Helmet CSP blocks Swagger and leaves the docs page blank
- Extract ALLOWED_ORIGIN into a variable with a localhost fallback so
  CORS never silently falls back to permissive defaults when the env var
  is unset; origin validation is enforced at startup in PR #118
- Remove cookie-parser, @types/cookie-parser, and joi from package.json
  as they are unused on this branch; those deps are introduced in the
  cookie auth (#117) and env validation (#118) PRs respectively
- Add @httpcode(HttpStatus.OK) to all auth POST endpoints that
  document 200 (login, refresh, logout, forgot/reset/change-password)
  so Swagger docs match actual HTTP status codes; update e2e expectations
- Add explicit set-cookie header and access_token cookie assertions in
  roles and organizations e2e beforeAll to surface cookie auth failures
  early with clear messages rather than silent empty authCookie
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (2)

backend/test/user-organization-roles.e2e-spec.ts:149

  • This role-creation request is awaited but doesn’t assert a successful status. If role creation fails (e.g., validation/authorization), role2Response.body.id can be undefined and the test will fail later in a less obvious place. Add .expect(201) (and optionally assert role2Response.body.id is defined) here.
      const role2Response = await request(app.getHttpServer())
        .post('/roles')
        .set('Cookie', authCookie)
        .send({
          name: 'Developer Role',
          permissions: { canDeploy: true },
        });

backend/test/user-organization-roles.e2e-spec.ts:157

  • This second role-creation request is also missing a status assertion. Add .expect(201) (and optionally verify the response body contains an id) so failures surface at the correct step before using role3Response.body.id.
      const role3Response = await request(app.getHttpServer())
        .post('/roles')
        .set('Cookie', authCookie)
        .send({
          name: 'Viewer Role',
          permissions: { canView: true },
        });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/test/user-organization-roles.e2e-spec.ts Outdated
Comment thread backend/test/user-organization-roles.e2e-spec.ts
Comment thread backend/src/modules/auth/auth.controller.ts Outdated
- Fix LocalStrategy.validate() return type from Omit<UserDto, 'password'>
  to Omit<User, 'password'> — validateUser() strips password from a User
  entity, not a UserDto; the wrong annotation caused the unsafe cast in
  the controller
- Replace the unsafe cast in AuthController.login() with Omit<User, 'password'>
  now that the type flows correctly from LocalStrategy
- Add .expect(200) and explicit set-cookie/access_token assertions to
  user-organization-roles.e2e-spec.ts beforeAll (consistent with the
  pattern already applied to roles and organizations specs)
- Add .expect(201) to role assignment setup in permissions test
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/src/modules/auth/auth.controller.ts
With httpOnly cookie auth the frontend cannot decode the JWT to read user
info. GET /auth/me returns { id, username } from the validated access
token so the frontend can populate session state on load or page refresh.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- main.ts: keep both cookieParser (httpOnly cookies) and helmet (security

  headers); adopt main's NODE_ENV validation and isProduction flag

- auth.service.ts: keep camelCase return type on refreshAccessToken —

  controller already references tokens.accessToken / tokens.refreshToken
@GitAddRemote GitAddRemote merged commit f36a91a into main Apr 13, 2026
9 checks passed
@GitAddRemote GitAddRemote deleted the fix/ISSUE-93-httponly-cookies branch April 13, 2026 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tech Story: Switch auth tokens to httpOnly cookies

2 participants