Merge branch 'gitlab-auth' into 'main'
Gitlab authentication See merge request root/fileserver!3
This commit is contained in:
		@@ -1,11 +1,11 @@
 | 
				
			|||||||
<script setup async lang="ts">
 | 
					<script setup async lang="ts">
 | 
				
			||||||
import { provide, ref } from 'vue';
 | 
					import { provide, ref } from 'vue';
 | 
				
			||||||
import { useRouter } from 'vue-router';
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
import { Auth, TokenInjectType, isErrorResponse } from '@/api';
 | 
					import { TokenInjectType } from '@/api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = useRouter();
 | 
					const router = useRouter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jwt = ref<string | null>(null);
 | 
					const jwt = ref<string | null>(localStorage.getItem('token'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setToken(token: string) {
 | 
					function setToken(token: string) {
 | 
				
			||||||
	jwt.value = token;
 | 
						jwt.value = token;
 | 
				
			||||||
@@ -18,14 +18,6 @@ function logout() {
 | 
				
			|||||||
	router.push({ name: 'login' });
 | 
						router.push({ name: 'login' });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jwt.value = localStorage.getItem('token');
 | 
					 | 
				
			||||||
if (jwt.value == null) await router.push({ name: 'login' });
 | 
					 | 
				
			||||||
else {
 | 
					 | 
				
			||||||
	const new_token = await Auth.refresh_token(jwt.value ?? '');
 | 
					 | 
				
			||||||
	if (isErrorResponse(new_token)) logout();
 | 
					 | 
				
			||||||
	else setToken(new_token.jwt);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
provide<TokenInjectType>('jwt', {
 | 
					provide<TokenInjectType>('jwt', {
 | 
				
			||||||
	jwt,
 | 
						jwt,
 | 
				
			||||||
	setToken,
 | 
						setToken,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,12 @@ import SignupView from '@/views/SignupView.vue';
 | 
				
			|||||||
import HomeView from '@/views/HomeView.vue';
 | 
					import HomeView from '@/views/HomeView.vue';
 | 
				
			||||||
import AboutView from '@/views/AboutView.vue';
 | 
					import AboutView from '@/views/AboutView.vue';
 | 
				
			||||||
import FSView from '@/views/FSView.vue';
 | 
					import FSView from '@/views/FSView.vue';
 | 
				
			||||||
 | 
					import SetTokenView from '@/views/SetTokenView.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const routes: Array<RouteRecordRaw> = [
 | 
					const routes: Array<RouteRecordRaw> = [
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		path: '/',
 | 
							path: '/',
 | 
				
			||||||
 | 
							name: 'home',
 | 
				
			||||||
		component: HomeView
 | 
							component: HomeView
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
@@ -25,6 +27,10 @@ const routes: Array<RouteRecordRaw> = [
 | 
				
			|||||||
		name: 'signup',
 | 
							name: 'signup',
 | 
				
			||||||
		component: SignupView
 | 
							component: SignupView
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							path: '/set_token',
 | 
				
			||||||
 | 
							component: SetTokenView
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		path: '/fs/:node_id',
 | 
							path: '/fs/:node_id',
 | 
				
			||||||
		name: 'fs',
 | 
							name: 'fs',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,6 +54,7 @@ async function login() {
 | 
				
			|||||||
		<input type="text" placeholder="Code" v-model="otp" />
 | 
							<input type="text" placeholder="Code" v-model="otp" />
 | 
				
			||||||
	</template>
 | 
						</template>
 | 
				
			||||||
	<button @click="login()">Login</button>
 | 
						<button @click="login()">Login</button>
 | 
				
			||||||
 | 
						<a href="/api/auth/gitlab">Login with gitlab</a>
 | 
				
			||||||
	<router-link to="signup">Signup instead?</router-link>
 | 
						<router-link to="signup">Signup instead?</router-link>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										19
									
								
								frontend/src/views/SetTokenView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/views/SetTokenView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { inject } from 'vue';
 | 
				
			||||||
 | 
					import { TokenInjectType } from '@/api';
 | 
				
			||||||
 | 
					import { useRoute, useRouter } from 'vue-router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = useRouter();
 | 
				
			||||||
 | 
					const route = useRoute();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if ('token' in route.query) jwt.setToken(route.query['token'] as string);
 | 
				
			||||||
 | 
					router.replace({ path: '/' });
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<router-link to="home">Click here to go home</router-link>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped></style>
 | 
				
			||||||
@@ -31,6 +31,7 @@
 | 
				
			|||||||
		"@nestjs/serve-static": "^3.0.0",
 | 
							"@nestjs/serve-static": "^3.0.0",
 | 
				
			||||||
		"@nestjs/typeorm": "^9.0.0",
 | 
							"@nestjs/typeorm": "^9.0.0",
 | 
				
			||||||
		"argon2": "^0.28.7",
 | 
							"argon2": "^0.28.7",
 | 
				
			||||||
 | 
							"axios": "^0.27.2",
 | 
				
			||||||
		"jsonwebtoken": "^8.5.1",
 | 
							"jsonwebtoken": "^8.5.1",
 | 
				
			||||||
		"nodemailer": "^6.7.8",
 | 
							"nodemailer": "^6.7.8",
 | 
				
			||||||
		"notp": "^2.0.3",
 | 
							"notp": "^2.0.3",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,8 @@ import {
 | 
				
			|||||||
	HttpCode,
 | 
						HttpCode,
 | 
				
			||||||
	ParseBoolPipe,
 | 
						ParseBoolPipe,
 | 
				
			||||||
	Post,
 | 
						Post,
 | 
				
			||||||
 | 
						Query,
 | 
				
			||||||
 | 
						Redirect,
 | 
				
			||||||
	Request,
 | 
						Request,
 | 
				
			||||||
	UnauthorizedException,
 | 
						UnauthorizedException,
 | 
				
			||||||
	UseGuards
 | 
						UseGuards
 | 
				
			||||||
@@ -45,7 +47,7 @@ export default class AuthController {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			statusCode: 200,
 | 
								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('username') username,
 | 
				
			||||||
		@Body('password') password
 | 
							@Body('password') password
 | 
				
			||||||
	): Promise<Responses.Auth.SignupResponse> {
 | 
						): 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');
 | 
								throw new BadRequestException('Username already taken');
 | 
				
			||||||
		await this.authService.signup(username, password);
 | 
							await this.authService.signup(username, password);
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
@@ -120,11 +122,31 @@ export default class AuthController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	@Post('refresh')
 | 
						@Post('refresh')
 | 
				
			||||||
	async refresh(@Request() req): Promise<Responses.Auth.RefreshResponse> {
 | 
						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);
 | 
							await this.authService.revoke(req.token);
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			statusCode: 200,
 | 
								statusCode: 200,
 | 
				
			||||||
			jwt: token
 | 
								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 {
 | 
					export class User {
 | 
				
			||||||
	@PrimaryGeneratedColumn()
 | 
						@PrimaryGeneratedColumn()
 | 
				
			||||||
	id: number;
 | 
						id: number;
 | 
				
			||||||
 | 
						@Column({ default: false })
 | 
				
			||||||
 | 
						isGitlabUser: boolean;
 | 
				
			||||||
	@Column()
 | 
						@Column()
 | 
				
			||||||
	name: string;
 | 
						name: string;
 | 
				
			||||||
	@Column()
 | 
						@Column()
 | 
				
			||||||
@@ -78,6 +80,11 @@ export class User {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	@Column({ nullable: true })
 | 
						@Column({ nullable: true })
 | 
				
			||||||
	tfaSecret: string;
 | 
						tfaSecret: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column({ nullable: true })
 | 
				
			||||||
 | 
						gitlabAT: string;
 | 
				
			||||||
 | 
						@Column({ nullable: true })
 | 
				
			||||||
 | 
						gitlabRT: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity()
 | 
					@Entity()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,15 @@ import * as jwt from 'jsonwebtoken';
 | 
				
			|||||||
import { createTransport } from 'nodemailer';
 | 
					import { createTransport } from 'nodemailer';
 | 
				
			||||||
import * as notp from 'notp';
 | 
					import * as notp from 'notp';
 | 
				
			||||||
import { randomBytes } from 'crypto';
 | 
					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 jwtSecret = 'CUM';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -35,6 +44,19 @@ interface jwtPayload {
 | 
				
			|||||||
	iat?: 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()
 | 
					@Injectable()
 | 
				
			||||||
export class AuthService {
 | 
					export class AuthService {
 | 
				
			||||||
	constructor(
 | 
						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({
 | 
							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> {
 | 
						async validateUser(username: string, pass: string): Promise<User | null> {
 | 
				
			||||||
		const user = await this.findUser(username);
 | 
							const user = await this.findUser(username, false);
 | 
				
			||||||
		if (!user)
 | 
							if (!user)
 | 
				
			||||||
			throw new UnauthorizedException('Invalid username or password');
 | 
								throw new UnauthorizedException('Invalid username or password');
 | 
				
			||||||
		if (!(await argon2.verify(user.password, pass)))
 | 
							if (!(await argon2.verify(user.password, pass)))
 | 
				
			||||||
@@ -136,7 +159,11 @@ export class AuthService {
 | 
				
			|||||||
		await this.userRepo.save(user);
 | 
							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();
 | 
							const token = new JWTToken();
 | 
				
			||||||
		token.ownerId = user.id;
 | 
							token.ownerId = user.id;
 | 
				
			||||||
		const db_token = await this.tokenRepo.save(token);
 | 
							const db_token = await this.tokenRepo.save(token);
 | 
				
			||||||
@@ -153,16 +180,31 @@ export class AuthService {
 | 
				
			|||||||
		return jwtToken;
 | 
							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) {
 | 
						async signup(username: string, password: string) {
 | 
				
			||||||
		if (await this.findUser(username))
 | 
							if (await this.findUser(username, false))
 | 
				
			||||||
			throw new BadRequestException('User already exists');
 | 
								throw new BadRequestException('User already exists');
 | 
				
			||||||
		const user = new User();
 | 
							const user = new User();
 | 
				
			||||||
		user.name = username;
 | 
							user.name = username;
 | 
				
			||||||
		user.password = await argon2.hash(password);
 | 
							user.password = await argon2.hash(password);
 | 
				
			||||||
		const dbUser = await this.userRepo.save(user);
 | 
							await this.singupInternal(await this.userRepo.save(user));
 | 
				
			||||||
		const root = await this.fsService.generateRoot(dbUser);
 | 
					 | 
				
			||||||
		dbUser.rootId = root.id;
 | 
					 | 
				
			||||||
		await this.userRepo.save(dbUser);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async revoke(token: JWTToken) {
 | 
						async revoke(token: JWTToken) {
 | 
				
			||||||
@@ -176,6 +218,87 @@ export class AuthService {
 | 
				
			|||||||
			ownerId: user.id
 | 
								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()
 | 
					@Injectable()
 | 
				
			||||||
@@ -218,6 +341,13 @@ export class AuthJwtService extends PassportStrategy(JWTStrategy) {
 | 
				
			|||||||
			throw new UnauthorizedException(
 | 
								throw new UnauthorizedException(
 | 
				
			||||||
				'Invalid token, please log in again'
 | 
									'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
 | 
							// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 | 
				
			||||||
		// @ts-ignore
 | 
							// @ts-ignore
 | 
				
			||||||
		req.token = token;
 | 
							req.token = token;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								yarn.lock
									
									
									
									
									
								
							@@ -1699,6 +1699,14 @@ avvio@^8.1.3:
 | 
				
			|||||||
    debug "^4.0.0"
 | 
					    debug "^4.0.0"
 | 
				
			||||||
    fastq "^1.6.1"
 | 
					    fastq "^1.6.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					axios@^0.27.2:
 | 
				
			||||||
 | 
					  version "0.27.2"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
 | 
				
			||||||
 | 
					  integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    follow-redirects "^1.14.9"
 | 
				
			||||||
 | 
					    form-data "^4.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
babel-jest@^28.1.3:
 | 
					babel-jest@^28.1.3:
 | 
				
			||||||
  version "28.1.3"
 | 
					  version "28.1.3"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.3.tgz#c1187258197c099072156a0a121c11ee1e3917d5"
 | 
					  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.3.tgz#c1187258197c099072156a0a121c11ee1e3917d5"
 | 
				
			||||||
@@ -2800,6 +2808,11 @@ flatted@^3.1.0:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
 | 
					  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
 | 
				
			||||||
  integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
 | 
					  integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					follow-redirects@^1.14.9:
 | 
				
			||||||
 | 
					  version "1.15.1"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
 | 
				
			||||||
 | 
					  integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fork-ts-checker-webpack-plugin@7.2.11:
 | 
					fork-ts-checker-webpack-plugin@7.2.11:
 | 
				
			||||||
  version "7.2.11"
 | 
					  version "7.2.11"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.11.tgz#aff3febbc11544ba3ad0ae4d5aa4055bd15cd26d"
 | 
					  resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.11.tgz#aff3febbc11544ba3ad0ae4d5aa4055bd15cd26d"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user