Przeglądaj źródła

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

Route parserForAccessToken token resolution through the shared extractAccessToken,
adding X-GROWI-ACCESS-TOKEN header support while leaving scope, read-only, and
serialization behavior unchanged. Adds a header-path integration test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Yuki Takei 1 tydzień temu
rodzic
commit
29fff26ba1

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

@@ -13,7 +13,7 @@
   - _Boundary: extractAccessToken_
 
 - [ ] 2. Core: parser integration with header support
-- [ ] 2.1 (P) Route the scoped access-token parser through the shared extractor
+- [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
   - Observable: the access-token integration suite passes including the new header test, and the existing invalid-token / insufficient-scope / read-only tests remain green

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

@@ -183,6 +183,44 @@ describe('access-token-parser middleware for access token with scopes', () => {
     expect(serializeUserSecurely).toHaveBeenCalledOnce();
   });
 
+  it('should authenticate with token supplied in X-GROWI-ACCESS-TOKEN header', async () => {
+    // arrange
+    const reqMock = mock<AccessTokenParserReq>({
+      user: undefined,
+    });
+    const resMock = mock<Response>();
+
+    expect(reqMock.user).toBeUndefined();
+
+    // prepare a user
+    const targetUser = await User.create({
+      name: faker.person.fullName(),
+      username: faker.string.uuid(),
+      password: faker.internet.password(),
+      lang: 'en_US',
+    });
+
+    // generate token with a wildcard (parent) scope
+    const { token } = await AccessToken.generateToken(
+      targetUser._id,
+      new Date(Date.now() + 1000 * 60 * 60 * 24),
+      [SCOPE.READ.USER_SETTINGS.ALL],
+    );
+
+    // act - supply the token via the X-GROWI-ACCESS-TOKEN header (Express lowercases keys),
+    // and require a narrower scope to also exercise scope satisfaction
+    reqMock.headers['x-growi-access-token'] = token;
+    await parserForAccessToken([SCOPE.READ.USER_SETTINGS.INFO])(
+      reqMock,
+      resMock,
+    );
+
+    // assert
+    expect(reqMock.user).toBeDefined();
+    expect(reqMock.user?._id).toStrictEqual(targetUser._id);
+    expect(serializeUserSecurely).toHaveBeenCalledOnce();
+  });
+
   it('should authenticate with wildcard scope', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({

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

@@ -6,7 +6,7 @@ import type { Response } from 'express';
 import { AccessToken } from '~/server/models/access-token';
 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:access-token',
@@ -14,13 +14,8 @@ const logger = loggerFactory(
 
 export const parserForAccessToken = (scopes: Scope[]) => {
   return 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);
-
-    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;
     }
     if (scopes == null || scopes.length === 0) {