diff --git a/idp/src/app.module.ts b/idp/src/app.module.ts index 9210011..5fc13f9 100644 --- a/idp/src/app.module.ts +++ b/idp/src/app.module.ts @@ -8,6 +8,7 @@ import { join } from 'path'; import { LoggerModule } from './core/logger.module'; import { SessionMiddleware } from './core/session.middleware'; import { ClientModule } from './client/client.module'; +import { ApplicationModule } from './application/application.module'; @Module({ imports: [ AuthModule, @@ -35,6 +36,7 @@ import { ClientModule } from './client/client.module'; retryDelay: 10000, }), }), + ApplicationModule, // TypeOrmModule.forRoot({ // type: 'mysql', // host: '85.215.137.185', // MySQL Hostname diff --git a/idp/src/application/application.controller.spec.ts b/idp/src/application/application.controller.spec.ts new file mode 100644 index 0000000..2c7cf77 --- /dev/null +++ b/idp/src/application/application.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApplicationController } from './application.controller'; + +describe('ApplicationController', () => { + let controller: ApplicationController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ApplicationController], + }).compile(); + + controller = module.get(ApplicationController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/idp/src/application/application.controller.ts b/idp/src/application/application.controller.ts new file mode 100644 index 0000000..8580015 --- /dev/null +++ b/idp/src/application/application.controller.ts @@ -0,0 +1,22 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { LoginUserDto } from 'src/model/dto'; +import { User } from 'src/model/user.entity'; +import { UsersService } from 'src/users/users.service'; + +@Controller('app') +export class ApplicationController { + constructor(private userService: UsersService) {} + @Get() + getIt() { + console.log('HERE'); + return { success: true }; + } + + @Post('login') + loginUser(@Body() b: LoginUserDto): Promise { + return this.userService.loginUser({ + username: b.username, + password: b.password, + }); + } +} diff --git a/idp/src/application/application.module.ts b/idp/src/application/application.module.ts new file mode 100644 index 0000000..8597d00 --- /dev/null +++ b/idp/src/application/application.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ApplicationController } from './application.controller'; +import { UserModule } from './user/user.module'; +import { LoggerModule } from 'src/core/logger.module'; +import { SecureModule } from 'src/core/secure/secure.module'; + +@Module({ + controllers: [ApplicationController], + providers: [], + imports: [LoggerModule, UserModule, SecureModule], +}) +export class ApplicationModule {} diff --git a/idp/src/application/user/user.controller.spec.ts b/idp/src/application/user/user.controller.spec.ts new file mode 100644 index 0000000..7057a1a --- /dev/null +++ b/idp/src/application/user/user.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserController } from './user.controller'; + +describe('UserController', () => { + let controller: UserController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UserController], + }).compile(); + + controller = module.get(UserController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/idp/src/application/user/user.controller.ts b/idp/src/application/user/user.controller.ts new file mode 100644 index 0000000..04ed321 --- /dev/null +++ b/idp/src/application/user/user.controller.ts @@ -0,0 +1,11 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { AuthGuard } from 'src/core/auth.guard'; + +@UseGuards(AuthGuard) +@Controller('app/user') +export class UserController { + @Get() + getIt() { + return 'secure'; + } +} diff --git a/idp/src/application/user/user.module.ts b/idp/src/application/user/user.module.ts new file mode 100644 index 0000000..994672f --- /dev/null +++ b/idp/src/application/user/user.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { UserController } from './user.controller'; +import { SecureModule } from 'src/core/secure/secure.module'; + +@Module({ + controllers: [UserController], + imports: [SecureModule], +}) +export class UserModule {} diff --git a/idp/src/auth/auth.controller.ts b/idp/src/auth/auth.controller.ts index 3087f55..94500db 100644 --- a/idp/src/auth/auth.controller.ts +++ b/idp/src/auth/auth.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Post, Query, Req } from '@nestjs/common'; +import { Body, Controller, Get, Post, Query } from '@nestjs/common'; import { UsersService } from 'src/users/users.service'; import { ClientService } from 'src/client/client.service'; import { FormDataRequest } from 'nestjs-form-data'; @@ -37,14 +37,17 @@ export class AuthController { @Post('login-with-session-id') async loginWithCode( - @Req() request: Request, @Body('code') code: string, @Body('client_id') clientId: string, ) { - console.log(request['session']); return this.usersService.loginWithSessionKey(code, clientId); } + @Post('login-with-session-id/userlogin') + async loginUserWithCode(@Body('code') code: string) { + return this.usersService.loginWithSessionKey(code, null, true); + } + @Get() async getClient( @Query('client_id') clientId: string, diff --git a/idp/src/core/auth.guard.ts b/idp/src/core/auth.guard.ts new file mode 100644 index 0000000..244eef1 --- /dev/null +++ b/idp/src/core/auth.guard.ts @@ -0,0 +1,47 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; +import { UsersService } from 'src/users/users.service'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor( + private readonly jwtService: JwtService, + private reflector: Reflector, + private readonly userService: UsersService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + + const authHeader = request.headers['authorization']; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new UnauthorizedException( + 'Authorization header is missing or invalid', + ); + } + + const token = authHeader.split(' ')[1]; + + try { + // Token validieren + const decoded = this.jwtService.verify(token); + const user = await this.userService.getUserById(decoded.id); + + if (!user) { + throw new UnauthorizedException('Verification not successfull'); + } + request.user = user; + + return true; + } catch (error) { + console.log(error); + throw new UnauthorizedException(error.message); + } + } +} diff --git a/idp/src/core/secure/secure.module.ts b/idp/src/core/secure/secure.module.ts new file mode 100644 index 0000000..7c54526 --- /dev/null +++ b/idp/src/core/secure/secure.module.ts @@ -0,0 +1,50 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NestjsFormDataModule } from 'nestjs-form-data'; +import { ClientService } from 'src/client/client.service'; +import { + AuthorizationCode, + AuthorizationCodeRepository, +} from 'src/model/auth-code.entity'; +import { Client, ClientRepository } from 'src/model/client.entity'; +import { RedirectUri } from 'src/model/redirect-uri.entity'; +import { SessionKey, SessionKeyRepository } from 'src/model/session-key.entity'; +import { User, UserRepository } from 'src/model/user.entity'; +import { UsersService } from 'src/users/users.service'; +import { AuthGuard } from '../auth.guard'; +import { LoggerModule } from '../logger.module'; + +@Module({ + imports: [ + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (config: ConfigService) => ({ + secret: config.get('JWT_SECRET'), + signOptions: { expiresIn: config.get('JWT_EXPIRES_IN') }, + }), + }), + NestjsFormDataModule, + TypeOrmModule.forFeature([ + User, + Client, + RedirectUri, + AuthorizationCode, + SessionKey, + ]), + LoggerModule, + ], + providers: [ + UsersService, + ClientService, + UserRepository, + ClientRepository, + AuthorizationCodeRepository, + SessionKeyRepository, + AuthGuard, + ], + exports: [JwtModule, UsersService, AuthGuard], +}) +export class SecureModule {} diff --git a/idp/src/model/dto/index.ts b/idp/src/model/dto/index.ts new file mode 100644 index 0000000..baa5dbd --- /dev/null +++ b/idp/src/model/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-user.dto'; +export * from './login-user.dto'; diff --git a/idp/src/model/dto/login-user.dto.ts b/idp/src/model/dto/login-user.dto.ts new file mode 100644 index 0000000..975f2e0 --- /dev/null +++ b/idp/src/model/dto/login-user.dto.ts @@ -0,0 +1,4 @@ +export interface LoginUserDto { + username: string; + password: string; +} diff --git a/idp/src/model/user.entity.ts b/idp/src/model/user.entity.ts index afa90db..a954929 100644 --- a/idp/src/model/user.entity.ts +++ b/idp/src/model/user.entity.ts @@ -44,6 +44,9 @@ export class User { @Exclude() @OneToMany(() => SessionKey, (key) => key.user) sessionKeys: SessionKey[]; + + accessToken?: string; + refreshToken?: string; } @Injectable() diff --git a/idp/src/users/users.service.ts b/idp/src/users/users.service.ts index 4d01ca0..dc4e33b 100644 --- a/idp/src/users/users.service.ts +++ b/idp/src/users/users.service.ts @@ -12,6 +12,7 @@ import { CustomLogger } from 'src/core/custom.logger'; import { CreateUserDto } from 'src/model/dto/create-user.dto'; import { SessionKeyRepository } from 'src/model/session-key.entity'; import { Client } from 'src/model/client.entity'; +import { LoginUserDto } from 'src/model/dto'; @Injectable() export class UsersService { @@ -48,7 +49,7 @@ export class UsersService { username: string, password: string, clientId: string, - ): Promise<{ code: string; session_key: string; access?: string }> { + ): Promise<{ code: string; session_key: string }> { const user = await this.userRepo.findByUsername(username); if (!user) { this.logger.error(`User ${username} not found`); @@ -74,9 +75,7 @@ export class UsersService { }); const session = await this.sessionRepo.save(s); - const refresh = this.createAccessToken(user); - - return { code: token.code, session_key: session.id, access: refresh }; + return { code: token.code, session_key: session.id }; } async createAuthToken(user: User, client: Client) { @@ -90,7 +89,11 @@ export class UsersService { return token; } - async loginWithSessionKey(sessionKey: string, clientId: string) { + async loginWithSessionKey( + sessionKey: string, + clientId: string, + getUserAccessToken = false, + ) { const client = await this.clientService.getClientById(clientId); if (!client) { @@ -113,9 +116,13 @@ export class UsersService { throw new HttpException('User is not active', 401); } + if (getUserAccessToken) { + user.accessToken = this.createAccessToken(user); + user.refreshToken = this.createRefreshToken(user); + return user; + } + const token = await this.createAuthToken(user, client); - const refresh = this.createAccessToken(user); - token.user['access'] = refresh; this.logger.log(`User logged in with code on client ${clientId}`); return token; } @@ -225,4 +232,28 @@ export class UsersService { throw new HttpException(e.message, 401); } } + + getUserById(id: string): Promise { + return this.userRepo.findById(id); + } + + async loginUser({ username, password }: LoginUserDto): Promise { + const user = await this.userRepo.findByUsername(username); + if (!user) { + this.logger.error(`User ${username} not found`); + throw new HttpException('Invalid credentials', 401); + } + + const valid = await bcrypt.compare(password, user.password); + if (!valid) { + this.logger.error(`User ${username} provided invalid credentials`); + throw new HttpException('Invalid credentials', 401); + } + + const access = this.createAccessToken(user); + const refresh = this.createRefreshToken(user); + user.accessToken = access; + user.refreshToken = refresh; + return user; + } } diff --git a/idp_client/src/app/auth/login/login.component.ts b/idp_client/src/app/auth/login/login.component.ts index f26ff09..ff35a2a 100644 --- a/idp_client/src/app/auth/login/login.component.ts +++ b/idp_client/src/app/auth/login/login.component.ts @@ -4,6 +4,8 @@ import { ActivatedRoute, Router } from '@angular/router'; import { CommonModule } from '@angular/common'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { HotToastService } from '@ngxpert/hot-toast'; +import { UserService } from '../user.service'; +import { User } from '../../model/user.interface'; @Component({ selector: 'app-login', @@ -17,6 +19,7 @@ export class LoginComponent { private route: ActivatedRoute = inject(ActivatedRoute); private toast: HotToastService = inject(HotToastService); private router: Router = inject(Router); + private userService: UserService = inject(UserService); redirectUri = null; client: string = ""; @@ -38,7 +41,9 @@ export class LoginComponent { const id = window.localStorage.getItem("auth_session_key"); if (!id ||id.length < 2) { return; } - this.http.post('api/auth/login-with-session-id', { + const url = this.client_id ? 'api/auth/login-with-session-id' : 'api/auth/login-with-session-id/userlogin' + + this.http.post(url, { code: id, client_id: this.client_id }).pipe( @@ -49,17 +54,7 @@ export class LoginComponent { }) ).subscribe({ next: (data) => { - if (data["code"] != null) { - if (this.redirectUri) { - location.href = this.redirectUri + "?code=" + data["code"]; - } else { - if (data['user'] && data['user']['access']) { - sessionStorage.setItem('access', data['user']['access']); - this.router.navigate(['dashboard']) - } - } - - } + this.handleLoginData(data); }, error: (error) => { console.error(error); @@ -67,12 +62,28 @@ export class LoginComponent { }); } + private handleLoginData(data: any) { + if (data["code"] != null) { + if (this.redirectUri) { + location.href = this.redirectUri + "?code=" + data["code"]; + } + } else if (data["id"] != null) { + this.userService.user = data as User; + this.navigateToDashboard(); + } + } + + private navigateToDashboard() { + this.router.navigateByUrl("/dashboard"); + } getclient() { const params = (this.route.snapshot.queryParamMap as any)["params"]; this.redirectUri = params.redirect_uri; this.client_id = params.client_id; + if (!this.client_id) { return; } + this.http.get('api/auth/', { params }).subscribe({ @@ -88,7 +99,9 @@ export class LoginComponent { login() { this.isLoading = true; - this.http.post('api/auth/login?'+ 'client_id=' + this.client_id, this.loginForm.value). + const url = this.client_id ? `api/auth/login?client_id=${this.client_id}` : 'api/app/login'; + console.log(url, this.client_id) + this.http.post(url, this.loginForm.value). pipe( this.toast.observe({ loading: 'Logging in...', @@ -98,17 +111,7 @@ export class LoginComponent { ) .subscribe({ next: (data) => { - if (data["code"] != null) { - window.localStorage.setItem("auth_session_key", data["session_key"]); - if (this.redirectUri) { - location.href = this.redirectUri + "?code=" + data["code"]; - } else { - if (data['user'] && data['user']['access']) { - sessionStorage.setItem('access', data['user']['access']); - this.router.navigate(['dashboard']) - } - } - } + this.handleLoginData(data); }, error: (error) => { console.error(error); diff --git a/idp_client/src/app/auth/user.service.spec.ts b/idp_client/src/app/auth/user.service.spec.ts new file mode 100644 index 0000000..3f804c9 --- /dev/null +++ b/idp_client/src/app/auth/user.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UserService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/idp_client/src/app/auth/user.service.ts b/idp_client/src/app/auth/user.service.ts new file mode 100644 index 0000000..45bd901 --- /dev/null +++ b/idp_client/src/app/auth/user.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { User } from '../model/user.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + private _user: User; + + constructor() { } + + + set user(user: User) { + this._user = user; + } + + get user(): User { + return this._user; + } +} diff --git a/idp_client/src/app/dashboard/dashboard.component.ts b/idp_client/src/app/dashboard/dashboard.component.ts index ebbda56..fb46c59 100644 --- a/idp_client/src/app/dashboard/dashboard.component.ts +++ b/idp_client/src/app/dashboard/dashboard.component.ts @@ -1,7 +1,6 @@ -import { HttpClient } from '@angular/common/http'; import { Component, inject, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { User } from '../model/user.interface'; +import { UserService } from '../auth/user.service'; +import { Router } from '@angular/router'; @Component({ selector: 'app-dashboard', @@ -11,42 +10,17 @@ import { User } from '../model/user.interface'; styleUrl: './dashboard.component.scss' }) export class DashboardComponent implements OnInit { - private router: Router = inject(Router); - private http: HttpClient = inject(HttpClient); - private user: User; - accessToken: string; + private userService: UserService = inject(UserService); + private router: Router = inject(Router); ngOnInit(): void { - - const refresh = sessionStorage.getItem('access'); - if (!refresh || refresh.length == 0) { - this.router.navigate(['login']); - } else { - this.accessToken = refresh; - this.refreshToken(refresh); - this.getClients(); + if (!this.userService.user) { + this.router.navigateByUrl("/login"); } - } - refreshToken(token: string) { - this.http.post('api/auth/verify', { access_token: token}).subscribe({ - next: res => { - console.log(res) - - } - }) - } - - - getClients() { - this.http.get('api/client', {headers: { auth: 'Bearer ' + this.accessToken}}).subscribe({ - next: result => { - console.log(result); - } - }) - } + } diff --git a/idp_client/src/app/model/user.interface.ts b/idp_client/src/app/model/user.interface.ts index 85f3604..9c6da37 100644 --- a/idp_client/src/app/model/user.interface.ts +++ b/idp_client/src/app/model/user.interface.ts @@ -4,4 +4,6 @@ export interface User { firstName: string; lastName: string; createdAt: string; + accessToken: string; + refreshToken: string; } \ No newline at end of file