Added gitlab authentication
This commit is contained in:
parent
70dd272b5d
commit
715311f575
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user