Added totp/mail otp, split up dto and api into multiple files
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -76,7 +76,6 @@ export class User {
|
||||
})
|
||||
tfaType: tfaTypes;
|
||||
|
||||
// base32 string
|
||||
@Column({ nullable: true })
|
||||
tfaSecret: string;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user