access-token-parser.integ.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import { faker } from '@faker-js/faker';
  2. import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
  3. import type { Response } from 'express';
  4. import { mock } from 'vitest-mock-extended';
  5. import { SCOPE } from '~/interfaces/scope';
  6. import type Crowi from '~/server/crowi';
  7. import type UserEvent from '~/server/events/user';
  8. import { AccessToken } from '~/server/models/access-token';
  9. import type { AccessTokenParserReq } from './interfaces';
  10. import { accessTokenParser } from '.';
  11. vi.mock('@growi/core/dist/models/serializers', { spy: true });
  12. describe('access-token-parser middleware', () => {
  13. let User;
  14. beforeAll(async() => {
  15. const crowiMock = mock<Crowi>({
  16. event: vi.fn().mockImplementation((eventName) => {
  17. if (eventName === 'user') {
  18. return mock<UserEvent>({
  19. on: vi.fn(),
  20. });
  21. }
  22. }),
  23. });
  24. const userModelFactory = (await import('../../models/user')).default;
  25. User = userModelFactory(crowiMock);
  26. });
  27. it('should call next if no access token is provided', async() => {
  28. // arrange
  29. const reqMock = mock<AccessTokenParserReq>({
  30. user: undefined,
  31. });
  32. const resMock = mock<Response>();
  33. const nextMock = vi.fn();
  34. expect(reqMock.user).toBeUndefined();
  35. // act
  36. await accessTokenParser()(reqMock, resMock, nextMock);
  37. // assert
  38. expect(reqMock.user).toBeUndefined();
  39. expect(serializeUserSecurely).not.toHaveBeenCalled();
  40. expect(nextMock).toHaveBeenCalled();
  41. });
  42. it('should call next if the given access token is invalid', async() => {
  43. // arrange
  44. const reqMock = mock<AccessTokenParserReq>({
  45. user: undefined,
  46. });
  47. const resMock = mock<Response>();
  48. const nextMock = vi.fn();
  49. expect(reqMock.user).toBeUndefined();
  50. // act
  51. reqMock.query.access_token = 'invalidToken';
  52. await accessTokenParser()(reqMock, resMock, nextMock);
  53. // assert
  54. expect(reqMock.user).toBeUndefined();
  55. expect(serializeUserSecurely).not.toHaveBeenCalled();
  56. expect(nextMock).toHaveBeenCalled();
  57. });
  58. it('should set req.user with a valid api token in query', async() => {
  59. // arrange
  60. const reqMock = mock<AccessTokenParserReq>({
  61. user: undefined,
  62. });
  63. const resMock = mock<Response>();
  64. const nextMock = vi.fn();
  65. expect(reqMock.user).toBeUndefined();
  66. // prepare a user with an access token
  67. const targetUser = await User.create({
  68. name: faker.person.fullName(),
  69. username: faker.string.uuid(),
  70. password: faker.internet.password(),
  71. lang: 'en_US',
  72. apiToken: faker.internet.password(),
  73. });
  74. // act
  75. reqMock.query.access_token = targetUser.apiToken;
  76. await accessTokenParser()(reqMock, resMock, nextMock);
  77. // assert
  78. expect(reqMock.user).toBeDefined();
  79. expect(reqMock.user?._id).toStrictEqual(targetUser._id);
  80. expect(serializeUserSecurely).toHaveBeenCalledOnce();
  81. expect(nextMock).toHaveBeenCalled();
  82. });
  83. it('should set req.user with a valid api token in body', async() => {
  84. // arrange
  85. const reqMock = mock<AccessTokenParserReq>({
  86. user: undefined,
  87. });
  88. const resMock = mock<Response>();
  89. const nextMock = vi.fn();
  90. expect(reqMock.user).toBeUndefined();
  91. // prepare a user with an access token
  92. const targetUser = await User.create({
  93. name: faker.person.fullName(),
  94. username: faker.string.uuid(),
  95. password: faker.internet.password(),
  96. lang: 'en_US',
  97. apiToken: faker.internet.password(),
  98. });
  99. // act
  100. reqMock.body.access_token = targetUser.apiToken;
  101. await accessTokenParser()(reqMock, resMock, nextMock);
  102. // assert
  103. expect(reqMock.user).toBeDefined();
  104. expect(reqMock.user?._id).toStrictEqual(targetUser._id);
  105. expect(serializeUserSecurely).toHaveBeenCalledOnce();
  106. expect(nextMock).toHaveBeenCalled();
  107. });
  108. });
  109. describe('access-token-parser middleware for access token with scopes', () => {
  110. let User;
  111. beforeAll(async() => {
  112. const crowiMock = mock<Crowi>({
  113. event: vi.fn().mockImplementation((eventName) => {
  114. if (eventName === 'user') {
  115. return mock<UserEvent>({
  116. on: vi.fn(),
  117. });
  118. }
  119. }),
  120. });
  121. const userModelFactory = (await import('../../models/user')).default;
  122. User = userModelFactory(crowiMock);
  123. });
  124. it('should authenticate with no scopes', async() => {
  125. // arrange
  126. const reqMock = mock<AccessTokenParserReq>({
  127. user: undefined,
  128. });
  129. const resMock = mock<Response>();
  130. const nextMock = vi.fn();
  131. expect(reqMock.user).toBeUndefined();
  132. // prepare a user
  133. const targetUser = await User.create({
  134. name: faker.person.fullName(),
  135. username: faker.string.uuid(),
  136. password: faker.internet.password(),
  137. lang: 'en_US',
  138. });
  139. // generate token with read:user:info scope
  140. const { token } = await AccessToken.generateToken(
  141. targetUser._id,
  142. new Date(Date.now() + 1000 * 60 * 60 * 24),
  143. );
  144. // act
  145. reqMock.query.access_token = token;
  146. await accessTokenParser()(reqMock, resMock, nextMock);
  147. // assert
  148. expect(reqMock.user).toBeDefined();
  149. expect(reqMock.user?._id).toStrictEqual(targetUser._id);
  150. expect(serializeUserSecurely).toHaveBeenCalledOnce();
  151. expect(nextMock).toHaveBeenCalled();
  152. });
  153. it('should authenticate with specific scope', async() => {
  154. // arrange
  155. const reqMock = mock<AccessTokenParserReq>({
  156. user: undefined,
  157. });
  158. const resMock = mock<Response>();
  159. const nextMock = vi.fn();
  160. expect(reqMock.user).toBeUndefined();
  161. // prepare a user
  162. const targetUser = await User.create({
  163. name: faker.person.fullName(),
  164. username: faker.string.uuid(),
  165. password: faker.internet.password(),
  166. lang: 'en_US',
  167. });
  168. // generate token with read:user:info scope
  169. const { token } = await AccessToken.generateToken(
  170. targetUser._id,
  171. new Date(Date.now() + 1000 * 60 * 60 * 24),
  172. [SCOPE.READ.USER.INFO],
  173. );
  174. // act
  175. reqMock.query.access_token = token;
  176. await accessTokenParser()(reqMock, resMock, nextMock);
  177. // assert
  178. expect(reqMock.user).toBeDefined();
  179. expect(reqMock.user?._id).toStrictEqual(targetUser._id);
  180. expect(serializeUserSecurely).toHaveBeenCalledOnce();
  181. expect(nextMock).toHaveBeenCalled();
  182. });
  183. it('should reject with insufficient scopes', async() => {
  184. // arrange
  185. const reqMock = mock<AccessTokenParserReq>({
  186. user: undefined,
  187. });
  188. const resMock = mock<Response>();
  189. const nextMock = vi.fn();
  190. expect(reqMock.user).toBeUndefined();
  191. // prepare a user
  192. const targetUser = await User.create({
  193. name: faker.person.fullName(),
  194. username: faker.string.uuid(),
  195. password: faker.internet.password(),
  196. lang: 'en_US',
  197. });
  198. // generate token with read:user:info scope
  199. const { token } = await AccessToken.generateToken(
  200. targetUser._id,
  201. new Date(Date.now() + 1000 * 60 * 60 * 24),
  202. [SCOPE.READ.USER.INFO],
  203. );
  204. // act - try to access with write:user:info scope
  205. reqMock.query.access_token = token;
  206. await accessTokenParser([SCOPE.WRITE.USER.INFO])(reqMock, resMock, nextMock);
  207. // // assert
  208. expect(reqMock.user).toBeUndefined();
  209. expect(serializeUserSecurely).not.toHaveBeenCalled();
  210. expect(nextMock).toHaveBeenCalled();
  211. });
  212. it('should authenticate with write scope implying read scope', async() => {
  213. // arrange
  214. const reqMock = mock<AccessTokenParserReq>({
  215. user: undefined,
  216. });
  217. const resMock = mock<Response>();
  218. const nextMock = vi.fn();
  219. expect(reqMock.user).toBeUndefined();
  220. // prepare a user
  221. const targetUser = await User.create({
  222. name: faker.person.fullName(),
  223. username: faker.string.uuid(),
  224. password: faker.internet.password(),
  225. lang: 'en_US',
  226. });
  227. // generate token with write:user:info scope
  228. const { token } = await AccessToken.generateToken(
  229. targetUser._id,
  230. new Date(Date.now() + 1000 * 60 * 60 * 24),
  231. [SCOPE.WRITE.USER.INFO],
  232. );
  233. // act - try to access with read:user:info scope
  234. reqMock.query.access_token = token;
  235. await accessTokenParser([SCOPE.READ.USER.INFO])(reqMock, resMock, nextMock);
  236. // assert
  237. expect(reqMock.user).toBeDefined();
  238. expect(reqMock.user?._id).toStrictEqual(targetUser._id);
  239. expect(serializeUserSecurely).toHaveBeenCalledOnce();
  240. expect(nextMock).toHaveBeenCalled();
  241. });
  242. it('should authenticate with wildcard scope', async() => {
  243. // arrange
  244. const reqMock = mock<AccessTokenParserReq>({
  245. user: undefined,
  246. });
  247. const resMock = mock<Response>();
  248. const nextMock = vi.fn();
  249. // prepare a user
  250. const targetUser = await User.create({
  251. name: faker.person.fullName(),
  252. username: faker.string.uuid(),
  253. password: faker.internet.password(),
  254. lang: 'en_US',
  255. });
  256. // generate token with read:user:* scope
  257. const { token } = await AccessToken.generateToken(
  258. targetUser._id,
  259. new Date(Date.now() + 1000 * 60 * 60 * 24),
  260. [SCOPE.READ.USER.ALL],
  261. );
  262. // act - try to access with read:user:info scope
  263. reqMock.query.access_token = token;
  264. await accessTokenParser([SCOPE.READ.USER.INFO, SCOPE.READ.USER.API.ACCESS_TOKEN])(reqMock, resMock, nextMock);
  265. // assert
  266. expect(reqMock.user).toBeDefined();
  267. expect(reqMock.user?._id).toStrictEqual(targetUser._id);
  268. expect(serializeUserSecurely).toHaveBeenCalledOnce();
  269. expect(nextMock).toHaveBeenCalled();
  270. });
  271. });