Split up Auth Service into multiple files
This commit is contained in:
parent
762c1c84c9
commit
6113478acd
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user