Split up Auth Service into multiple files
This commit is contained in:
		@@ -1,387 +0,0 @@
 | 
				
			|||||||
import {
 | 
					 | 
				
			||||||
	BadRequestException,
 | 
					 | 
				
			||||||
	ForbiddenException,
 | 
					 | 
				
			||||||
	Injectable,
 | 
					 | 
				
			||||||
	UnauthorizedException
 | 
					 | 
				
			||||||
} from '@nestjs/common';
 | 
					 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					 | 
				
			||||||
import { JWTToken, tfaTypes, User, UserRole } from '../entities';
 | 
					 | 
				
			||||||
import { LessThanOrEqual, Repository } from 'typeorm';
 | 
					 | 
				
			||||||
import * as argon2 from 'argon2';
 | 
					 | 
				
			||||||
import { PassportStrategy } from '@nestjs/passport';
 | 
					 | 
				
			||||||
import { Strategy as LocalStrategy } from 'passport-local';
 | 
					 | 
				
			||||||
import { ExtractJwt, Strategy as JWTStrategy } from 'passport-jwt';
 | 
					 | 
				
			||||||
import FileSystemService from './filesystem';
 | 
					 | 
				
			||||||
import * as jwt from 'jsonwebtoken';
 | 
					 | 
				
			||||||
import { createTransport } from 'nodemailer';
 | 
					 | 
				
			||||||
import * as notp from 'notp';
 | 
					 | 
				
			||||||
import { randomBytes } from 'crypto';
 | 
					 | 
				
			||||||
import { FastifyRequest } from 'fastify';
 | 
					 | 
				
			||||||
import axios from 'axios';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const GITLAB_ID =
 | 
					 | 
				
			||||||
	'98bcbad78cb1f880d1d1de62291d70a791251a7bea077bfe7df111ef3c115760';
 | 
					 | 
				
			||||||
const GITLAB_SECRET =
 | 
					 | 
				
			||||||
	'7ee01d2b204aff3a05f9d028f004d169b6d381ec873e195f314b3935fa150959';
 | 
					 | 
				
			||||||
const GITLAB_URL = 'https://gitlab.mattv.de';
 | 
					 | 
				
			||||||
const GITLAB_API_URL = 'https://ssh.gitlab.mattv.de';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const jwtSecret = 'CUM';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mailAccount = createTransport({
 | 
					 | 
				
			||||||
	host: 'mail.mattv.de',
 | 
					 | 
				
			||||||
	port: 587,
 | 
					 | 
				
			||||||
	secure: false,
 | 
					 | 
				
			||||||
	auth: {
 | 
					 | 
				
			||||||
		user: 'no-reply@mattv.de',
 | 
					 | 
				
			||||||
		pass: 'noreplyLONGPASS123'
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface jwtPayload {
 | 
					 | 
				
			||||||
	sub: number;
 | 
					 | 
				
			||||||
	jti: number;
 | 
					 | 
				
			||||||
	exp?: number;
 | 
					 | 
				
			||||||
	iat?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface GitlabTokenResponse {
 | 
					 | 
				
			||||||
	access_token: string;
 | 
					 | 
				
			||||||
	token_type: string;
 | 
					 | 
				
			||||||
	expires_in: number;
 | 
					 | 
				
			||||||
	refresh_token: string;
 | 
					 | 
				
			||||||
	created_at: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface GitlabUserResponse {
 | 
					 | 
				
			||||||
	username: string;
 | 
					 | 
				
			||||||
	is_admin?: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Injectable()
 | 
					 | 
				
			||||||
export class AuthService {
 | 
					 | 
				
			||||||
	constructor(
 | 
					 | 
				
			||||||
		@InjectRepository(User)
 | 
					 | 
				
			||||||
		private userRepo: Repository<User>,
 | 
					 | 
				
			||||||
		@InjectRepository(JWTToken)
 | 
					 | 
				
			||||||
		private tokenRepo: Repository<JWTToken>,
 | 
					 | 
				
			||||||
		private fsService: FileSystemService
 | 
					 | 
				
			||||||
	) {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	generateTfaSecret(): string {
 | 
					 | 
				
			||||||
		const set =
 | 
					 | 
				
			||||||
			'0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz!@#$%^&*()<>?/[]{},.:;';
 | 
					 | 
				
			||||||
		return randomBytes(32)
 | 
					 | 
				
			||||||
			.map((b) =>
 | 
					 | 
				
			||||||
				set.charCodeAt(Math.floor((b / 255.0) * (set.length - 1)))
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
			.toString();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async getUser(userId: number): Promise<User | null> {
 | 
					 | 
				
			||||||
		return this.userRepo.findOneBy({
 | 
					 | 
				
			||||||
			id: userId
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async findUser(username: string, gitlab: boolean): Promise<User | null> {
 | 
					 | 
				
			||||||
		return this.userRepo.findOneBy({
 | 
					 | 
				
			||||||
			name: username,
 | 
					 | 
				
			||||||
			isGitlabUser: gitlab
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async getToken(tokenId: number): Promise<JWTToken | null> {
 | 
					 | 
				
			||||||
		return this.tokenRepo.findOneBy({
 | 
					 | 
				
			||||||
			id: tokenId
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async validateUser(username: string, pass: string): Promise<User | null> {
 | 
					 | 
				
			||||||
		const user = await this.findUser(username, false);
 | 
					 | 
				
			||||||
		if (!user)
 | 
					 | 
				
			||||||
			throw new UnauthorizedException('Invalid username or password');
 | 
					 | 
				
			||||||
		if (!(await argon2.verify(user.password, pass)))
 | 
					 | 
				
			||||||
			throw new UnauthorizedException('Invalid username or password');
 | 
					 | 
				
			||||||
		if (user.role == UserRole.DISABLED)
 | 
					 | 
				
			||||||
			throw new UnauthorizedException('Account is disabled');
 | 
					 | 
				
			||||||
		return user;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async cleanupTokens(): Promise<void> {
 | 
					 | 
				
			||||||
		await this.tokenRepo.delete({
 | 
					 | 
				
			||||||
			exp: LessThanOrEqual(Math.floor(Date.now() / 1000))
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	requiresTfa(user: User): boolean {
 | 
					 | 
				
			||||||
		return user.tfaType != tfaTypes.NONE;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async verifyTfa(
 | 
					 | 
				
			||||||
		user: User,
 | 
					 | 
				
			||||||
		token: string,
 | 
					 | 
				
			||||||
		type?: tfaTypes
 | 
					 | 
				
			||||||
	): Promise<boolean> {
 | 
					 | 
				
			||||||
		if (!type) type = user.tfaType;
 | 
					 | 
				
			||||||
		const delta = notp.totp.verify(token, user.tfaSecret, {
 | 
					 | 
				
			||||||
			window: 10
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
		return (
 | 
					 | 
				
			||||||
			delta &&
 | 
					 | 
				
			||||||
			(type == tfaTypes.EMAIL ? delta.delta <= 0 : delta.delta == 0)
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async sendTfaMail(user: User) {
 | 
					 | 
				
			||||||
		await mailAccount.sendMail({
 | 
					 | 
				
			||||||
			from: 'fileserver@mattv.de',
 | 
					 | 
				
			||||||
			to: user.name,
 | 
					 | 
				
			||||||
			subject: 'Fileserver - EMail 2fa code',
 | 
					 | 
				
			||||||
			text: `Your code is: ${notp.totp.gen(
 | 
					 | 
				
			||||||
				user.tfaSecret
 | 
					 | 
				
			||||||
			)}\nIt is valid for 5 Minutes`
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async setupTfa(user: User): Promise<string> {
 | 
					 | 
				
			||||||
		if (user.tfaType != tfaTypes.NONE)
 | 
					 | 
				
			||||||
			throw new BadRequestException(
 | 
					 | 
				
			||||||
				'2 Factor authentication is already setup'
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
		const secret = this.generateTfaSecret();
 | 
					 | 
				
			||||||
		user.tfaSecret = secret;
 | 
					 | 
				
			||||||
		await this.userRepo.save(user);
 | 
					 | 
				
			||||||
		return secret;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async setTfaType(user: User, type: tfaTypes) {
 | 
					 | 
				
			||||||
		user.tfaType = type;
 | 
					 | 
				
			||||||
		await this.userRepo.save(user);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async login(req: Request, user: User) {
 | 
					 | 
				
			||||||
		if (user.isGitlabUser && !(await this.verifyGitlabUser(req, user))) {
 | 
					 | 
				
			||||||
			await this.revokeAll(user);
 | 
					 | 
				
			||||||
			throw new UnauthorizedException('Invalid gitlab token');
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		const token = new JWTToken();
 | 
					 | 
				
			||||||
		token.ownerId = user.id;
 | 
					 | 
				
			||||||
		const db_token = await this.tokenRepo.save(token);
 | 
					 | 
				
			||||||
		const payload: jwtPayload = {
 | 
					 | 
				
			||||||
			sub: user.id,
 | 
					 | 
				
			||||||
			jti: db_token.id
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
		const jwtToken = jwt.sign(payload, jwtSecret, {
 | 
					 | 
				
			||||||
			mutatePayload: true,
 | 
					 | 
				
			||||||
			expiresIn: '1d'
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
		db_token.exp = payload.exp;
 | 
					 | 
				
			||||||
		await this.tokenRepo.save(db_token);
 | 
					 | 
				
			||||||
		return jwtToken;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async singupInternal(user: User): Promise<User> {
 | 
					 | 
				
			||||||
		const root = await this.fsService.generateRoot(user);
 | 
					 | 
				
			||||||
		user.rootId = root.id;
 | 
					 | 
				
			||||||
		return this.userRepo.save(user);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async singupGitlab(
 | 
					 | 
				
			||||||
		info: GitlabUserResponse,
 | 
					 | 
				
			||||||
		data: GitlabTokenResponse
 | 
					 | 
				
			||||||
	): Promise<User> {
 | 
					 | 
				
			||||||
		const user = new User();
 | 
					 | 
				
			||||||
		user.name = info.username;
 | 
					 | 
				
			||||||
		user.password = '';
 | 
					 | 
				
			||||||
		user.isGitlabUser = true;
 | 
					 | 
				
			||||||
		user.role = info.is_admin ? UserRole.ADMIN : UserRole.DISABLED;
 | 
					 | 
				
			||||||
		return this.singupInternal(await this.setGitlabTokens(user, data));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async signup(username: string, password: string) {
 | 
					 | 
				
			||||||
		if (await this.findUser(username, false))
 | 
					 | 
				
			||||||
			throw new BadRequestException('User already exists');
 | 
					 | 
				
			||||||
		const user = new User();
 | 
					 | 
				
			||||||
		user.name = username;
 | 
					 | 
				
			||||||
		user.password = await argon2.hash(password);
 | 
					 | 
				
			||||||
		await this.singupInternal(await this.userRepo.save(user));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async revoke(token: JWTToken) {
 | 
					 | 
				
			||||||
		await this.tokenRepo.delete({
 | 
					 | 
				
			||||||
			id: token.id
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async revokeAll(user: User) {
 | 
					 | 
				
			||||||
		await this.tokenRepo.delete({
 | 
					 | 
				
			||||||
			ownerId: user.id
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async setGitlabTokens(
 | 
					 | 
				
			||||||
		user: User,
 | 
					 | 
				
			||||||
		data: GitlabTokenResponse
 | 
					 | 
				
			||||||
	): Promise<User> {
 | 
					 | 
				
			||||||
		user.gitlabAT = data.access_token;
 | 
					 | 
				
			||||||
		user.gitlabRT = data.refresh_token;
 | 
					 | 
				
			||||||
		return this.userRepo.save(user);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	getGitlabRedirectUrl(req: Request): string {
 | 
					 | 
				
			||||||
		const _req = req as unknown as FastifyRequest;
 | 
					 | 
				
			||||||
		return `${_req.protocol}://${_req.hostname}/api/auth/gitlab_callback`;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	getGitlabAuthUrl(req: Request): string {
 | 
					 | 
				
			||||||
		const params = new URLSearchParams();
 | 
					 | 
				
			||||||
		params.append('redirect_uri', this.getGitlabRedirectUrl(req));
 | 
					 | 
				
			||||||
		params.append('response_type', 'code');
 | 
					 | 
				
			||||||
		params.append('scope', 'read_user');
 | 
					 | 
				
			||||||
		params.append('client_id', GITLAB_ID);
 | 
					 | 
				
			||||||
		return `${GITLAB_URL}/oauth/authorize?${params.toString()}`;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async getGitlabUserFromCode(req: Request, code: string): Promise<User> {
 | 
					 | 
				
			||||||
		const params = new URLSearchParams();
 | 
					 | 
				
			||||||
		params.append('redirect_uri', this.getGitlabRedirectUrl(req));
 | 
					 | 
				
			||||||
		params.append('client_id', GITLAB_ID);
 | 
					 | 
				
			||||||
		params.append('client_secret', GITLAB_SECRET);
 | 
					 | 
				
			||||||
		params.append('code', code);
 | 
					 | 
				
			||||||
		params.append('grant_type', 'authorization_code');
 | 
					 | 
				
			||||||
		const resp = await axios.post(
 | 
					 | 
				
			||||||
			`${GITLAB_API_URL}/oauth/token?${params.toString()}`,
 | 
					 | 
				
			||||||
			{}
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
		const data: GitlabTokenResponse = resp.data;
 | 
					 | 
				
			||||||
		const userInfoResp = await axios.get(`${GITLAB_API_URL}/api/v4/user`, {
 | 
					 | 
				
			||||||
			headers: { Authorization: `Bearer ${data.access_token}` }
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
		const userInfo: GitlabUserResponse = userInfoResp.data;
 | 
					 | 
				
			||||||
		let user = await this.findUser(userInfo.username, true);
 | 
					 | 
				
			||||||
		if (!user) user = await this.singupGitlab(userInfo, data);
 | 
					 | 
				
			||||||
		else user = await this.setGitlabTokens(user, data);
 | 
					 | 
				
			||||||
		return user;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async getGitlabUserInfo(
 | 
					 | 
				
			||||||
		req: Request,
 | 
					 | 
				
			||||||
		user: User
 | 
					 | 
				
			||||||
	): Promise<GitlabUserResponse | null> {
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			const userInfoResp = await axios.get(
 | 
					 | 
				
			||||||
				`${GITLAB_API_URL}/api/v4/user`,
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					headers: { Authorization: `Bearer ${user.gitlabAT}` }
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
			return userInfoResp.data;
 | 
					 | 
				
			||||||
		} catch (e) {
 | 
					 | 
				
			||||||
			return null;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async tryRefreshGitlabTokens(
 | 
					 | 
				
			||||||
		req: Request,
 | 
					 | 
				
			||||||
		user: User
 | 
					 | 
				
			||||||
	): Promise<User | null> {
 | 
					 | 
				
			||||||
		const params = new URLSearchParams();
 | 
					 | 
				
			||||||
		params.append('redirect_uri', this.getGitlabRedirectUrl(req));
 | 
					 | 
				
			||||||
		params.append('client_id', GITLAB_ID);
 | 
					 | 
				
			||||||
		params.append('client_secret', GITLAB_SECRET);
 | 
					 | 
				
			||||||
		params.append('refresh_token', user.gitlabRT);
 | 
					 | 
				
			||||||
		params.append('grant_type', 'refresh_token');
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			const resp = await axios.post(
 | 
					 | 
				
			||||||
				`${GITLAB_API_URL}/oauth/token?${params.toString()}`,
 | 
					 | 
				
			||||||
				{}
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
			const data: GitlabTokenResponse = resp.data;
 | 
					 | 
				
			||||||
			return this.setGitlabTokens(user, data);
 | 
					 | 
				
			||||||
		} catch (e) {
 | 
					 | 
				
			||||||
			return null;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async verifyGitlabUser(req: Request, user: User): Promise<boolean> {
 | 
					 | 
				
			||||||
		let info = await this.getGitlabUserInfo(req, user);
 | 
					 | 
				
			||||||
		if (!info) {
 | 
					 | 
				
			||||||
			user = await this.tryRefreshGitlabTokens(req, user);
 | 
					 | 
				
			||||||
			if (!user) return false;
 | 
					 | 
				
			||||||
			info = await this.getGitlabUserInfo(req, user);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return info && info.username == user.name;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async deleteUser(user: User): Promise<void> {
 | 
					 | 
				
			||||||
		await this.revokeAll(user);
 | 
					 | 
				
			||||||
		await this.fsService.delete(await user.root, true);
 | 
					 | 
				
			||||||
		await this.userRepo.remove(user);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async changePassword(
 | 
					 | 
				
			||||||
		user: User,
 | 
					 | 
				
			||||||
		oldPW: string,
 | 
					 | 
				
			||||||
		newPw: string
 | 
					 | 
				
			||||||
	): Promise<void> {
 | 
					 | 
				
			||||||
		if (!(await argon2.verify(user.password, oldPW)))
 | 
					 | 
				
			||||||
			throw new ForbiddenException('Old password is wrong');
 | 
					 | 
				
			||||||
		user.password = await argon2.hash(newPw);
 | 
					 | 
				
			||||||
		await this.revokeAll(await this.userRepo.save(user));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Injectable()
 | 
					 | 
				
			||||||
export class AuthLocalService extends PassportStrategy(LocalStrategy) {
 | 
					 | 
				
			||||||
	constructor(private authService: AuthService) {
 | 
					 | 
				
			||||||
		super();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async validate(username: string, pass: string) {
 | 
					 | 
				
			||||||
		const user = await this.authService.validateUser(username, pass);
 | 
					 | 
				
			||||||
		if (!user)
 | 
					 | 
				
			||||||
			throw new UnauthorizedException('Invalid username or password');
 | 
					 | 
				
			||||||
		return user;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Injectable()
 | 
					 | 
				
			||||||
export class AuthJwtService extends PassportStrategy(JWTStrategy) {
 | 
					 | 
				
			||||||
	constructor(private authService: AuthService) {
 | 
					 | 
				
			||||||
		super({
 | 
					 | 
				
			||||||
			jwtFromRequest: ExtractJwt.fromExtractors([
 | 
					 | 
				
			||||||
				ExtractJwt.fromAuthHeaderAsBearerToken(),
 | 
					 | 
				
			||||||
				ExtractJwt.fromBodyField('jwtToken')
 | 
					 | 
				
			||||||
			]),
 | 
					 | 
				
			||||||
			ignoreExpiration: false,
 | 
					 | 
				
			||||||
			passReqToCallback: true,
 | 
					 | 
				
			||||||
			secretOrKey: jwtSecret
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async validate(req: Request, payload: jwtPayload) {
 | 
					 | 
				
			||||||
		await this.authService.cleanupTokens();
 | 
					 | 
				
			||||||
		const token = await this.authService.getToken(payload.jti);
 | 
					 | 
				
			||||||
		if (!token)
 | 
					 | 
				
			||||||
			throw new UnauthorizedException(
 | 
					 | 
				
			||||||
				'Invalid token, please log in again'
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
		const user = await this.authService.getUser(token.ownerId);
 | 
					 | 
				
			||||||
		if (!user || user.id != payload.sub)
 | 
					 | 
				
			||||||
			throw new UnauthorizedException(
 | 
					 | 
				
			||||||
				'Invalid token, please log in again'
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
		if (
 | 
					 | 
				
			||||||
			user.isGitlabUser &&
 | 
					 | 
				
			||||||
			!(await this.authService.verifyGitlabUser(req, user))
 | 
					 | 
				
			||||||
		) {
 | 
					 | 
				
			||||||
			await this.authService.revokeAll(user);
 | 
					 | 
				
			||||||
			throw new UnauthorizedException('Invalid gitlab token');
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 | 
					 | 
				
			||||||
		// @ts-ignore
 | 
					 | 
				
			||||||
		req.token = token;
 | 
					 | 
				
			||||||
		return user;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										111
									
								
								src/services/auth/base.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/services/auth/base.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
						BadRequestException,
 | 
				
			||||||
 | 
						Injectable,
 | 
				
			||||||
 | 
						UnauthorizedException
 | 
				
			||||||
 | 
					} from '@nestjs/common';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { JWTToken, User, UserRole } from '../../entities';
 | 
				
			||||||
 | 
					import { LessThanOrEqual, Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import * as argon2 from 'argon2';
 | 
				
			||||||
 | 
					import FileSystemService from '../filesystem';
 | 
				
			||||||
 | 
					import * as jwt from 'jsonwebtoken';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const jwtSecret = 'CUM';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface jwtPayload {
 | 
				
			||||||
 | 
						sub: number;
 | 
				
			||||||
 | 
						jti: number;
 | 
				
			||||||
 | 
						exp?: number;
 | 
				
			||||||
 | 
						iat?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export default class BaseAuthService {
 | 
				
			||||||
 | 
						constructor(
 | 
				
			||||||
 | 
							@InjectRepository(User)
 | 
				
			||||||
 | 
							protected userRepo: Repository<User>,
 | 
				
			||||||
 | 
							@InjectRepository(JWTToken)
 | 
				
			||||||
 | 
							protected tokenRepo: Repository<JWTToken>,
 | 
				
			||||||
 | 
							protected fsService: FileSystemService
 | 
				
			||||||
 | 
						) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async getUser(userId: number): Promise<User | null> {
 | 
				
			||||||
 | 
							return this.userRepo.findOneBy({
 | 
				
			||||||
 | 
								id: userId
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async findUser(username: string, gitlab: boolean): Promise<User | null> {
 | 
				
			||||||
 | 
							return this.userRepo.findOneBy({
 | 
				
			||||||
 | 
								name: username,
 | 
				
			||||||
 | 
								isGitlabUser: gitlab
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async getToken(tokenId: number): Promise<JWTToken | null> {
 | 
				
			||||||
 | 
							return this.tokenRepo.findOneBy({
 | 
				
			||||||
 | 
								id: tokenId
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async validateUser(username: string, pass: string): Promise<User | null> {
 | 
				
			||||||
 | 
							const user = await this.findUser(username, false);
 | 
				
			||||||
 | 
							if (!user)
 | 
				
			||||||
 | 
								throw new UnauthorizedException('Invalid username or password');
 | 
				
			||||||
 | 
							if (!(await argon2.verify(user.password, pass)))
 | 
				
			||||||
 | 
								throw new UnauthorizedException('Invalid username or password');
 | 
				
			||||||
 | 
							if (user.role == UserRole.DISABLED)
 | 
				
			||||||
 | 
								throw new UnauthorizedException('Account is disabled');
 | 
				
			||||||
 | 
							return user;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async cleanupTokens(): Promise<void> {
 | 
				
			||||||
 | 
							await this.tokenRepo.delete({
 | 
				
			||||||
 | 
								exp: LessThanOrEqual(Math.floor(Date.now() / 1000))
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async login(req: Request, user: User) {
 | 
				
			||||||
 | 
							const token = new JWTToken();
 | 
				
			||||||
 | 
							token.ownerId = user.id;
 | 
				
			||||||
 | 
							const db_token = await this.tokenRepo.save(token);
 | 
				
			||||||
 | 
							const payload: jwtPayload = {
 | 
				
			||||||
 | 
								sub: user.id,
 | 
				
			||||||
 | 
								jti: db_token.id
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
							const jwtToken = jwt.sign(payload, jwtSecret, {
 | 
				
			||||||
 | 
								mutatePayload: true,
 | 
				
			||||||
 | 
								expiresIn: '1d'
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							db_token.exp = payload.exp;
 | 
				
			||||||
 | 
							await this.tokenRepo.save(db_token);
 | 
				
			||||||
 | 
							return jwtToken;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async singupInternal(user: User): Promise<User> {
 | 
				
			||||||
 | 
							const root = await this.fsService.generateRoot(user);
 | 
				
			||||||
 | 
							user.rootId = root.id;
 | 
				
			||||||
 | 
							return this.userRepo.save(user);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async signup(username: string, password: string) {
 | 
				
			||||||
 | 
							if (await this.findUser(username, false))
 | 
				
			||||||
 | 
								throw new BadRequestException('User already exists');
 | 
				
			||||||
 | 
							const user = new User();
 | 
				
			||||||
 | 
							user.name = username;
 | 
				
			||||||
 | 
							user.password = await argon2.hash(password);
 | 
				
			||||||
 | 
							await this.singupInternal(await this.userRepo.save(user));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async revoke(token: JWTToken) {
 | 
				
			||||||
 | 
							await this.tokenRepo.delete({
 | 
				
			||||||
 | 
								id: token.id
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async revokeAll(user: User) {
 | 
				
			||||||
 | 
							await this.tokenRepo.delete({
 | 
				
			||||||
 | 
								ownerId: user.id
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										159
									
								
								src/services/auth/gitlab.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/services/auth/gitlab.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,159 @@
 | 
				
			|||||||
 | 
					import { User, UserRole } from '../../entities';
 | 
				
			||||||
 | 
					import { FastifyRequest } from 'fastify';
 | 
				
			||||||
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					import * as argon2 from 'argon2';
 | 
				
			||||||
 | 
					import { ForbiddenException, UnauthorizedException } from '@nestjs/common';
 | 
				
			||||||
 | 
					import TfaAuthService from './tfa';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const GITLAB_ID =
 | 
				
			||||||
 | 
						'98bcbad78cb1f880d1d1de62291d70a791251a7bea077bfe7df111ef3c115760';
 | 
				
			||||||
 | 
					const GITLAB_SECRET =
 | 
				
			||||||
 | 
						'7ee01d2b204aff3a05f9d028f004d169b6d381ec873e195f314b3935fa150959';
 | 
				
			||||||
 | 
					const GITLAB_URL = 'https://gitlab.mattv.de';
 | 
				
			||||||
 | 
					const GITLAB_API_URL = 'https://ssh.gitlab.mattv.de';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface GitlabTokenResponse {
 | 
				
			||||||
 | 
						access_token: string;
 | 
				
			||||||
 | 
						token_type: string;
 | 
				
			||||||
 | 
						expires_in: number;
 | 
				
			||||||
 | 
						refresh_token: string;
 | 
				
			||||||
 | 
						created_at: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface GitlabUserResponse {
 | 
				
			||||||
 | 
						username: string;
 | 
				
			||||||
 | 
						is_admin?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class GitlabAuthService extends TfaAuthService {
 | 
				
			||||||
 | 
						async login(req: Request, user: User) {
 | 
				
			||||||
 | 
							if (user.isGitlabUser && !(await this.verifyGitlabUser(req, user))) {
 | 
				
			||||||
 | 
								await this.revokeAll(user);
 | 
				
			||||||
 | 
								throw new UnauthorizedException('Invalid gitlab token');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return super.login(req, user);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async singupGitlab(
 | 
				
			||||||
 | 
							info: GitlabUserResponse,
 | 
				
			||||||
 | 
							data: GitlabTokenResponse
 | 
				
			||||||
 | 
						): Promise<User> {
 | 
				
			||||||
 | 
							const user = new User();
 | 
				
			||||||
 | 
							user.name = info.username;
 | 
				
			||||||
 | 
							user.password = '';
 | 
				
			||||||
 | 
							user.isGitlabUser = true;
 | 
				
			||||||
 | 
							user.role = info.is_admin ? UserRole.ADMIN : UserRole.DISABLED;
 | 
				
			||||||
 | 
							return this.singupInternal(await this.setGitlabTokens(user, data));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async setGitlabTokens(
 | 
				
			||||||
 | 
							user: User,
 | 
				
			||||||
 | 
							data: GitlabTokenResponse
 | 
				
			||||||
 | 
						): Promise<User> {
 | 
				
			||||||
 | 
							user.gitlabAT = data.access_token;
 | 
				
			||||||
 | 
							user.gitlabRT = data.refresh_token;
 | 
				
			||||||
 | 
							return this.userRepo.save(user);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						getGitlabRedirectUrl(req: Request): string {
 | 
				
			||||||
 | 
							const _req = req as unknown as FastifyRequest;
 | 
				
			||||||
 | 
							return `${_req.protocol}://${_req.hostname}/api/auth/gitlab_callback`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						getGitlabAuthUrl(req: Request): string {
 | 
				
			||||||
 | 
							const params = new URLSearchParams();
 | 
				
			||||||
 | 
							params.append('redirect_uri', this.getGitlabRedirectUrl(req));
 | 
				
			||||||
 | 
							params.append('response_type', 'code');
 | 
				
			||||||
 | 
							params.append('scope', 'read_user');
 | 
				
			||||||
 | 
							params.append('client_id', GITLAB_ID);
 | 
				
			||||||
 | 
							return `${GITLAB_URL}/oauth/authorize?${params.toString()}`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async getGitlabUserFromCode(req: Request, code: string): Promise<User> {
 | 
				
			||||||
 | 
							const params = new URLSearchParams();
 | 
				
			||||||
 | 
							params.append('redirect_uri', this.getGitlabRedirectUrl(req));
 | 
				
			||||||
 | 
							params.append('client_id', GITLAB_ID);
 | 
				
			||||||
 | 
							params.append('client_secret', GITLAB_SECRET);
 | 
				
			||||||
 | 
							params.append('code', code);
 | 
				
			||||||
 | 
							params.append('grant_type', 'authorization_code');
 | 
				
			||||||
 | 
							const resp = await axios.post(
 | 
				
			||||||
 | 
								`${GITLAB_API_URL}/oauth/token?${params.toString()}`,
 | 
				
			||||||
 | 
								{}
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							const data: GitlabTokenResponse = resp.data;
 | 
				
			||||||
 | 
							const userInfoResp = await axios.get(`${GITLAB_API_URL}/api/v4/user`, {
 | 
				
			||||||
 | 
								headers: { Authorization: `Bearer ${data.access_token}` }
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							const userInfo: GitlabUserResponse = userInfoResp.data;
 | 
				
			||||||
 | 
							let user = await this.findUser(userInfo.username, true);
 | 
				
			||||||
 | 
							if (!user) user = await this.singupGitlab(userInfo, data);
 | 
				
			||||||
 | 
							else user = await this.setGitlabTokens(user, data);
 | 
				
			||||||
 | 
							return user;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async getGitlabUserInfo(
 | 
				
			||||||
 | 
							req: Request,
 | 
				
			||||||
 | 
							user: User
 | 
				
			||||||
 | 
						): Promise<GitlabUserResponse | null> {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const userInfoResp = await axios.get(
 | 
				
			||||||
 | 
									`${GITLAB_API_URL}/api/v4/user`,
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										headers: { Authorization: `Bearer ${user.gitlabAT}` }
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								return userInfoResp.data;
 | 
				
			||||||
 | 
							} catch (e) {
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async tryRefreshGitlabTokens(
 | 
				
			||||||
 | 
							req: Request,
 | 
				
			||||||
 | 
							user: User
 | 
				
			||||||
 | 
						): Promise<User | null> {
 | 
				
			||||||
 | 
							const params = new URLSearchParams();
 | 
				
			||||||
 | 
							params.append('redirect_uri', this.getGitlabRedirectUrl(req));
 | 
				
			||||||
 | 
							params.append('client_id', GITLAB_ID);
 | 
				
			||||||
 | 
							params.append('client_secret', GITLAB_SECRET);
 | 
				
			||||||
 | 
							params.append('refresh_token', user.gitlabRT);
 | 
				
			||||||
 | 
							params.append('grant_type', 'refresh_token');
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const resp = await axios.post(
 | 
				
			||||||
 | 
									`${GITLAB_API_URL}/oauth/token?${params.toString()}`,
 | 
				
			||||||
 | 
									{}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								const data: GitlabTokenResponse = resp.data;
 | 
				
			||||||
 | 
								return this.setGitlabTokens(user, data);
 | 
				
			||||||
 | 
							} catch (e) {
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async verifyGitlabUser(req: Request, user: User): Promise<boolean> {
 | 
				
			||||||
 | 
							let info = await this.getGitlabUserInfo(req, user);
 | 
				
			||||||
 | 
							if (!info) {
 | 
				
			||||||
 | 
								user = await this.tryRefreshGitlabTokens(req, user);
 | 
				
			||||||
 | 
								if (!user) return false;
 | 
				
			||||||
 | 
								info = await this.getGitlabUserInfo(req, user);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return info && info.username == user.name;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async deleteUser(user: User): Promise<void> {
 | 
				
			||||||
 | 
							await this.revokeAll(user);
 | 
				
			||||||
 | 
							await this.fsService.delete(await user.root, true);
 | 
				
			||||||
 | 
							await this.userRepo.remove(user);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async changePassword(
 | 
				
			||||||
 | 
							user: User,
 | 
				
			||||||
 | 
							oldPW: string,
 | 
				
			||||||
 | 
							newPw: string
 | 
				
			||||||
 | 
						): Promise<void> {
 | 
				
			||||||
 | 
							if (!(await argon2.verify(user.password, oldPW)))
 | 
				
			||||||
 | 
								throw new ForbiddenException('Old password is wrong');
 | 
				
			||||||
 | 
							user.password = await argon2.hash(newPw);
 | 
				
			||||||
 | 
							await this.revokeAll(await this.userRepo.save(user));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								src/services/auth/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/services/auth/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					export { default as AuthService } from './gitlab';
 | 
				
			||||||
 | 
					export * from './strategies';
 | 
				
			||||||
							
								
								
									
										60
									
								
								src/services/auth/strategies.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/services/auth/strategies.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					import { Injectable, UnauthorizedException } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { PassportStrategy } from '@nestjs/passport';
 | 
				
			||||||
 | 
					import { Strategy as LocalStrategy } from 'passport-local';
 | 
				
			||||||
 | 
					import { ExtractJwt, Strategy as JWTStrategy } from 'passport-jwt';
 | 
				
			||||||
 | 
					import AuthService from './gitlab';
 | 
				
			||||||
 | 
					import { jwtPayload, jwtSecret } from './base';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class AuthLocalService extends PassportStrategy(LocalStrategy) {
 | 
				
			||||||
 | 
						constructor(private authService: AuthService) {
 | 
				
			||||||
 | 
							super();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async validate(username: string, pass: string) {
 | 
				
			||||||
 | 
							const user = await this.authService.validateUser(username, pass);
 | 
				
			||||||
 | 
							if (!user)
 | 
				
			||||||
 | 
								throw new UnauthorizedException('Invalid username or password');
 | 
				
			||||||
 | 
							return user;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class AuthJwtService extends PassportStrategy(JWTStrategy) {
 | 
				
			||||||
 | 
						constructor(private authService: AuthService) {
 | 
				
			||||||
 | 
							super({
 | 
				
			||||||
 | 
								jwtFromRequest: ExtractJwt.fromExtractors([
 | 
				
			||||||
 | 
									ExtractJwt.fromAuthHeaderAsBearerToken(),
 | 
				
			||||||
 | 
									ExtractJwt.fromBodyField('jwtToken')
 | 
				
			||||||
 | 
								]),
 | 
				
			||||||
 | 
								ignoreExpiration: false,
 | 
				
			||||||
 | 
								passReqToCallback: true,
 | 
				
			||||||
 | 
								secretOrKey: jwtSecret
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async validate(req: Request, payload: jwtPayload) {
 | 
				
			||||||
 | 
							await this.authService.cleanupTokens();
 | 
				
			||||||
 | 
							const token = await this.authService.getToken(payload.jti);
 | 
				
			||||||
 | 
							if (!token)
 | 
				
			||||||
 | 
								throw new UnauthorizedException(
 | 
				
			||||||
 | 
									'Invalid token, please log in again'
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							const user = await this.authService.getUser(token.ownerId);
 | 
				
			||||||
 | 
							if (!user || user.id != payload.sub)
 | 
				
			||||||
 | 
								throw new UnauthorizedException(
 | 
				
			||||||
 | 
									'Invalid token, please log in again'
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
								user.isGitlabUser &&
 | 
				
			||||||
 | 
								!(await this.authService.verifyGitlabUser(req, user))
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								await this.authService.revokeAll(user);
 | 
				
			||||||
 | 
								throw new UnauthorizedException('Invalid gitlab token');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 | 
				
			||||||
 | 
							// @ts-ignore
 | 
				
			||||||
 | 
							req.token = token;
 | 
				
			||||||
 | 
							return user;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										74
									
								
								src/services/auth/tfa.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/services/auth/tfa.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					import { tfaTypes, User } from '../../entities';
 | 
				
			||||||
 | 
					import { BadRequestException } from '@nestjs/common';
 | 
				
			||||||
 | 
					import BaseAuthService from './base';
 | 
				
			||||||
 | 
					import { randomBytes } from 'crypto';
 | 
				
			||||||
 | 
					import * as notp from 'notp';
 | 
				
			||||||
 | 
					import { createTransport } from 'nodemailer';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mailAccount = createTransport({
 | 
				
			||||||
 | 
						host: 'mail.mattv.de',
 | 
				
			||||||
 | 
						port: 587,
 | 
				
			||||||
 | 
						secure: false,
 | 
				
			||||||
 | 
						auth: {
 | 
				
			||||||
 | 
							user: 'no-reply@mattv.de',
 | 
				
			||||||
 | 
							pass: 'noreplyLONGPASS123'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class TfaAuthService extends BaseAuthService {
 | 
				
			||||||
 | 
						generateTfaSecret(): string {
 | 
				
			||||||
 | 
							const set =
 | 
				
			||||||
 | 
								'0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz!@#$%^&*()<>?/[]{},.:;';
 | 
				
			||||||
 | 
							return randomBytes(32)
 | 
				
			||||||
 | 
								.map((b) =>
 | 
				
			||||||
 | 
									set.charCodeAt(Math.floor((b / 255.0) * (set.length - 1)))
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								.toString();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requiresTfa(user: User): boolean {
 | 
				
			||||||
 | 
							return user.tfaType != tfaTypes.NONE;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async verifyTfa(
 | 
				
			||||||
 | 
							user: User,
 | 
				
			||||||
 | 
							token: string,
 | 
				
			||||||
 | 
							type?: tfaTypes
 | 
				
			||||||
 | 
						): Promise<boolean> {
 | 
				
			||||||
 | 
							if (!type) type = user.tfaType;
 | 
				
			||||||
 | 
							const delta = notp.totp.verify(token, user.tfaSecret, {
 | 
				
			||||||
 | 
								window: 10
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							return (
 | 
				
			||||||
 | 
								delta &&
 | 
				
			||||||
 | 
								(type == tfaTypes.EMAIL ? delta.delta <= 0 : delta.delta == 0)
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async sendTfaMail(user: User) {
 | 
				
			||||||
 | 
							await mailAccount.sendMail({
 | 
				
			||||||
 | 
								from: 'fileserver@mattv.de',
 | 
				
			||||||
 | 
								to: user.name,
 | 
				
			||||||
 | 
								subject: 'Fileserver - EMail 2fa code',
 | 
				
			||||||
 | 
								text: `Your code is: ${notp.totp.gen(
 | 
				
			||||||
 | 
									user.tfaSecret
 | 
				
			||||||
 | 
								)}\nIt is valid for 5 Minutes`
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async setupTfa(user: User): Promise<string> {
 | 
				
			||||||
 | 
							if (user.tfaType != tfaTypes.NONE)
 | 
				
			||||||
 | 
								throw new BadRequestException(
 | 
				
			||||||
 | 
									'2 Factor authentication is already setup'
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							const secret = this.generateTfaSecret();
 | 
				
			||||||
 | 
							user.tfaSecret = secret;
 | 
				
			||||||
 | 
							await this.userRepo.save(user);
 | 
				
			||||||
 | 
							return secret;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async setTfaType(user: User, type: tfaTypes) {
 | 
				
			||||||
 | 
							user.tfaType = type;
 | 
				
			||||||
 | 
							await this.userRepo.save(user);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user