/* * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; import { UsersService } from '../users/users.service'; import { User } from '../users/user.entity'; import { AuthToken } from './auth-token.entity'; import { AuthTokenDto } from './auth-token.dto'; import { AuthTokenWithSecretDto } from './auth-token-with-secret.dto'; import { compare, hash } from 'bcrypt'; import { NotInDBError, TokenNotValidError } from '../errors/errors'; import { randomBytes } from 'crypto'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ConsoleLoggerService } from '../logger/console-logger.service'; @Injectable() export class AuthService { constructor( private readonly logger: ConsoleLoggerService, private usersService: UsersService, @InjectRepository(AuthToken) private authTokenRepository: Repository, ) { this.logger.setContext(AuthService.name); } async validateToken(token: string): Promise { const parts = token.split('.'); const accessToken = await this.getAuthTokenAndValidate(parts[0], parts[1]); const user = await this.usersService.getUserByUsername( accessToken.user.userName, ); if (user) { await this.setLastUsedToken(parts[0]); return user; } return null; } async hashPassword(cleartext: string): Promise { // hash the password with bcrypt and 2^12 iterations return hash(cleartext, 12); } async checkPassword(cleartext: string, password: string): Promise { return compare(cleartext, password); } async randomString(length: number): Promise { // This is necessary as the is no base64url encoding in the toString method // but as can be seen on https://tools.ietf.org/html/rfc4648#page-7 // base64url is quite easy buildable from base64 if (length <= 0) { return null; } return randomBytes(length); } BufferToBase64Url(text: Buffer): string { return text .toString('base64') .replace('+', '-') .replace('/', '_') .replace(/=+$/, ''); } async createTokenForUser( userName: string, identifier: string, until: number, ): Promise { const user = await this.usersService.getUserByUsername(userName); const secret = await this.randomString(64); const keyId = this.BufferToBase64Url(await this.randomString(8)); const accessTokenString = await this.hashPassword(secret.toString()); const accessToken = this.BufferToBase64Url(Buffer.from(accessTokenString)); let token; if (until === 0) { token = AuthToken.create(user, identifier, keyId, accessToken); } else { token = AuthToken.create(user, identifier, keyId, accessToken, until); } const createdToken = await this.authTokenRepository.save(token); return this.toAuthTokenWithSecretDto(createdToken, `${keyId}.${secret}`); } async setLastUsedToken(keyId: string) { const accessToken = await this.authTokenRepository.findOne({ where: { keyId: keyId }, }); accessToken.lastUsed = new Date().getTime(); await this.authTokenRepository.save(accessToken); } async getAuthTokenAndValidate( keyId: string, token: string, ): Promise { const accessToken = await this.authTokenRepository.findOne({ where: { keyId: keyId }, relations: ['user'], }); if (accessToken === undefined) { throw new NotInDBError(`AuthToken '${token}' not found`); } if (!(await this.checkPassword(token, accessToken.accessTokenHash))) { // hashes are not the same throw new TokenNotValidError(`AuthToken '${token}' is not valid.`); } if ( accessToken.validUntil && accessToken.validUntil < new Date().getTime() ) { // tokens validUntil Date lies in the past throw new TokenNotValidError( `AuthToken '${token}' is not valid since ${new Date( accessToken.validUntil, )}.`, ); } return accessToken; } async getTokensByUsername(userName: string): Promise { const user = await this.usersService.getUserByUsername(userName, true); if (user.authTokens === undefined) { return []; } return user.authTokens; } async removeToken(userName: string, keyId: string) { const user = await this.usersService.getUserByUsername(userName); const token = await this.authTokenRepository.findOne({ where: { keyId: keyId, user: user }, }); await this.authTokenRepository.remove(token); } toAuthTokenDto(authToken: AuthToken | null | undefined): AuthTokenDto | null { if (!authToken) { this.logger.warn(`Recieved ${authToken} argument!`, 'toAuthTokenDto'); return null; } return { label: authToken.identifier, keyId: authToken.keyId, created: authToken.createdAt.getTime(), validUntil: authToken.validUntil, lastUsed: authToken.lastUsed, }; } toAuthTokenWithSecretDto( authToken: AuthToken | null | undefined, secret: string, ): AuthTokenWithSecretDto | null { const tokenDto = this.toAuthTokenDto(authToken); return { ...tokenDto, secret: secret, }; } }