access-token.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import type { IUserHasId, Ref, Scope } from '@growi/core/dist/interfaces';
  2. import crypto from 'crypto';
  3. import type { Document, HydratedDocument, Model, Types } from 'mongoose';
  4. import { Schema } from 'mongoose';
  5. import mongoosePaginate from 'mongoose-paginate-v2';
  6. import uniqueValidator from 'mongoose-unique-validator';
  7. import loggerFactory from '~/utils/logger';
  8. import { getOrCreateModel } from '../util/mongoose-utils';
  9. import { extractScopes } from '../util/scope-utils';
  10. const logger = loggerFactory('growi:models:access-token');
  11. const generateTokenHash = (token: string) =>
  12. crypto.createHash('sha256').update(token).digest('hex');
  13. type GenerateTokenResult = {
  14. token: string;
  15. _id: Types.ObjectId;
  16. expiredAt: Date;
  17. scopes?: Scope[];
  18. description?: string;
  19. };
  20. export type IAccessToken = {
  21. user: Ref<IUserHasId>;
  22. tokenHash: string;
  23. expiredAt: Date;
  24. scopes?: Scope[];
  25. description?: string;
  26. };
  27. export interface IAccessTokenDocument extends IAccessToken, Document {
  28. isExpired: () => boolean;
  29. }
  30. export interface IAccessTokenModel extends Model<IAccessTokenDocument> {
  31. generateToken: (
  32. userId: Types.ObjectId | string,
  33. expiredAt: Date,
  34. scopes?: Scope[],
  35. description?: string,
  36. ) => Promise<GenerateTokenResult>;
  37. deleteToken: (token: string) => Promise<void>;
  38. deleteTokenById: (tokenId: Types.ObjectId | string) => Promise<void>;
  39. deleteAllTokensByUserId: (userId: Types.ObjectId | string) => Promise<void>;
  40. deleteExpiredToken: () => Promise<void>;
  41. findUserIdByToken: (
  42. token: string,
  43. requiredScopes: Scope[],
  44. ) => Promise<HydratedDocument<IAccessTokenDocument> | null>;
  45. findTokenByUserId: (
  46. userId: Types.ObjectId | string,
  47. ) => Promise<HydratedDocument<IAccessTokenDocument>[] | null>;
  48. validateTokenScopes: (
  49. token: string,
  50. requiredScopes: Scope[],
  51. ) => Promise<boolean>;
  52. }
  53. const accessTokenSchema = new Schema<IAccessTokenDocument, IAccessTokenModel>({
  54. user: {
  55. type: Schema.Types.ObjectId,
  56. ref: 'User',
  57. required: true,
  58. },
  59. tokenHash: { type: String, required: true, unique: true },
  60. expiredAt: { type: Date, required: true, index: true },
  61. scopes: [{ type: String, default: '' }],
  62. description: { type: String, default: '' },
  63. });
  64. accessTokenSchema.plugin(mongoosePaginate);
  65. accessTokenSchema.plugin(uniqueValidator);
  66. accessTokenSchema.statics.generateToken = async function (
  67. userId: Types.ObjectId | string,
  68. expiredAt: Date,
  69. scopes?: Scope[],
  70. description?: string,
  71. ) {
  72. const extractedScopes = extractScopes(scopes ?? []);
  73. const token = crypto.randomBytes(32).toString('hex');
  74. const tokenHash = generateTokenHash(token);
  75. try {
  76. const { _id } = await this.create({
  77. user: userId,
  78. tokenHash,
  79. expiredAt,
  80. scopes: extractedScopes,
  81. description,
  82. });
  83. logger.debug('Token generated');
  84. return {
  85. token,
  86. _id,
  87. expiredAt,
  88. scopes: extractedScopes,
  89. description,
  90. };
  91. } catch (err) {
  92. logger.debug('Failed to generate token');
  93. throw err;
  94. }
  95. };
  96. accessTokenSchema.statics.deleteToken = async function (token: string) {
  97. const tokenHash = generateTokenHash(token);
  98. await this.deleteOne({ tokenHash });
  99. };
  100. accessTokenSchema.statics.deleteTokenById = async function (
  101. tokenId: Types.ObjectId | string,
  102. ) {
  103. await this.deleteOne({ _id: tokenId });
  104. };
  105. accessTokenSchema.statics.deleteAllTokensByUserId = async function (
  106. userId: Types.ObjectId | string,
  107. ) {
  108. await this.deleteMany({ user: userId });
  109. };
  110. accessTokenSchema.statics.deleteExpiredToken = async function () {
  111. const now = new Date();
  112. await this.deleteMany({ expiredAt: { $lt: now } });
  113. };
  114. accessTokenSchema.statics.findUserIdByToken = async function (
  115. token: string,
  116. requiredScopes: Scope[],
  117. ) {
  118. const tokenHash = generateTokenHash(token);
  119. const now = new Date();
  120. if (requiredScopes.length === 0) {
  121. return;
  122. }
  123. const extractedScopes = extractScopes(requiredScopes);
  124. return this.findOne({
  125. tokenHash,
  126. expiredAt: { $gte: now },
  127. scopes: { $all: extractedScopes },
  128. }).select('user');
  129. };
  130. accessTokenSchema.statics.findTokenByUserId = async function (
  131. userId: Types.ObjectId | string,
  132. ) {
  133. const now = new Date();
  134. return this.find({ user: userId, expiredAt: { $gte: now } }).select(
  135. '_id expiredAt scopes description',
  136. );
  137. };
  138. accessTokenSchema.statics.validateTokenScopes = async function (
  139. token: string,
  140. requiredScopes: Scope[],
  141. ) {
  142. return this.findUserIdByToken(token, requiredScopes) != null;
  143. };
  144. accessTokenSchema.methods.isExpired = function () {
  145. return this.expiredAt < new Date();
  146. };
  147. export const AccessToken = getOrCreateModel<
  148. IAccessTokenDocument,
  149. IAccessTokenModel
  150. >('AccessToken', accessTokenSchema);