access-token.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  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. type GenerateTokenResult = {
  14. token: string,
  15. _id: Types.ObjectId,
  16. expiredAt: Date,
  17. scope?: string[],
  18. description?: string,
  19. }
  20. export type IAccessToken = {
  21. user: Ref<IUserHasId>,
  22. tokenHash: string,
  23. expiredAt: Date,
  24. scope?: string[],
  25. description?: string,
  26. }
  27. export interface IAccessTokenDocument extends IAccessToken, Document {
  28. isExpired: () => boolean
  29. }
  30. export interface IAccessTokenModel extends Model<IAccessTokenDocument> {
  31. generateToken: (userId: Types.ObjectId, expiredAt: Date, scope: string[], description?: string,) => Promise<GenerateTokenResult>
  32. deleteToken: (token: string) => Promise<void>
  33. deleteTokenById: (tokenId: Types.ObjectId) => Promise<void>
  34. deleteAllTokensByUserId: (userId: Types.ObjectId) => Promise<void>
  35. deleteExpiredToken: () => Promise<void>
  36. findUserIdByToken: (token: string) => Promise<HydratedDocument<IAccessTokenDocument> | null>
  37. findTokenByUserId: (userId: Types.ObjectId) => Promise<HydratedDocument<IAccessTokenDocument>[]>
  38. validateTokenScopes: (token: string, requiredScope: string[]) => Promise<boolean>
  39. }
  40. const accessTokenSchema = new Schema<IAccessTokenDocument, IAccessTokenModel>({
  41. user: {
  42. type: Schema.Types.ObjectId, ref: 'User', required: true,
  43. },
  44. tokenHash: { type: String, required: true, unique: true },
  45. expiredAt: { type: Date, required: true, index: true },
  46. scope: [{ type: String, default: '' }],
  47. description: { type: String, default: '' },
  48. });
  49. accessTokenSchema.plugin(mongoosePaginate);
  50. accessTokenSchema.plugin(uniqueValidator);
  51. accessTokenSchema.statics.generateToken = async function(userId: Types.ObjectId, expiredAt: Date, scope?: string[], description?: string) {
  52. const token = crypto.randomBytes(32).toString('hex');
  53. const tokenHash = generateTokenHash(token);
  54. try {
  55. const { _id } = await this.create({
  56. user: userId, tokenHash, expiredAt, scope, description,
  57. });
  58. logger.debug('Token generated');
  59. return {
  60. token, _id, expiredAt, scope, description,
  61. };
  62. }
  63. catch (err) {
  64. logger.debug('Failed to generate token');
  65. throw err;
  66. }
  67. };
  68. accessTokenSchema.statics.deleteToken = async function(token: string) {
  69. const tokenHash = generateTokenHash(token);
  70. await this.deleteOne({ tokenHash });
  71. };
  72. accessTokenSchema.statics.deleteTokenById = async function(tokenId: Types.ObjectId) {
  73. await this.deleteOne({ _id: tokenId });
  74. };
  75. accessTokenSchema.statics.deleteAllTokensByUserId = async function(userId: Types.ObjectId) {
  76. await this.deleteMany({ user: userId });
  77. };
  78. accessTokenSchema.statics.deleteExpiredToken = async function() {
  79. const now = new Date();
  80. await this.deleteMany({ expiredAt: { $lte: now } });
  81. };
  82. accessTokenSchema.statics.findUserIdByToken = async function(token: string) {
  83. const tokenHash = generateTokenHash(token);
  84. const now = new Date();
  85. return this.findOne({ tokenHash, expiredAt: { $gt: now } }).select('user');
  86. };
  87. accessTokenSchema.statics.findTokenByUserId = async function(userId: Types.ObjectId) {
  88. const now = new Date();
  89. return this.find({ user: userId, expiredAt: { $gt: now } }).select('_id expiredAt scope description');
  90. };
  91. accessTokenSchema.statics.validateTokenScopes = async function(token: string, requiredScopes: string[]) {
  92. const tokenHash = generateTokenHash(token);
  93. const now = new Date();
  94. const tokenData = await this.findOne({ tokenHash, expiredAt: { $gt: now }, scope: { $all: requiredScopes } });
  95. return tokenData != null;
  96. };
  97. accessTokenSchema.methods.isExpired = function() {
  98. return this.expiredAt < new Date();
  99. };
  100. export const AccessToken = getOrCreateModel<IAccessTokenDocument, IAccessTokenModel>('AccessToken', accessTokenSchema);