Added gitlab authentication
This commit is contained in:
		@@ -6,6 +6,8 @@ import {
 | 
			
		||||
	HttpCode,
 | 
			
		||||
	ParseBoolPipe,
 | 
			
		||||
	Post,
 | 
			
		||||
	Query,
 | 
			
		||||
	Redirect,
 | 
			
		||||
	Request,
 | 
			
		||||
	UnauthorizedException,
 | 
			
		||||
	UseGuards
 | 
			
		||||
@@ -45,7 +47,7 @@ export default class AuthController {
 | 
			
		||||
		}
 | 
			
		||||
		return {
 | 
			
		||||
			statusCode: 200,
 | 
			
		||||
			jwt: await this.authService.login(req.user)
 | 
			
		||||
			jwt: await this.authService.login(req, req.user)
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -110,7 +112,7 @@ export default class AuthController {
 | 
			
		||||
		@Body('username') username,
 | 
			
		||||
		@Body('password') password
 | 
			
		||||
	): Promise<Responses.Auth.SignupResponse> {
 | 
			
		||||
		if ((await this.authService.findUser(username)) != null)
 | 
			
		||||
		if ((await this.authService.findUser(username, false)) != null)
 | 
			
		||||
			throw new BadRequestException('Username already taken');
 | 
			
		||||
		await this.authService.signup(username, password);
 | 
			
		||||
		return {
 | 
			
		||||
@@ -120,11 +122,31 @@ export default class AuthController {
 | 
			
		||||
 | 
			
		||||
	@Post('refresh')
 | 
			
		||||
	async refresh(@Request() req): Promise<Responses.Auth.RefreshResponse> {
 | 
			
		||||
		const token = await this.authService.login(req.user);
 | 
			
		||||
		const token = await this.authService.login(req, req.user);
 | 
			
		||||
		await this.authService.revoke(req.token);
 | 
			
		||||
		return {
 | 
			
		||||
			statusCode: 200,
 | 
			
		||||
			jwt: token
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Public()
 | 
			
		||||
	@Redirect()
 | 
			
		||||
	@Get('gitlab')
 | 
			
		||||
	async gitlab(@Request() req) {
 | 
			
		||||
		return {
 | 
			
		||||
			url: this.authService.getGitlabAuthUrl(req)
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Public()
 | 
			
		||||
	@Redirect()
 | 
			
		||||
	@Get('gitlab_callback')
 | 
			
		||||
	async gitlabCallback(@Request() req, @Query('code') code) {
 | 
			
		||||
		const user = await this.authService.getGitlabUserFromCode(req, code);
 | 
			
		||||
		const token = await this.authService.login(req, user);
 | 
			
		||||
		return {
 | 
			
		||||
			url: `/set_token?token=${token}`
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,8 @@ export class INode {
 | 
			
		||||
export class User {
 | 
			
		||||
	@PrimaryGeneratedColumn()
 | 
			
		||||
	id: number;
 | 
			
		||||
	@Column({ default: false })
 | 
			
		||||
	isGitlabUser: boolean;
 | 
			
		||||
	@Column()
 | 
			
		||||
	name: string;
 | 
			
		||||
	@Column()
 | 
			
		||||
@@ -78,6 +80,11 @@ export class User {
 | 
			
		||||
 | 
			
		||||
	@Column({ nullable: true })
 | 
			
		||||
	tfaSecret: string;
 | 
			
		||||
 | 
			
		||||
	@Column({ nullable: true })
 | 
			
		||||
	gitlabAT: string;
 | 
			
		||||
	@Column({ nullable: true })
 | 
			
		||||
	gitlabRT: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Entity()
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,15 @@ 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';
 | 
			
		||||
 | 
			
		||||
@@ -35,6 +44,19 @@ interface jwtPayload {
 | 
			
		||||
	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(
 | 
			
		||||
@@ -61,9 +83,10 @@ export class AuthService {
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async findUser(username: string): Promise<User | null> {
 | 
			
		||||
	async findUser(username: string, gitlab: boolean): Promise<User | null> {
 | 
			
		||||
		return this.userRepo.findOneBy({
 | 
			
		||||
			name: username
 | 
			
		||||
			name: username,
 | 
			
		||||
			isGitlabUser: gitlab
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -74,7 +97,7 @@ export class AuthService {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async validateUser(username: string, pass: string): Promise<User | null> {
 | 
			
		||||
		const user = await this.findUser(username);
 | 
			
		||||
		const user = await this.findUser(username, false);
 | 
			
		||||
		if (!user)
 | 
			
		||||
			throw new UnauthorizedException('Invalid username or password');
 | 
			
		||||
		if (!(await argon2.verify(user.password, pass)))
 | 
			
		||||
@@ -136,7 +159,11 @@ export class AuthService {
 | 
			
		||||
		await this.userRepo.save(user);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async login(user: 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);
 | 
			
		||||
@@ -153,16 +180,31 @@ export class AuthService {
 | 
			
		||||
		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))
 | 
			
		||||
		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);
 | 
			
		||||
		const dbUser = await this.userRepo.save(user);
 | 
			
		||||
		const root = await this.fsService.generateRoot(dbUser);
 | 
			
		||||
		dbUser.rootId = root.id;
 | 
			
		||||
		await this.userRepo.save(dbUser);
 | 
			
		||||
		await this.singupInternal(await this.userRepo.save(user));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async revoke(token: JWTToken) {
 | 
			
		||||
@@ -176,6 +218,87 @@ export class AuthService {
 | 
			
		||||
			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 isGitlabATValid(user: User): Promise<boolean> {
 | 
			
		||||
		try {
 | 
			
		||||
			await axios.get(`${GITLAB_API_URL}/oauth/token/info`, {
 | 
			
		||||
				headers: { Authorization: `Bearer ${user.gitlabAT}` }
 | 
			
		||||
			});
 | 
			
		||||
			return true;
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async tryRefreshGitlabTokens(req: Request, user: User): Promise<boolean> {
 | 
			
		||||
		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;
 | 
			
		||||
			await this.setGitlabTokens(user, data);
 | 
			
		||||
			return true;
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async verifyGitlabUser(req: Request, user: User): Promise<boolean> {
 | 
			
		||||
		if (await this.isGitlabATValid(user)) return true;
 | 
			
		||||
		return await this.tryRefreshGitlabTokens(req, user);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
@@ -218,6 +341,13 @@ export class AuthJwtService extends PassportStrategy(JWTStrategy) {
 | 
			
		||||
			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;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user