import crypto from 'crypto'; import type { Ref, IUserHasId } from '@growi/core/dist/interfaces'; import type { Document, Model, Types, HydratedDocument, } from 'mongoose'; import { Schema } from 'mongoose'; import mongoosePaginate from 'mongoose-paginate-v2'; import uniqueValidator from 'mongoose-unique-validator'; import type { Scope } from '~/interfaces/scope'; import loggerFactory from '~/utils/logger'; import { getOrCreateModel } from '../util/mongoose-utils'; import { extractScopes } from '../util/scope-utils'; const logger = loggerFactory('growi:models:access-token'); const generateTokenHash = (token: string) => crypto.createHash('sha256').update(token).digest('hex'); type GenerateTokenResult = { token: string, _id: Types.ObjectId, expiredAt: Date, scopes?: Scope[], description?: string, } export type IAccessToken = { user: Ref, tokenHash: string, expiredAt: Date, scopes?: Scope[], description?: string, } export interface IAccessTokenDocument extends IAccessToken, Document { isExpired: () => boolean } export interface IAccessTokenModel extends Model { generateToken: (userId: Types.ObjectId | string, expiredAt: Date, scopes?: Scope[], description?: string,) => Promise deleteToken: (token: string) => Promise deleteTokenById: (tokenId: Types.ObjectId | string) => Promise deleteAllTokensByUserId: (userId: Types.ObjectId | string) => Promise deleteExpiredToken: () => Promise findUserIdByToken: (token: string, requiredScopes: Scope[]) => Promise | null> findTokenByUserId: (userId: Types.ObjectId | string) => Promise[] | null> validateTokenScopes: (token: string, requiredScopes: Scope[]) => Promise } const accessTokenSchema = new Schema({ user: { type: Schema.Types.ObjectId, ref: 'User', required: true, }, tokenHash: { type: String, required: true, unique: true }, expiredAt: { type: Date, required: true, index: true }, scopes: [{ type: String, default: '' }], description: { type: String, default: '' }, }); accessTokenSchema.plugin(mongoosePaginate); accessTokenSchema.plugin(uniqueValidator); accessTokenSchema.statics.generateToken = async function(userId: Types.ObjectId | string, expiredAt: Date, scopes?: Scope[], description?: string) { const extractedScopes = extractScopes(scopes ?? []); const token = crypto.randomBytes(32).toString('hex'); const tokenHash = generateTokenHash(token); try { const { _id } = await this.create({ user: userId, tokenHash, expiredAt, scopes: extractedScopes, description, }); logger.debug('Token generated'); return { token, _id, expiredAt, scopes: extractedScopes, description, }; } catch (err) { logger.debug('Failed to generate token'); throw err; } }; accessTokenSchema.statics.deleteToken = async function(token: string) { const tokenHash = generateTokenHash(token); await this.deleteOne({ tokenHash }); }; accessTokenSchema.statics.deleteTokenById = async function(tokenId: Types.ObjectId | string) { await this.deleteOne({ _id: tokenId }); }; accessTokenSchema.statics.deleteAllTokensByUserId = async function(userId: Types.ObjectId | string) { await this.deleteMany({ user: userId }); }; accessTokenSchema.statics.deleteExpiredToken = async function() { const now = new Date(); await this.deleteMany({ expiredAt: { $lte: now } }); }; accessTokenSchema.statics.findUserIdByToken = async function(token: string, requiredScopes: Scope[]) { const tokenHash = generateTokenHash(token); const now = new Date(); if (requiredScopes.length === 0) { return; } const extractedScopes = extractScopes(requiredScopes); return this.findOne({ tokenHash, expiredAt: { $gt: now }, scopes: { $all: extractedScopes } }).select('user'); }; accessTokenSchema.statics.findTokenByUserId = async function(userId: Types.ObjectId | string) { const now = new Date(); return this.find({ user: userId, expiredAt: { $gt: now } }).select('_id expiredAt scopes description'); }; accessTokenSchema.statics.validateTokenScopes = async function(token: string, requiredScopes: Scope[]) { return this.findUserIdByToken(token, requiredScopes) != null; }; accessTokenSchema.methods.isExpired = function() { return this.expiredAt < new Date(); }; export const AccessToken = getOrCreateModel('AccessToken', accessTokenSchema);