From 226d2ebc1ed706d1959e8e98f5ab5c8d69f64a1f Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 27 May 2024 18:02:47 -0500 Subject: [PATCH] =?UTF-8?q?Auth=20Token=20=C3=9Cbersendung=20eingebaut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bizmatch-server/package.json | 1 + bizmatch-server/src/app.controller.ts | 15 ++- bizmatch-server/src/app.module.ts | 9 ++ bizmatch-server/src/auth/auth.module.ts | 17 ++-- bizmatch-server/src/drizzle/import.ts | 1 + .../src/jwt-auth/jwt-auth.guard.ts | 18 ++++ .../src/jwt-auth/optional-jwt-auth.guard.ts | 13 +++ bizmatch-server/src/jwt.strategy.ts | 36 +++++++ .../listings/business-listings.controller.ts | 9 +- ...commercial-property-listings.controller.ts | 10 +- .../src/listings/listings.module.ts | 3 +- .../src/listings/listings.service.ts | 6 +- bizmatch-server/src/main.ts | 10 +- bizmatch/src/app/app.config.ts | 11 ++- .../keycloak-bearer.interceptor.ts | 95 ------------------- 15 files changed, 131 insertions(+), 123 deletions(-) create mode 100644 bizmatch-server/src/jwt-auth/jwt-auth.guard.ts create mode 100644 bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts create mode 100644 bizmatch-server/src/jwt.strategy.ts delete mode 100644 bizmatch/src/app/interceptors/keycloak-bearer.interceptor.ts diff --git a/bizmatch-server/package.json b/bizmatch-server/package.json index eb08ec5..4847626 100644 --- a/bizmatch-server/package.json +++ b/bizmatch-server/package.json @@ -39,6 +39,7 @@ "drizzle-orm": "^0.30.8", "fs-extra": "^11.2.0", "handlebars": "^4.7.8", + "jwks-rsa": "^3.1.0", "ky": "^1.2.0", "nest-winston": "^1.9.4", "nodemailer": "^6.9.10", diff --git a/bizmatch-server/src/app.controller.ts b/bizmatch-server/src/app.controller.ts index 22fbf4d..3f0ae2c 100644 --- a/bizmatch-server/src/app.controller.ts +++ b/bizmatch-server/src/app.controller.ts @@ -1,12 +1,19 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, Request, UseGuards } from '@nestjs/common'; import { AppService } from './app.service.js'; +import { AuthService } from './auth/auth.service.js'; +import { JwtAuthGuard } from './jwt-auth/jwt-auth.guard.js'; @Controller() export class AppController { - constructor(private readonly appService: AppService) {} + constructor( + private readonly appService: AppService, + private authService: AuthService, + ) {} + @UseGuards(JwtAuthGuard) @Get() - getHello(): string { - return this.appService.getHello(); + getHello(@Request() req): string { + return req.user; + //return 'dfgdf'; } } diff --git a/bizmatch-server/src/app.module.ts b/bizmatch-server/src/app.module.ts index 07dd928..1c67b6e 100644 --- a/bizmatch-server/src/app.module.ts +++ b/bizmatch-server/src/app.module.ts @@ -15,6 +15,7 @@ import { MailModule } from './mail/mail.module.js'; import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js'; import { SelectOptionsModule } from './select-options/select-options.module.js'; +import { PassportModule } from '@nestjs/passport'; import { UserModule } from './user/user.module.js'; const __filename = fileURLToPath(import.meta.url); @@ -41,11 +42,19 @@ const __dirname = path.dirname(__filename); ], // other options }), + // KeycloakConnectModule.register({ + // authServerUrl: 'http://auth.bizmatch.net', + // realm: 'dev', + // clientId: 'dev', + // secret: 'Yu3lETbYUphDiJxgnhhpelcJ63p2FCDM', + // // Secret key of the client taken from keycloak server + // }), GeoModule, UserModule, ListingsModule, SelectOptionsModule, ImageModule, + PassportModule, ], controllers: [AppController], providers: [AppService, FileService], diff --git a/bizmatch-server/src/auth/auth.module.ts b/bizmatch-server/src/auth/auth.module.ts index 18fbde9..f1a8f81 100644 --- a/bizmatch-server/src/auth/auth.module.ts +++ b/bizmatch-server/src/auth/auth.module.ts @@ -1,17 +1,16 @@ import { Module } from '@nestjs/common'; -import { MailerModule } from '@nestjs-modules/mailer'; -import path, { join } from 'path'; -import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js'; +import { PassportModule } from '@nestjs/passport'; +import path from 'path'; import { fileURLToPath } from 'url'; -import { AuthService } from './auth.service.js'; +import { JwtStrategy } from '../jwt.strategy.js'; import { AuthController } from './auth.controller.js'; +import { AuthService } from './auth.service.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @Module({ - imports: [ - ], - providers: [AuthService], + imports: [PassportModule], + providers: [AuthService, JwtStrategy], controllers: [AuthController], - exports:[AuthService] + exports: [AuthService], }) -export class AuthModule {} \ No newline at end of file +export class AuthModule {} diff --git a/bizmatch-server/src/drizzle/import.ts b/bizmatch-server/src/drizzle/import.ts index a71111b..a112183 100644 --- a/bizmatch-server/src/drizzle/import.ts +++ b/bizmatch-server/src/drizzle/import.ts @@ -109,6 +109,7 @@ for (const commercial of commercialJsonData) { commercial.created = insertionDate; commercial.updated = insertionDate; commercial.userId = user.insertedId; + commercial.draft = false; const result = await db.insert(schema.commercials).values(commercial).returning(); //fs.ensureDirSync(`./pictures/property/${result[0].imagePath}/${result[0].serialId}`); try { diff --git a/bizmatch-server/src/jwt-auth/jwt-auth.guard.ts b/bizmatch-server/src/jwt-auth/jwt-auth.guard.ts new file mode 100644 index 0000000..45d4ac7 --- /dev/null +++ b/bizmatch-server/src/jwt-auth/jwt-auth.guard.ts @@ -0,0 +1,18 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate { + canActivate(context: ExecutionContext) { + // Add your custom authentication logic here + // for example, call super.logIn(request) to establish a session. + return super.canActivate(context); + } + handleRequest(err, user, info) { + // You can throw an exception based on either "info" or "err" arguments + if (err || !user) { + throw err || new UnauthorizedException(info); + } + return user; + } +} diff --git a/bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts b/bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts new file mode 100644 index 0000000..26dcccd --- /dev/null +++ b/bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class OptionalJwtAuthGuard extends AuthGuard('jwt') { + handleRequest(err, user, info) { + // Wenn der Benutzer nicht authentifiziert ist, aber kein Fehler vorliegt, geben Sie null zurück + if (err || !user) { + return null; + } + return user; + } +} diff --git a/bizmatch-server/src/jwt.strategy.ts b/bizmatch-server/src/jwt.strategy.ts new file mode 100644 index 0000000..ba64d28 --- /dev/null +++ b/bizmatch-server/src/jwt.strategy.ts @@ -0,0 +1,36 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { passportJwtSecret } from 'jwks-rsa'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKeyProvider: passportJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: 'https://auth.bizmatch.net/realms/dev/protocol/openid-connect/certs', + }), + audience: 'account', // Keycloak Client ID + issuer: 'https://auth.bizmatch.net/realms/dev', + algorithms: ['RS256'], + }); + } + + async validate(payload: any) { + console.log('JWT Payload:', payload); // Debugging: JWT Payload anzeigen + if (!payload) { + console.error('Invalid payload'); + throw new UnauthorizedException(); + } + if (!payload.sub || !payload.preferred_username) { + console.error('Missing required claims'); + throw new UnauthorizedException(); + } + return { userId: payload.sub, username: payload.preferred_username, roles: payload.realm_access?.roles }; + } +} diff --git a/bizmatch-server/src/listings/business-listings.controller.ts b/bizmatch-server/src/listings/business-listings.controller.ts index 7fada7b..578cf9b 100644 --- a/bizmatch-server/src/listings/business-listings.controller.ts +++ b/bizmatch-server/src/listings/business-listings.controller.ts @@ -1,7 +1,8 @@ -import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; import { businesses } from '../drizzle/schema.js'; +import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js'; import { ListingCriteria } from '../models/main.model.js'; import { ListingsService } from './listings.service.js'; @@ -16,9 +17,11 @@ export class BusinessListingsController { findById(@Param('id') id: string): any { return this.listingsService.findById(id, businesses); } + + @UseGuards(OptionalJwtAuthGuard) @Get('user/:userid') - findByUserId(@Param('userid') userid: string): any { - return this.listingsService.findByUserId(userid, businesses); + findByUserId(@Request() req, @Param('userid') userid: string): any { + return this.listingsService.findByUserId(userid, businesses, req.user?.username); } @Post('search') diff --git a/bizmatch-server/src/listings/commercial-property-listings.controller.ts b/bizmatch-server/src/listings/commercial-property-listings.controller.ts index 83757a2..48203c5 100644 --- a/bizmatch-server/src/listings/commercial-property-listings.controller.ts +++ b/bizmatch-server/src/listings/commercial-property-listings.controller.ts @@ -1,8 +1,9 @@ -import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; import { commercials } from '../drizzle/schema.js'; import { FileService } from '../file/file.service.js'; +import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js'; import { ListingCriteria } from '../models/main.model.js'; import { ListingsService } from './listings.service.js'; @@ -18,9 +19,12 @@ export class CommercialPropertyListingsController { findById(@Param('id') id: string): any { return this.listingsService.findById(id, commercials); } + + @UseGuards(OptionalJwtAuthGuard) @Get('user/:userid') - findByUserId(@Param('userid') userid: string): any { - return this.listingsService.findByUserId(userid, commercials); + findByUserId(@Request() req, @Param('userid') userid: string): any { + console.log(req.user?.username); + return this.listingsService.findByUserId(userid, commercials, req.user?.username); } @Post('search') async find(@Body() criteria: ListingCriteria): Promise { diff --git a/bizmatch-server/src/listings/listings.module.ts b/bizmatch-server/src/listings/listings.module.ts index 7fc855a..60a1ae4 100644 --- a/bizmatch-server/src/listings/listings.module.ts +++ b/bizmatch-server/src/listings/listings.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { AuthModule } from '../auth/auth.module.js'; import { DrizzleModule } from '../drizzle/drizzle.module.js'; import { FileService } from '../file/file.service.js'; import { UserService } from '../user/user.service.js'; @@ -9,7 +10,7 @@ import { ListingsService } from './listings.service.js'; import { UnknownListingsController } from './unknown-listings.controller.js'; @Module({ - imports: [DrizzleModule], + imports: [DrizzleModule, AuthModule], controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController], providers: [ListingsService, FileService, UserService], exports: [ListingsService], diff --git a/bizmatch-server/src/listings/listings.service.ts b/bizmatch-server/src/listings/listings.service.ts index 1619066..bfcfb00 100644 --- a/bizmatch-server/src/listings/listings.service.ts +++ b/bizmatch-server/src/listings/listings.service.ts @@ -68,17 +68,17 @@ export class ListingsService { const result = await this.conn .select() .from(table) - .where(sql`${table.id} = ${id}`); + .where(and(sql`${table.id} = ${id}`, ne(table.draft, true))); return result[0] as BusinessListing | CommercialPropertyListing; } async findByImagePath(imagePath: string, serial: string): Promise { const result = await this.conn .select() .from(commercials) - .where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`)); + .where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`, ne(commercials.draft, true))); return result[0] as CommercialPropertyListing; } - async findByUserId(userId: string, table: typeof businesses | typeof commercials): Promise { + async findByUserId(userId: string, table: typeof businesses | typeof commercials, email: string): Promise { return (await this.conn.select().from(table).where(eq(table.userId, userId))) as BusinessListing[] | CommercialPropertyListing[]; } diff --git a/bizmatch-server/src/main.ts b/bizmatch-server/src/main.ts index b040e08..bd873a8 100644 --- a/bizmatch-server/src/main.ts +++ b/bizmatch-server/src/main.ts @@ -1,17 +1,19 @@ import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module.js'; -import * as express from 'express'; -import path, { join } from 'path'; +import express from 'express'; +import path from 'path'; import { fileURLToPath } from 'url'; +import { AppModule } from './app.module.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); async function bootstrap() { + const server = express(); const app = await NestFactory.create(AppModule); app.setGlobalPrefix('bizmatch'); app.enableCors({ origin: '*', + //origin: 'http://localhost:4200', // Die URL Ihrer Angular-App methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', - allowedHeaders: 'Content-Type, Accept', + allowedHeaders: 'Content-Type, Accept, Authorization', }); //origin: 'http://localhost:4200', await app.listen(3000); diff --git a/bizmatch/src/app/app.config.ts b/bizmatch/src/app/app.config.ts index afb6f72..40519b3 100644 --- a/bizmatch/src/app/app.config.ts +++ b/bizmatch/src/app/app.config.ts @@ -3,7 +3,7 @@ import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScroll import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { provideAnimations } from '@angular/platform-browser/animations'; -import { KeycloakService } from 'keycloak-angular'; +import { KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular'; import { environment } from '../environments/environment'; import { customKeycloakAdapter } from '../keycloak'; import { routes } from './app.routes'; @@ -37,6 +37,11 @@ export const appConfig: ApplicationConfig = { useClass: LoadingInterceptor, multi: true, }, + { + provide: HTTP_INTERCEPTORS, + useClass: KeycloakBearerInterceptor, + multi: true, + }, provideRouter( routes, withEnabledBlockingInitialNavigation(), @@ -89,6 +94,10 @@ function initializeKeycloak(keycloak: KeycloakService) { onLoad: 'check-sso', silentCheckSsoRedirectUri: (window).location.origin + '/assets/silent-check-sso.html', }, + bearerExcludedUrls: ['/assets'], + shouldUpdateToken(request) { + return !request.headers.get('token-update') === false; + }, }); logger.info(`+++>${authenticated}`); }; diff --git a/bizmatch/src/app/interceptors/keycloak-bearer.interceptor.ts b/bizmatch/src/app/interceptors/keycloak-bearer.interceptor.ts deleted file mode 100644 index 2e64ad3..0000000 --- a/bizmatch/src/app/interceptors/keycloak-bearer.interceptor.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @license - * Copyright Mauricio Gemelli Vigolo and contributors. - * - * Use of this source code is governed by a MIT-style license that can be - * found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md - */ - -import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; -import { Injectable } from '@angular/core'; - -import { Observable, combineLatest, from, of } from 'rxjs'; -import { mergeMap } from 'rxjs/operators'; - -import { ExcludedUrlRegex } from '../models/keycloak-options'; -import { KeycloakService } from '../services/keycloak.service'; - -/** - * This interceptor includes the bearer by default in all HttpClient requests. - * - * If you need to exclude some URLs from adding the bearer, please, take a look - * at the {@link KeycloakOptions} bearerExcludedUrls property. - */ -@Injectable() -export class KeycloakBearerInterceptor implements HttpInterceptor { - constructor(private keycloak: KeycloakService) {} - - /** - * Calls to update the keycloak token if the request should update the token. - * - * @param req http request from @angular http module. - * @returns - * A promise boolean for the token update or noop result. - */ - private async conditionallyUpdateToken(req: HttpRequest): Promise { - if (this.keycloak.shouldUpdateToken(req)) { - return await this.keycloak.updateToken(); - } - - return true; - } - - /** - * @deprecated - * Checks if the url is excluded from having the Bearer Authorization - * header added. - * - * @param req http request from @angular http module. - * @param excludedUrlRegex contains the url pattern and the http methods, - * excluded from adding the bearer at the Http Request. - */ - private isUrlExcluded({ method, url }: HttpRequest, { urlPattern, httpMethods }: ExcludedUrlRegex): boolean { - const httpTest = httpMethods.length === 0 || httpMethods.join().indexOf(method.toUpperCase()) > -1; - - const urlTest = urlPattern.test(url); - - return httpTest && urlTest; - } - - /** - * Intercept implementation that checks if the request url matches the excludedUrls. - * If not, adds the Authorization header to the request if the user is logged in. - * - * @param req - * @param next - */ - public intercept(req: HttpRequest, next: HttpHandler): Observable> { - const { enableBearerInterceptor, excludedUrls } = this.keycloak; - if (!enableBearerInterceptor) { - return next.handle(req); - } - - const shallPass: boolean = !this.keycloak.shouldAddToken(req) || excludedUrls.findIndex(item => this.isUrlExcluded(req, item)) > -1; - if (shallPass) { - return next.handle(req); - } - - return combineLatest([from(this.conditionallyUpdateToken(req)), of(this.keycloak.isLoggedIn())]).pipe(mergeMap(([_, isLoggedIn]) => (isLoggedIn ? this.handleRequestWithTokenHeader(req, next) : next.handle(req)))); - } - - /** - * Adds the token of the current user to the Authorization header - * - * @param req - * @param next - */ - private handleRequestWithTokenHeader(req: HttpRequest, next: HttpHandler): Observable> { - return this.keycloak.addTokenToHeader(req.headers).pipe( - mergeMap(headersWithBearer => { - const kcReq = req.clone({ headers: headersWithBearer }); - return next.handle(kcReq); - }), - ); - } -}