Added gitlab authentication

This commit is contained in:
Mutzi 2022-08-24 21:27:04 +02:00
parent 70dd272b5d
commit 715311f575
3 changed files with 171 additions and 12 deletions

View File

@ -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}`
};
}
} }

View File

@ -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()

View File

@ -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;