access-token.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import crypto from 'crypto';
  2. import type { Ref, IUserHasId } from '@growi/core/dist/interfaces';
  3. import type {
  4. Document, Model, Types, HydratedDocument,
  5. } from 'mongoose';
  6. import { Schema } from 'mongoose';
  7. import mongoosePaginate from 'mongoose-paginate-v2';
  8. import uniqueValidator from 'mongoose-unique-validator';
  9. import loggerFactory from '~/utils/logger';
  10. import { getOrCreateModel } from '../util/mongoose-utils';
  11. const logger = loggerFactory('growi:models:access-token');
  12. const generateTokenHash = (token: string) => crypto.createHash('sha256').update(token).digest('hex');
  13. const getNowDate = () => {
  14. const now = new Date();
  15. now.setHours(0, 0, 0, 0);
  16. return now;
  17. };
  18. type GenerateTokenResult = {
  19. token: string,
  20. _id: Types.ObjectId,
  21. expiredAt: Date,
  22. scope?: string[],
  23. description?: string,
  24. }
  25. export type IAccessToken = {
  26. user: Ref<IUserHasId>,
  27. tokenHash: string,
  28. expiredAt: Date,
  29. scope?: string[],
  30. description?: string,
  31. }
  32. export interface IAccessTokenDocument extends IAccessToken, Document {
  33. isExpired: () => boolean
  34. }
  35. export interface IAccessTokenModel extends Model<IAccessTokenDocument> {
  36. generateToken: (userId: Types.ObjectId | string, expiredAt: Date, scope?: string[], description?: string,) => 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: (token: string) => Promise<HydratedDocument<IAccessTokenDocument> | null>
  42. findTokenByUserId: (userId: Types.ObjectId | string) => Promise<HydratedDocument<IAccessTokenDocument>[] | null>
  43. validateTokenScopes: (token: string, requiredScope: string[]) => Promise<boolean>
  44. }
  45. const accessTokenSchema = new Schema<IAccessTokenDocument, IAccessTokenModel>({
  46. user: {
  47. type: Schema.Types.ObjectId, ref: 'User', required: true,
  48. },
  49. tokenHash: { type: String, required: true, unique: true },
  50. expiredAt: { type: Date, required: true, index: true },
  51. scope: [{ type: String, default: '' }],
  52. description: { type: String, default: '' },
  53. });
  54. accessTokenSchema.plugin(mongoosePaginate);
  55. accessTokenSchema.plugin(uniqueValidator);
  56. accessTokenSchema.statics.generateToken = async function(userId: Types.ObjectId | string, expiredAt: Date, scope?: string[], description?: string) {
  57. const token = crypto.randomBytes(32).toString('hex');
  58. const tokenHash = generateTokenHash(token);
  59. try {
  60. const { _id } = await this.create({
  61. user: userId, tokenHash, expiredAt, scope, description,
  62. });
  63. logger.debug('Token generated');
  64. return {
  65. token, _id, expiredAt, scope, description,
  66. };
  67. }
  68. catch (err) {
  69. logger.debug('Failed to generate token');
  70. throw err;
  71. }
  72. };
  73. accessTokenSchema.statics.deleteToken = async function(token: string) {
  74. const tokenHash = generateTokenHash(token);
  75. await this.deleteOne({ tokenHash });
  76. };
  77. accessTokenSchema.statics.deleteTokenById = async function(tokenId: Types.ObjectId | string) {
  78. await this.deleteOne({ _id: tokenId });
  79. };
  80. accessTokenSchema.statics.deleteAllTokensByUserId = async function(userId: Types.ObjectId | string) {
  81. await this.deleteMany({ user: userId });
  82. };
  83. accessTokenSchema.statics.deleteExpiredToken = async function() {
  84. const now = getNowDate();
  85. await this.deleteMany({ expiredAt: { $lt: now } });
  86. };
  87. accessTokenSchema.statics.findUserIdByToken = async function(token: string) {
  88. const tokenHash = generateTokenHash(token);
  89. const now = getNowDate();
  90. return this.findOne({ tokenHash, expiredAt: { $gte: now } }).select('user');
  91. };
  92. accessTokenSchema.statics.findTokenByUserId = async function(userId: Types.ObjectId | string) {
  93. const now = getNowDate();
  94. return this.find({ user: userId, expiredAt: { $gte: now } }).select('_id expiredAt scope description');
  95. };
  96. accessTokenSchema.statics.validateTokenScopes = async function(token: string, requiredScopes: string[]) {
  97. const tokenHash = generateTokenHash(token);
  98. const now = getNowDate();
  99. const tokenData = await this.findOne({ tokenHash, expiredAt: { $gte: now }, scope: { $all: requiredScopes } });
  100. return tokenData != null;
  101. };
  102. accessTokenSchema.methods.isExpired = function() {
  103. return this.expiredAt < new Date();
  104. };
  105. export const AccessToken = getOrCreateModel<IAccessTokenDocument, IAccessTokenModel>('AccessToken', accessTokenSchema);