authentication

This commit is contained in:
Bastian Wagner
2024-09-13 21:14:09 +02:00
parent c00aad559d
commit b4a5f04505
65 changed files with 1140 additions and 77 deletions

View File

@@ -1,18 +1,22 @@
import { Body, Controller, Get, Post } from '@nestjs/common';
import { Body, Controller, Get, HttpException, HttpStatus, Post, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { LoginDTO } from './model/dto/login.dto';
import { AuthGuard } from './core/guards/auth.guard';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@UseGuards(AuthGuard)
@Get()
getHello(): any {
return this.appService.getHello();
}
@Post()
login(@Body() createUserDto: LoginDTO) {
return { success: createUserDto };
}
}

View File

@@ -4,6 +4,9 @@ import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './shared/database/database.module';
import { AuthModule } from './modules/auth/auth.module';
import { AuthGuard } from './core/guards/auth.guard';
import { UserModule } from './modules/user/user.module';
import { RoleModule } from './modules/role/role.module';
@Module({
imports: [
@@ -13,8 +16,10 @@ import { AuthModule } from './modules/auth/auth.module';
}),
DatabaseModule,
AuthModule,
UserModule,
RoleModule,
],
controllers: [AppController],
providers: [AppService],
providers: [AppService, AuthGuard],
})
export class AppModule {}

View File

@@ -0,0 +1,61 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JsonWebTokenError, JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { IPayload } from 'src/model/interface';
import { AuthService } from 'src/modules/auth/auth.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private config: ConfigService,
private authService: AuthService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('Token not provided');
}
try {
const secret = this.config.get('JWT_SECRET');
// Überprüft das JWT und dekodiert es
const payload: IPayload = this.jwtService.verify(token, { secret });
if (payload.type != 'access') {
throw new UnauthorizedException('wrong token');
}
const user = await this.authService.getUserById(payload.id);
if (!user.isActive) {
throw new HttpException('not active', HttpStatus.FORBIDDEN);
}
request['user'] = user;
} catch (error) {
const j = error as JsonWebTokenError;
const m = j.message;
throw new UnauthorizedException(m);
}
return true;
}
private extractTokenFromHeader(request: Request): string | null {
const authHeader = request.headers['authorization'];
if (!authHeader) {
return null;
}
const [type, token] = authHeader.split(' ');
return type === 'Bearer' && token ? token : null;
}
}

View File

@@ -0,0 +1,5 @@
import { Expose } from 'class-transformer';
export function AdminGroup() {
return Expose({ groups: ['admin'] }); // Setzt die Gruppe 'admin' automatisch
}

View File

@@ -1,2 +1,3 @@
export * from './sso.user.entity';
export * from './user.entity';
export * from './role.entity';

View File

@@ -0,0 +1,19 @@
import {
Column,
Entity,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Role {
@PrimaryGeneratedColumn('uuid')
id: string;
@OneToMany(() => User, (user) => user.role)
user: User[];
@Column({ nullable: true })
name: string;
}

View File

@@ -1,14 +1,17 @@
import { Exclude } from 'class-transformer';
import { Exclude, Transform } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { IUser } from '../interface';
import { SSOUser } from './sso.user.entity';
import { IsEmail } from 'class-validator';
import { Role } from './role.entity';
@Entity()
export class User implements IUser {
@@ -35,10 +38,14 @@ export class User implements IUser {
@OneToOne(() => SSOUser, (sso) => sso.user, { eager: true, cascade: true })
external: SSOUser;
@Exclude()
@Column({ default: true })
isActive: boolean;
@ManyToOne(() => Role, (role) => role.user, { cascade: true })
@JoinColumn()
@Transform(({ value }) => value.name)
role: Role;
accessToken?: string;
refreshToken?: string;
}

View File

@@ -1,3 +1,5 @@
import { Role } from '../entitites';
export interface IUser {
id: string;
username: string;
@@ -7,4 +9,6 @@ export interface IUser {
accessToken?: string;
refreshToken?: string;
role?: string | Role;
}

View File

@@ -1,2 +1,3 @@
export * from './user.repository';
export * from './ssouser.repository';
export * from './role.repository';

View File

@@ -0,0 +1,14 @@
import { Injectable } from '@nestjs/common';
import { Repository, DataSource } from 'typeorm';
import { Role } from '../entitites';
@Injectable()
export class RoleRepository extends Repository<Role> {
constructor(dataSource: DataSource) {
super(Role, dataSource.createEntityManager());
}
getStandardRole(): Promise<Role> {
return this.findOne({ where: { name: 'develop' } });
}
}

View File

@@ -3,12 +3,14 @@ import { Repository, DataSource } from 'typeorm';
import { User } from '../entitites';
import { CreateUserDto } from '../dto/create-user.dto';
import { SsoUserRepository } from './ssouser.repository';
import { RoleRepository } from './role.repository';
@Injectable()
export class UserRepository extends Repository<User> {
constructor(
dataSource: DataSource,
private ssoRepo: SsoUserRepository,
private roleRepo: RoleRepository,
) {
super(User, dataSource.createEntityManager());
}
@@ -38,6 +40,7 @@ export class UserRepository extends Repository<User> {
externalId: createUserDto.externalId,
});
created.external = sso;
created.role = await this.roleRepo.getStandardRole();
const user = await this.save(created);
sso.user = user;
this.ssoRepo.save(sso);

View File

@@ -1,13 +1,17 @@
import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthCodeDto } from 'src/model/dto';
import { User } from 'src/model/entitites';
import { AuthGuard } from 'src/core/guards/auth.guard';
@Controller('auth')
export class AuthController {
@@ -23,4 +27,19 @@ export class AuthController {
}
return user;
}
@UseGuards(AuthGuard)
@Get('me')
getMe(@Req() req: any) {
return req.user;
}
@Post('refresh')
async getNewAccessToken(@Body() b: any) {
if (b.refreshToken) {
return this.authService.getNewToken(b.refreshToken);
}
throw new HttpException('no token', HttpStatus.BAD_REQUEST);
}
}

View File

@@ -4,7 +4,7 @@ import { AuthService } from './auth.service';
import { DatabaseModule } from 'src/shared/database/database.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HttpModule } from '@nestjs/axios';
import { JwtModule } from '@nestjs/jwt';
import { JwtModule, JwtService } from '@nestjs/jwt';
@Module({
controllers: [AuthController],
@@ -22,5 +22,6 @@ import { JwtModule } from '@nestjs/jwt';
}),
}),
],
exports: [JwtModule, AuthService],
})
export class AuthModule {}

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { AuthCodeDto } from 'src/model/dto';
import { CreateUserDto } from 'src/model/dto/create-user.dto';
import { UserRepository } from 'src/model/repositories';
@@ -21,7 +21,6 @@ export class AuthService {
}
async registerOrLoginWithAuthCode(auth: AuthCodeDto): Promise<User> {
console.log(auth);
const body = this.createAuthCodeFormData(auth.code, 'authorization_code');
const url = this.configService.get('SSO_TOKEN_URL');
return new Promise<User>((resolve) => {
@@ -45,7 +44,6 @@ export class AuthService {
access_token: string;
refresh_token: string;
}): Promise<User> {
console.log(access_token, refresh_token);
const payload: IExternalAccessPayload = this.jwt.decode(access_token);
return new Promise<User>(async (resolve) => {
let user = await this.userRepo.findByUsername(payload.username);
@@ -56,6 +54,9 @@ export class AuthService {
externalId: payload.id,
});
}
if (!user.isActive) {
throw new HttpException('not active', HttpStatus.FORBIDDEN);
}
user.firstName = payload.firstName;
user.lastName = payload.lastName;
@@ -82,7 +83,7 @@ export class AuthService {
type: 'refresh',
};
user.refreshToken = this.jwt.sign(rPay);
user.refreshToken = this.jwt.sign(rPay, { expiresIn: '1w' });
}
private createAuthCodeFormData(
@@ -103,4 +104,76 @@ export class AuthService {
);
return bodyFormData;
}
getUserById(id: string): Promise<User> {
return this.userRepo.findById(id);
}
async getNewToken(refresh: string) {
try {
const payload: IPayload = this.jwt.verify(refresh);
const user = await this.getUserById(payload.id);
if (!user) {
throw new HttpException('not valid', HttpStatus.UNAUTHORIZED);
}
const s = await this.verifyExternal(user);
if (!s) {
throw new HttpException('not valid', HttpStatus.UNAUTHORIZED);
}
this.generateTokens(user);
return user;
} catch (e) {
throw new HttpException('invalid token', HttpStatus.BAD_REQUEST);
}
}
verifyExternal(user: User) {
if (!user || !user.external) {
return false;
}
const url = this.configService.get('SSO_VERIFY_URL');
return new Promise((resolve) => {
this.http
.post(url, { access_token: user.external.accessToken })
.subscribe({
next: (response) => {
const id = response.data.id;
if (id == user.external.externalId) {
resolve(true);
} else {
resolve(false);
}
},
error: async (error) => {
const data = error.response.data;
if (data.message == 'jwt expired') {
const s = await resolve(this.refreshExternalAccessToken(user));
return resolve(s);
}
resolve(false);
},
});
});
}
async refreshExternalAccessToken(user: User): Promise<any> {
const bodyFormData = this.createAuthCodeFormData(
user.external.refreshToken,
'refresh_token',
);
const url = this.configService.get('SSO_TOKEN_URL');
return new Promise((resolve) => {
this.http.post<any>(url, bodyFormData).subscribe({
next: async (response) => {
user.external.accessToken = response.data.access_token;
await this.userRepo.save(user);
resolve(true);
},
error: () => {
resolve(false);
},
});
});
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RoleController } from './role.controller';
describe('RoleController', () => {
let controller: RoleController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RoleController],
}).compile();
controller = module.get<RoleController>(RoleController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,15 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { RoleService } from './role.service';
import { AuthGuard } from 'src/core/guards/auth.guard';
@UseGuards(AuthGuard)
@Controller('role')
export class RoleController {
constructor(private readonly rolesService: RoleService) {}
@Get()
getRoles() {
return this.rolesService.getRoles();
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { RoleController } from './role.controller';
import { RoleService } from './role.service';
import { DatabaseModule } from 'src/shared/database/database.module';
import { AuthModule } from '../auth/auth.module';
@Module({
controllers: [RoleController],
providers: [RoleService],
imports: [ DatabaseModule, AuthModule ]
})
export class RoleModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RoleService } from './role.service';
describe('RoleService', () => {
let service: RoleService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [RoleService],
}).compile();
service = module.get<RoleService>(RoleService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,11 @@
import { Injectable } from '@nestjs/common';
import { RoleRepository } from 'src/model/repositories';
@Injectable()
export class RoleService {
constructor(private readonly rolesRepo: RoleRepository) {}
getRoles() {
return this.rolesRepo.find();
}
}

View File

@@ -0,0 +1,21 @@
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from 'src/core/guards/auth.guard';
import { UserService } from './user.service';
import { User } from 'src/model/entitites';
import { IUser } from 'src/model/interface';
@UseGuards(AuthGuard)
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
getUsers(): Promise<User[]> {
return this.userService.getAllUsers();
}
@Post()
saveUser(@Body() user: IUser) {
return this.userService.saveUser(user);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { AuthModule } from '../auth/auth.module';
import { DatabaseModule } from 'src/shared/database/database.module';
@Module({
controllers: [UserController],
providers: [UserService],
imports: [AuthModule, DatabaseModule],
})
export class UserModule {}

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { User } from 'src/model/entitites';
import { IUser } from 'src/model/interface';
import { RoleRepository, UserRepository } from 'src/model/repositories';
@Injectable()
export class UserService {
constructor(
private readonly userRepo: UserRepository,
private readonly roleRepo: RoleRepository,
) {}
getAllUsers(): Promise<User[]> {
return this.userRepo.find({
relations: ['role'],
where: [{ role: { name: 'user' } }, { role: { name: 'admin' } }],
});
}
async saveUser(user: IUser) {
if (typeof user.role == 'string') {
user.role = await this.roleRepo.findOneBy({ name: user.role });
}
return this.userRepo.save(user as any);
}
}

View File

@@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SSOUser, User } from 'src/model/entitites';
import { SsoUserRepository, UserRepository } from 'src/model/repositories';
import { Role, SSOUser, User } from 'src/model/entitites';
import { RoleRepository, SsoUserRepository, UserRepository } from 'src/model/repositories';
const ENTITIES = [User, SSOUser];
const REPOSITORIES = [UserRepository, SsoUserRepository];
const ENTITIES = [User, SSOUser, Role];
const REPOSITORIES = [UserRepository, SsoUserRepository, RoleRepository];
@Module({
imports: [