This commit is contained in:
Bastian Wagner
2024-08-25 16:54:55 +02:00
parent 7ca6c320bd
commit 328062eaa0
10 changed files with 150 additions and 30 deletions

View File

@@ -1,13 +1,11 @@
import { Injectable, MiddlewareConsumer, Module, NestMiddleware, NestModule, RequestMethod } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
import path, { join } from 'path'; import { join } from 'path';
import { AppController } from './app.controller'; import { LoggerModule } from './core/logger.module';
import * as express from 'express';
@Module({ @Module({
imports: [ imports: [
AuthModule, AuthModule,
@@ -15,6 +13,7 @@ import * as express from 'express';
envFilePath: ['.env'], envFilePath: ['.env'],
isGlobal: true, isGlobal: true,
}), }),
LoggerModule,
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({
rootPath: join(__dirname, '../client'), rootPath: join(__dirname, '../client'),
exclude: ['*/api*'], exclude: ['*/api*'],
@@ -46,5 +45,6 @@ import * as express from 'express';
], ],
controllers: [], controllers: [],
providers: [AppService], providers: [AppService],
exports: [],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -3,12 +3,14 @@ import { UsersService } from 'src/users/users.service';
import { ClientService } from 'src/client/client.service'; import { ClientService } from 'src/client/client.service';
import { FormDataRequest } from 'nestjs-form-data'; import { FormDataRequest } from 'nestjs-form-data';
import { Client } from 'src/model/client.entity'; import { Client } from 'src/model/client.entity';
import { CustomLogger } from 'src/core/custom.logger';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor( constructor(
private usersService: UsersService, private usersService: UsersService,
private clientService: ClientService, private clientService: ClientService,
private logger: CustomLogger,
) {} ) {}
@Post('register') @Post('register')
@@ -31,7 +33,7 @@ export class AuthController {
password, password,
clientId, clientId,
); );
this.logger.log(`User ${username} logged in on client ${clientId}`);
return token; return token;
} }
@@ -57,14 +59,12 @@ export class AuthController {
@Body('client_secret') clientSecret: string, @Body('client_secret') clientSecret: string,
@Body('code') code: string, @Body('code') code: string,
@Body('grant_type') grantType: string, @Body('grant_type') grantType: string,
@Body('redirect_uri') redirectUri: string,
) { ) {
return this.usersService.verifyToken({ return this.usersService.verifyToken({
clientId, clientId,
clientSecret, clientSecret,
code, code,
grantType, grantType,
redirectUri,
}); });
} }

View File

@@ -12,6 +12,7 @@ import {
AuthorizationCode, AuthorizationCode,
AuthorizationCodeRepository, AuthorizationCodeRepository,
} from 'src/model/auth-code.entity'; } from 'src/model/auth-code.entity';
import { LoggerModule } from 'src/core/logger.module';
@Module({ @Module({
providers: [ providers: [
@@ -25,10 +26,11 @@ import {
imports: [ imports: [
JwtModule.register({ JwtModule.register({
secret: 'SECRET_KEY', // Ändere dies zu einer Umgebungsvariablen in einer realen Anwendung secret: 'SECRET_KEY', // Ändere dies zu einer Umgebungsvariablen in einer realen Anwendung
signOptions: { expiresIn: '60s' }, signOptions: { expiresIn: '15m' },
}), }),
NestjsFormDataModule, NestjsFormDataModule,
TypeOrmModule.forFeature([User, Client, RedirectUri, AuthorizationCode]), TypeOrmModule.forFeature([User, Client, RedirectUri, AuthorizationCode]),
LoggerModule,
], ],
}) })
export class AuthModule {} export class AuthModule {}

View File

@@ -0,0 +1,42 @@
import { LoggerService, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Log } from 'src/model/log.entity';
import { Repository } from 'typeorm';
@Injectable()
export class CustomLogger implements LoggerService {
private readonly logger = new Logger('CustomLogger');
// constructor(private readonly connection: Connection) {}
constructor(@InjectRepository(Log) private logRepository: Repository<Log>) {}
private async logToDatabase(level: string, message: string) {
const logEntry = new Log();
logEntry.level = level;
logEntry.message = message;
logEntry.timestamp = new Date();
await this.logRepository.save(logEntry);
}
private logToConsole(level: string, message: string) {
this.logger[level](message);
}
async error(message: any) {
await this.logToDatabase('error', message.toString());
this.logToConsole('error', message);
}
async warn(message: any) {
await this.logToDatabase('warn', message.toString());
this.logToConsole('warn', message);
}
async log(message: any) {
this.logToDatabase('log', message);
}
async debug(message: any) {
this.logToConsole('debug', message);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { Log } from 'src/model/log.entity';
import { CustomLogger } from './custom.logger';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
providers: [CustomLogger],
controllers: [],
imports: [TypeOrmModule.forFeature([Log])],
exports: [CustomLogger],
})
export class LoggerModule {}

View File

@@ -1,6 +1,6 @@
import { NestFactory, Reflector } from '@nestjs/core'; import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; import { Logger, ValidationPipe } from '@nestjs/common';
import { ClassSerializerInterceptor } from '@nestjs/common'; import { ClassSerializerInterceptor } from '@nestjs/common';
async function bootstrap() { async function bootstrap() {
@@ -19,6 +19,7 @@ async function bootstrap() {
); );
const port = parseInt(process.env.PORT) || 5000; const port = parseInt(process.env.PORT) || 5000;
await app.listen(port); await app.listen(port);
console.log(`Application is running on: ${await app.getUrl()}`); const logger = new Logger('bootstrap');
logger.log(`Application is running on: ${await app.getUrl()}\n`);
} }
bootstrap(); bootstrap();

View File

@@ -0,0 +1,7 @@
export interface CustomLogger {
log(message: string, context?: string): void;
error(message: string, trace?: string, context?: string): void;
warn(message: string, context?: string): void;
debug(message: string, context?: string): void;
verbose(message: string, context?: string): void;
}

View File

@@ -0,0 +1,16 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Log {
@PrimaryGeneratedColumn()
id: number;
@Column()
level: string;
@Column()
message: string;
@Column()
timestamp: Date;
}

View File

@@ -12,6 +12,7 @@ import {
DataSource, DataSource,
Repository, Repository,
OneToMany, OneToMany,
CreateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { AuthorizationCode } from './auth-code.entity'; import { AuthorizationCode } from './auth-code.entity';
@@ -27,6 +28,16 @@ export class User {
@Column() @Column()
password: string; password: string;
@Column({ name: 'first_name', nullable: true })
firstName?: string;
@Column({ name: 'last_name', nullable: true })
lastName?: string;
@CreateDateColumn({ name: 'created_at' })
createdAt?: Date;
@Exclude()
@Column({ default: true }) @Column({ default: true })
isActive: boolean; isActive: boolean;

View File

@@ -8,6 +8,7 @@ import {
AuthorizationCodeRepository, AuthorizationCodeRepository,
} from 'src/model/auth-code.entity'; } from 'src/model/auth-code.entity';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { CustomLogger } from 'src/core/custom.logger';
@Injectable() @Injectable()
export class UsersService { export class UsersService {
@@ -16,6 +17,7 @@ export class UsersService {
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private userRepo: UserRepository, private userRepo: UserRepository,
private tokenRepo: AuthorizationCodeRepository, private tokenRepo: AuthorizationCodeRepository,
private logger: CustomLogger,
) {} ) {}
async createUser(username: string, password: string): Promise<User> { async createUser(username: string, password: string): Promise<User> {
@@ -39,17 +41,20 @@ export class UsersService {
): Promise<{ code: string }> { ): Promise<{ code: string }> {
const user = await this.userRepo.findByUsername(username); const user = await this.userRepo.findByUsername(username);
if (!user) { if (!user) {
this.logger.error(`User ${username} not found`);
throw new Error('User not found'); throw new Error('User not found');
} }
const valid = await bcrypt.compare(password, user.password); const valid = await bcrypt.compare(password, user.password);
if (!valid) { if (!valid) {
this.logger.error(`User ${username} provided invalid credentials`);
throw new HttpException('Invalid credentials', 401); throw new HttpException('Invalid credentials', 401);
} }
const client = await this.clientService.getClientById(clientId); const client = await this.clientService.getClientById(clientId);
if (!client) { if (!client) {
this.logger.error(`Client ${clientId} not found`);
throw new HttpException('Invalid client', 401); throw new HttpException('Invalid client', 401);
} }
@@ -63,18 +68,10 @@ export class UsersService {
return { code: token.code }; return { code: token.code };
} }
async verifyToken({ async verifyToken({ clientId, clientSecret, code, grantType }: any) {
clientId,
clientSecret,
code,
grantType,
redirectUri,
}: any) {
if (grantType !== 'authorization_code') {
throw new HttpException('Invalid grant type', 401);
}
const client = await this.clientService.getClientById(clientId); const client = await this.clientService.getClientById(clientId);
if (!client) { if (!client) {
this.logger.error(`Client ${clientId} not found for verification`);
throw new HttpException('Invalid client', 401); throw new HttpException('Invalid client', 401);
} }
@@ -82,8 +79,12 @@ export class UsersService {
throw new HttpException('Invalid client', 401); throw new HttpException('Invalid client', 401);
} }
if (!client.redirectUris.some((uri) => uri.uri === redirectUri)) { if (grantType == 'refresh_token') {
throw new HttpException('Invalid redirect', 401); return this.getNewAccessToken(code);
}
if (grantType !== 'authorization_code') {
throw new HttpException('Invalid grant type', 401);
} }
const token = await this.tokenRepo.findByCode(code); const token = await this.tokenRepo.findByCode(code);
@@ -103,25 +104,53 @@ export class UsersService {
this.tokenRepo.removeCode(token); this.tokenRepo.removeCode(token);
const access_token = this.createAccessToken(user);
const refresh_token = this.createRefreshToken(user);
return {
access_token,
refresh_token,
};
}
private createAccessToken(user: User) {
const payload = { const payload = {
username: user.username, username: user.username,
id: user.id, id: user.id,
iss: 'http://localhost:3000', firstName: user.firstName,
aud: 'http://localhost:3000', lastName: user.lastName,
iss: 'sso.beantastic.de',
aud: 'not set',
}; };
return this.jwtService.sign(payload);
}
const access_token = this.jwtService.sign(payload); private createRefreshToken(user: User) {
const refresh_token = this.jwtService.sign( this.jwtService.sign(
{ {
type: 'refresh', type: 'refresh',
id: user.id, id: user.id,
}, },
{ expiresIn: '365d' }, { expiresIn: '365d' },
); );
return { }
access_token,
refresh_token, async getNewAccessToken(refreshToken: string) {
const payload = this.jwtService.verify(refreshToken);
if (payload.type !== 'refresh') {
throw new HttpException('Invalid token', 401);
}
const user = await this.userRepo.findById(payload.id);
if (!user) {
throw new HttpException('Invalid token', 401);
}
const token = this.createAccessToken(user);
const pay = this.jwtService.decode(token);
const result = {
access_token: token,
expires_in: pay.exp - pay.iat,
}; };
return result;
} }
verifyAccessToken(token: string) { verifyAccessToken(token: string) {