Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/app/bin/openapi/definition-apiv1.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
{
bearer: [],
accessTokenInQuery: [],
accessTokenHeaderAuth: [],
},
],
components: {
Expand All @@ -40,6 +41,12 @@ module.exports = {
in: 'query',
description: 'Access token generated by each GROWI users',
},
accessTokenHeaderAuth: {
type: 'apiKey',
in: 'header',
name: 'x-growi-access-token',
description: 'Access token generated by each GROWI users',
},
},
},
};
7 changes: 7 additions & 0 deletions apps/app/bin/openapi/definition-apiv3.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
{
bearer: [],
accessTokenInQuery: [],
accessTokenHeaderAuth: [],
},
],
components: {
Expand All @@ -50,6 +51,12 @@ module.exports = {
in: 'header',
name: 'x-growi-transfer-key',
},
accessTokenHeaderAuth: {
type: 'apiKey',
in: 'header',
name: 'x-growi-access-token',
description: 'Access token generated by each GROWI users',
},
},
},
'x-tagGroups': [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { faker } from '@faker-js/faker';
import { SCOPE } from '@growi/core/dist/interfaces';
import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
import type { Response } from 'express';
import { mock } from 'vitest-mock-extended';

import { SCOPE } from '@growi/core/dist/interfaces';
import type Crowi from '~/server/crowi';
import type UserEvent from '~/server/events/user';
import { AccessToken } from '~/server/models/access-token';
Expand Down Expand Up @@ -210,4 +210,36 @@ describe('access-token-parser middleware for access token with scopes', () => {
expect(serializeUserSecurely).toHaveBeenCalledOnce();
});

it('should authenticate with X-GROWI-ACCESS-TOKEN header and wildcard scope', async() => {
// arrange
const reqMock = mock<AccessTokenParserReq>({
user: undefined,
});
const resMock = mock<Response>();

// 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 read:user:* scope
const { token } = await AccessToken.generateToken(
targetUser._id,
new Date(Date.now() + 1000 * 60 * 60 * 24),
[SCOPE.READ.USER_SETTINGS.ALL],
);

// act - try to access with read:user:info scope
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();
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ export const parserForAccessToken = (scopes: Scope[]) => {
// 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;
const accessToken = bearerToken
?? req.headers['x-growi-access-token']
?? req.query.access_token
?? req.body.access_token;
if (accessToken == null || typeof accessToken !== 'string') {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,38 @@ describe('access-token-parser middleware', () => {
expect(serializeUserSecurely).toHaveBeenCalledOnce();
});

it('should set req.user with a valid Bearer token in X-GROWI-ACCESS-TOKEN header', async() => {
// arrange
const reqMock = mock<AccessTokenParserReq>({
user: undefined,
headers: {
authorization: undefined,
'x-growi-access-token': 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>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ export const parserForApiToken = async(req: AccessTokenParserReq, res: Response)
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;
const accessToken = bearerToken
?? req.headers['x-growi-access-token']
?? req.query.access_token
?? req.body.access_token;

if (accessToken == null || typeof accessToken !== 'string') {
return;
Expand Down
3 changes: 2 additions & 1 deletion apps/app/src/server/routes/apiv3/activity.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { SCOPE } from '@growi/core/dist/interfaces';
import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
import { parseISO, addMinutes, isValid } from 'date-fns';
import type { Request, Router } from 'express';
import express from 'express';
import { query } from 'express-validator';

import type { IActivity, ISearchFilter } from '~/interfaces/activity';
import { SCOPE } from '@growi/core/dist/interfaces';
import { accessTokenParser } from '~/server/middlewares/access-token-parser';
import Activity from '~/server/models/activity';
import { configManager } from '~/server/service/config-manager';
Expand Down Expand Up @@ -185,6 +185,7 @@ module.exports = (crowi: Crowi): Router => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* parameters:
* - name: limit
* in: query
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/server/routes/apiv3/app-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /app-settings
* description: get app setting params
* responses:
Expand Down Expand Up @@ -1052,6 +1053,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: AccessToken supported.
* description: Update V5SchemaMigration
* responses:
Expand Down Expand Up @@ -1098,6 +1100,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: AccessToken supported.
* description: Update MaintenanceMode
* requestBody:
Expand Down
8 changes: 7 additions & 1 deletion apps/app/src/server/routes/apiv3/bookmark-folder.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { SCOPE } from '@growi/core/dist/interfaces';
import { ErrorV3 } from '@growi/core/dist/models';
import { body } from 'express-validator';
import type { Types } from 'mongoose';

import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
import { SCOPE } from '@growi/core/dist/interfaces';
import { accessTokenParser } from '~/server/middlewares/access-token-parser';
import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
import { InvalidParentBookmarkFolderError } from '~/server/models/errors';
Expand Down Expand Up @@ -132,6 +132,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: Create bookmark folder
* description: Create a new bookmark folder
* requestBody:
Expand Down Expand Up @@ -189,6 +190,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: List bookmark folders of a user
* description: List bookmark folders of a user
* parameters:
Expand Down Expand Up @@ -278,6 +280,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: Delete bookmark folder
* description: Delete a bookmark folder and its children
* parameters:
Expand Down Expand Up @@ -321,6 +324,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: Update bookmark folder
* description: Update a bookmark folder
* requestBody:
Expand Down Expand Up @@ -379,6 +383,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: Update bookmark folder
* description: Update a bookmark folder
* requestBody:
Expand Down Expand Up @@ -431,6 +436,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: Update bookmark in folder
* description: Update a bookmark in a folder
* requestBody:
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/server/routes/apiv3/g2g-transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ module.exports = (crowi: Crowi): Router => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* requestBody:
* required: true
* content:
Expand Down Expand Up @@ -526,6 +527,7 @@ module.exports = (crowi: Crowi): Router => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* requestBody:
* required: true
* content:
Expand Down
5 changes: 5 additions & 0 deletions apps/app/src/server/routes/apiv3/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export default function route(crowi: Crowi): void {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /import
* description: Get import settings params
* responses:
Expand Down Expand Up @@ -227,6 +228,7 @@ export default function route(crowi: Crowi): void {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /import/status
* description: Get properties of stored zip files for import
* responses:
Expand Down Expand Up @@ -258,6 +260,7 @@ export default function route(crowi: Crowi): void {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /import
* description: import a collection from a zipped json
* requestBody:
Expand Down Expand Up @@ -393,6 +396,7 @@ export default function route(crowi: Crowi): void {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /import/upload
* description: upload a zip file
* requestBody:
Expand Down Expand Up @@ -452,6 +456,7 @@ export default function route(crowi: Crowi): void {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /import/all
* description: Delete all zip files
* responses:
Expand Down
6 changes: 5 additions & 1 deletion apps/app/src/server/routes/apiv3/in-app-notification.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { SCOPE } from '@growi/core/dist/interfaces';
import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
import express from 'express';

import { SupportedAction } from '~/interfaces/activity';
import type { CrowiRequest } from '~/interfaces/crowi-request';
import { SCOPE } from '@growi/core/dist/interfaces';
import { accessTokenParser } from '~/server/middlewares/access-token-parser';
import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';

Expand Down Expand Up @@ -107,6 +107,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /in-app-notification/list
* description: Get the list of in-app notifications
* parameters:
Expand Down Expand Up @@ -199,6 +200,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /in-app-notification/status
* description: Get the status of in-app notifications
* responses:
Expand Down Expand Up @@ -236,6 +238,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /in-app-notification/open
* description: Open the in-app notification
* requestBody:
Expand Down Expand Up @@ -283,6 +286,7 @@ module.exports = (crowi) => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /in-app-notification/all-statuses-open
* description: Open all in-app notifications
* responses:
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/server/routes/apiv3/page-listing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const routerFactory = (crowi: Crowi): Router => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /page-listing/root
* description: Get the root page
* responses:
Expand Down Expand Up @@ -111,6 +112,7 @@ const routerFactory = (crowi: Crowi): Router => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /page-listing/children
* description: Get the children of a page
* parameters:
Expand Down Expand Up @@ -167,6 +169,7 @@ const routerFactory = (crowi: Crowi): Router => {
* security:
* - bearer: []
* - accessTokenInQuery: []
* - accessTokenHeaderAuth: []
* summary: /page-listing/info
* description: Get summary information of pages
* parameters:
Expand Down
Loading