Added totp/mail otp, split up dto and api into multiple files

This commit is contained in:
2022-08-24 16:15:33 +02:00
parent af1df3e508
commit cd0d25ba4f
30 changed files with 535 additions and 379 deletions

View File

@@ -2,20 +2,21 @@ import {
BadRequestException,
Body,
Controller,
Get,
HttpCode,
ParseBoolPipe,
Post,
Request,
UnauthorizedException,
UseGuards
} from '@nestjs/common';
import { AuthService } from '../services/auth';
import { AuthGuard } from '@nestjs/passport';
import { Public } from '../authguards';
import {
ErrorResponse,
LoginResponse,
RefreshResponse,
SignupResponse
} from 'dto';
import { Responses } from 'dto';
import { tfaTypes } from '../entities';
import { toDataURL } from 'qrcode';
import * as base32 from 'thirty-two';
@Controller('api/auth')
export default class AuthController {
@@ -25,19 +26,90 @@ export default class AuthController {
@UseGuards(AuthGuard('local'))
@Post('login')
@HttpCode(200)
async login(@Request() req): Promise<LoginResponse> {
async login(
@Request() req,
@Body('otp') otp?: string
): Promise<
Responses.Auth.LoginResponse | Responses.Auth.TfaRequiredResponse
> {
if (this.authService.requiresTfa(req.user)) {
if (!otp) {
if (req.user.tfaType == tfaTypes.EMAIL)
await this.authService.sendTfaMail(req.user);
return {
statusCode: 200
};
}
if (!(await this.authService.verifyTfa(req.user, otp)))
throw new UnauthorizedException('Incorrect 2fa');
}
return {
statusCode: 200,
jwt: await this.authService.login(req.user)
};
}
async tfa(
req,
code: string,
type: tfaTypes
): Promise<Responses.Auth.TfaCompletedResponse> {
if (!(await this.authService.verifyTfa(req.user, code, type))) {
throw new UnauthorizedException('Incorrect 2fa');
}
await this.authService.setTfaType(req.user, type);
await this.authService.revokeAll(req.user);
return {
statusCode: 200
};
}
@Post('2fa/complete/mail')
async tfaMail(
@Request() req,
@Body('code') code: string
): Promise<Responses.Auth.TfaCompletedResponse> {
return await this.tfa(req, code, tfaTypes.EMAIL);
}
@Post('2fa/complete/totp')
async tfaTotp(
@Request() req,
@Body('code') code: string
): Promise<Responses.Auth.TfaCompletedResponse> {
return await this.tfa(req, code, tfaTypes.TOTP);
}
@Get('2fa/setup')
async setupTotp(
@Request() req,
@Body('mail', ParseBoolPipe) mail: boolean
): Promise<
| Responses.Auth.RequestTotpTfaResponse
| Responses.Auth.RequestEmailTfaResponse
> {
const secret = await this.authService.setupTfa(req.user);
if (mail)
return {
statusCode: 200
};
return {
statusCode: 200,
qrCode: await toDataURL(
`otpauth://totp/MFileserver:${req.user.name}?secret=${base32
.encode(secret)
.toString()}&issuer=MFileserver`
),
secret
};
}
@Public()
@Post('signup')
async signup(
@Body('username') username,
@Body('password') password
): Promise<SignupResponse | ErrorResponse> {
): Promise<Responses.Auth.SignupResponse> {
if ((await this.authService.findUser(username)) != null)
throw new BadRequestException('Username already taken');
await this.authService.signup(username, password);
@@ -47,7 +119,7 @@ export default class AuthController {
}
@Post('refresh')
async refresh(@Request() req): Promise<RefreshResponse | ErrorResponse> {
async refresh(@Request() req): Promise<Responses.Auth.RefreshResponse> {
const token = await this.authService.login(req.user);
await this.authService.revoke(req.token);
return {

View File

@@ -8,15 +8,7 @@ import {
Request,
StreamableFile
} from '@nestjs/common';
import {
CreateFileResponse,
CreateFolderResponse,
DeleteResponse,
GetNodeResponse,
GetPathResponse,
GetRootResponse,
UploadFileResponse
} from 'dto/index';
import { Responses } from 'dto';
import FileSystemService from '../services/filesystem';
import { UserRole } from '../entities';
import { Role } from '../authguards';
@@ -27,7 +19,7 @@ export default class FileSystemController {
@Get('root')
@Role(UserRole.USER)
async getRoot(@Request() req): Promise<GetRootResponse> {
async getRoot(@Request() req): Promise<Responses.FS.GetRootResponse> {
return {
statusCode: 200,
rootId: req.user.rootId
@@ -39,9 +31,9 @@ export default class FileSystemController {
async getNode(
@Request() req,
@Param('node', ParseIntPipe) nodeId
): Promise<GetNodeResponse> {
): Promise<Responses.FS.GetNodeResponse> {
const node = await this.fsService.getNodeAndValidate(nodeId, req.user);
const data: GetNodeResponse = {
const data: Responses.FS.GetNodeResponse = {
id: nodeId,
statusCode: 200,
name: node.name,
@@ -61,7 +53,7 @@ export default class FileSystemController {
async getPath(
@Request() req,
@Param('node', ParseIntPipe) nodeId
): Promise<GetPathResponse> {
): Promise<Responses.FS.GetPathResponse> {
return {
statusCode: 200,
path: await this.fsService.generatePath(
@@ -76,7 +68,7 @@ export default class FileSystemController {
@Request() req,
@Body('parent', ParseIntPipe) parent,
@Body('name') name
): Promise<CreateFolderResponse> {
): Promise<Responses.FS.CreateFolderResponse> {
const newChild = await this.fsService.create(
await this.fsService.getNodeAndValidate(parent, req.user),
name,
@@ -95,7 +87,7 @@ export default class FileSystemController {
@Request() req,
@Body('parent', ParseIntPipe) parent,
@Body('name') name
): Promise<CreateFileResponse> {
): Promise<Responses.FS.CreateFileResponse> {
const newChild = await this.fsService.create(
await this.fsService.getNodeAndValidate(parent, req.user),
name,
@@ -113,7 +105,7 @@ export default class FileSystemController {
async delete(
@Request() req,
@Body('node', ParseIntPipe) node_id
): Promise<DeleteResponse> {
): Promise<Responses.FS.DeleteResponse> {
await this.fsService.delete(
await this.fsService.getNodeAndValidate(node_id, req.user)
);
@@ -125,7 +117,7 @@ export default class FileSystemController {
async upload(
@Request() req,
@Param('node', ParseIntPipe) nodeId
): Promise<UploadFileResponse> {
): Promise<Responses.FS.UploadFileResponse> {
await this.fsService.uploadFile(await req.file(), nodeId, req.user);
return { statusCode: 200 };
}

View File

@@ -76,7 +76,6 @@ export class User {
})
tfaType: tfaTypes;
// base32 string
@Column({ nullable: true })
tfaSecret: string;
}

View File

@@ -4,17 +4,27 @@ import {
UnauthorizedException
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { JWTToken, User, UserRole } from '../entities';
import { Repository, LessThanOrEqual } from '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';
const jwtSecret = 'CUM';
const mailAccount = createTransport({
host: 'smtp.web.de',
port: 587,
secure: false,
auth: require('M:/projects/file_server/dist/auth.json')
});
interface jwtPayload {
sub: number;
jti: number;
@@ -32,6 +42,16 @@ export class AuthService {
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
@@ -67,6 +87,52 @@ export class AuthService {
});
}
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: 'matthiasveigel@web.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(user: User) {
const token = new JWTToken();
token.ownerId = user.id;
@@ -101,6 +167,12 @@ export class AuthService {
id: token.id
});
}
async revokeAll(user: User) {
await this.tokenRepo.delete({
ownerId: user.id
});
}
}
@Injectable()