access-token.ts 4.5 KB

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