Bläddra i källkod

feat(access-token-parser): support X-GROWI-ACCESS-TOKEN header on the legacy api-token parser

Route parserForApiToken token resolution through the shared extractAccessToken,
adding X-GROWI-ACCESS-TOKEN header support for the legacy api-token path. Adds a
header-path integration test. acceptLegacy gating (index.ts) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Yuki Takei 1 vecka sedan
förälder
incheckning
c6fe5de04b

+ 2 - 2
.kiro/specs/access-token-parser/tasks.md

@@ -12,7 +12,7 @@
   - _Requirements: 1.3, 3.1, 3.2, 3.3, 3.4_
   - _Boundary: extractAccessToken_
 
-- [ ] 2. Core: parser integration with header support
+- [x] 2. Core: parser integration with header support
 - [x] 2.1 (P) Route the scoped access-token parser through the shared extractor
   - Replace the inline token chain and type guard with the shared extractor; leave scope check, read-only rejection, and user serialization unchanged
   - Add an integration test: a valid scoped token supplied in the `X-GROWI-ACCESS-TOKEN` header with a satisfying scope authenticates the token owner
@@ -20,7 +20,7 @@
   - _Requirements: 1.1, 1.2, 4.1, 4.2, 4.3_
   - _Boundary: parserForAccessToken_
   - _Depends: 1.1_
-- [ ] 2.2 (P) Route the legacy api-token parser through the shared extractor
+- [x] 2.2 (P) Route the legacy api-token parser through the shared extractor
   - Replace the inline token chain and type guard with the shared extractor
   - Add an integration test: a valid legacy api-token supplied in the `X-GROWI-ACCESS-TOKEN` header authenticates the owner; confirm the `acceptLegacy` gating is unchanged (legacy token ignored when the route does not opt in)
   - Observable: the api-token integration suite passes including the new header test

+ 28 - 0
apps/app/src/server/middlewares/access-token-parser/api-token.integ.ts

@@ -150,6 +150,34 @@ describe('access-token-parser middleware', () => {
     expect(serializeUserSecurely).toHaveBeenCalledOnce();
   });
 
+  it('should set req.user with a valid api token in the X-GROWI-ACCESS-TOKEN header', async () => {
+    // arrange
+    const reqMock = mock<AccessTokenParserReq>({
+      user: undefined,
+    });
+    const resMock = mock<Response>();
+
+    expect(reqMock.user).toBeUndefined();
+
+    // prepare a user with an access token
+    const targetUser = await User.create({
+      name: faker.person.fullName(),
+      username: faker.string.uuid(),
+      password: faker.internet.password(),
+      lang: 'en_US',
+      apiToken: faker.internet.password(),
+    });
+
+    // act
+    reqMock.headers['x-growi-access-token'] = targetUser.apiToken;
+    await parserForApiToken(reqMock, resMock);
+
+    // assert
+    expect(reqMock.user).toBeDefined();
+    expect(reqMock.user?._id).toStrictEqual(targetUser._id);
+    expect(serializeUserSecurely).toHaveBeenCalledOnce();
+  });
+
   it('should ignore non-Bearer Authorization header', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({

+ 3 - 10
apps/app/src/server/middlewares/access-token-parser/api-token.ts

@@ -7,7 +7,7 @@ import mongoose from 'mongoose';
 
 import loggerFactory from '~/utils/logger';
 
-import { extractBearerToken } from './extract-bearer-token';
+import { extractAccessToken } from './extract-access-token';
 
 const logger = loggerFactory('growi:middleware:access-token-parser:api-token');
 
@@ -15,15 +15,8 @@ export const parserForApiToken = async (
   req: AccessTokenParserReq,
   res: Response,
 ): Promise<void> => {
-  // Extract token from Authorization header first
-  // It is more efficient to call it only once in "AccessTokenParser," which is the caller of the method
-  const bearerToken = extractBearerToken(req.headers.authorization);
-
-  // Try all possible token sources in order of priority
-  const accessToken =
-    bearerToken ?? req.query.access_token ?? req.body.access_token;
-
-  if (accessToken == null || typeof accessToken !== 'string') {
+  const accessToken = extractAccessToken(req);
+  if (accessToken == null) {
     return;
   }