From 095f33e16f824e6be1e41570a231479e87279596 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Sun, 25 Aug 2024 17:54:16 +0200 Subject: [PATCH] sessionkeys entered --- idp/src/auth/auth.controller.ts | 18 +++-- idp/src/auth/auth.module.ts | 10 ++- idp/src/model/dto/create-user.dto.ts | 7 ++ idp/src/model/session-key.entity.ts | 29 +++++++ idp/src/model/user.entity.ts | 13 +-- idp/src/users/users.service.ts | 58 ++++++++++++-- idp_client/src/app/app.routes.ts | 4 +- .../login.component.scss => auth/auth.scss} | 20 ++++- .../app/{ => auth}/login/login.component.html | 5 +- .../app/{ => auth}/login/login.component.ts | 38 +++++++-- .../app/auth/register/register.component.html | 40 ++++++++++ .../app/auth/register/register.component.ts | 80 +++++++++++++++++++ .../src/app/login/login.component.spec.ts | 23 ------ 13 files changed, 294 insertions(+), 51 deletions(-) create mode 100644 idp/src/model/dto/create-user.dto.ts create mode 100644 idp/src/model/session-key.entity.ts rename idp_client/src/app/{login/login.component.scss => auth/auth.scss} (91%) rename idp_client/src/app/{ => auth}/login/login.component.html (88%) rename idp_client/src/app/{ => auth}/login/login.component.ts (63%) create mode 100644 idp_client/src/app/auth/register/register.component.html create mode 100644 idp_client/src/app/auth/register/register.component.ts delete mode 100644 idp_client/src/app/login/login.component.spec.ts diff --git a/idp/src/auth/auth.controller.ts b/idp/src/auth/auth.controller.ts index 213ad66..65b3ad7 100644 --- a/idp/src/auth/auth.controller.ts +++ b/idp/src/auth/auth.controller.ts @@ -4,6 +4,7 @@ import { ClientService } from 'src/client/client.service'; import { FormDataRequest } from 'nestjs-form-data'; import { Client } from 'src/model/client.entity'; import { CustomLogger } from 'src/core/custom.logger'; +import { CreateUserDto } from 'src/model/dto/create-user.dto'; @Controller('auth') export class AuthController { @@ -14,11 +15,8 @@ export class AuthController { ) {} @Post('register') - async register( - @Body('username') username: string, - @Body('password') password: string, - ) { - const user = await this.usersService.createUser(username, password); + async register(@Body() userDto: CreateUserDto) { + const user = await this.usersService.createUser(userDto); return user; } @@ -37,6 +35,16 @@ export class AuthController { return token; } + @Post('login-with-session-id') + async loginWithCode( + @Body('code') code: string, + @Body('client_id') clientId: string, + ) { + const token = await this.usersService.loginWithSessionKey(code, clientId); + this.logger.log(`User logged in with code on client ${clientId}`); + return token; + } + @Get() async getClient( @Query('client_id') clientId: string, diff --git a/idp/src/auth/auth.module.ts b/idp/src/auth/auth.module.ts index e077629..2adee86 100644 --- a/idp/src/auth/auth.module.ts +++ b/idp/src/auth/auth.module.ts @@ -13,6 +13,7 @@ import { AuthorizationCodeRepository, } from 'src/model/auth-code.entity'; import { LoggerModule } from 'src/core/logger.module'; +import { SessionKey, SessionKeyRepository } from 'src/model/session-key.entity'; @Module({ providers: [ @@ -21,6 +22,7 @@ import { LoggerModule } from 'src/core/logger.module'; UserRepository, ClientRepository, AuthorizationCodeRepository, + SessionKeyRepository, ], controllers: [AuthController], imports: [ @@ -29,7 +31,13 @@ import { LoggerModule } from 'src/core/logger.module'; signOptions: { expiresIn: '15m' }, }), NestjsFormDataModule, - TypeOrmModule.forFeature([User, Client, RedirectUri, AuthorizationCode]), + TypeOrmModule.forFeature([ + User, + Client, + RedirectUri, + AuthorizationCode, + SessionKey, + ]), LoggerModule, ], }) diff --git a/idp/src/model/dto/create-user.dto.ts b/idp/src/model/dto/create-user.dto.ts new file mode 100644 index 0000000..4ca3100 --- /dev/null +++ b/idp/src/model/dto/create-user.dto.ts @@ -0,0 +1,7 @@ +export interface CreateUserDto { + username: string; + password: string; + clientId: string; + firstName: string; + lastName: string; +} diff --git a/idp/src/model/session-key.entity.ts b/idp/src/model/session-key.entity.ts new file mode 100644 index 0000000..f8a73d6 --- /dev/null +++ b/idp/src/model/session-key.entity.ts @@ -0,0 +1,29 @@ +import { + Entity, + PrimaryGeneratedColumn, + ManyToOne, + DataSource, + Repository, +} from 'typeorm'; +import { User } from './user.entity'; +import { Injectable } from '@nestjs/common'; + +@Entity() +export class SessionKey { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, (user) => user.sessionKeys, { eager: true }) + user: User; +} + +@Injectable() +export class SessionKeyRepository extends Repository { + constructor(dataSource: DataSource) { + super(SessionKey, dataSource.createEntityManager()); + } + + findById(id: string): Promise { + return this.findOneBy({ id }); + } +} diff --git a/idp/src/model/user.entity.ts b/idp/src/model/user.entity.ts index 82ac885..afa90db 100644 --- a/idp/src/model/user.entity.ts +++ b/idp/src/model/user.entity.ts @@ -1,8 +1,3 @@ -export interface CreateUserDto { - username: string; - password: string; -} - import { Injectable } from '@nestjs/common'; import { Exclude } from 'class-transformer'; import { @@ -15,13 +10,14 @@ import { CreateDateColumn, } from 'typeorm'; import { AuthorizationCode } from './auth-code.entity'; +import { SessionKey } from './session-key.entity'; @Entity() export class User { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ unique: true }) username: string; @Exclude() @@ -41,8 +37,13 @@ export class User { @Column({ default: true }) isActive: boolean; + @Exclude() @OneToMany(() => AuthorizationCode, (code) => code.user) authorizationCodes: AuthorizationCode[]; + + @Exclude() + @OneToMany(() => SessionKey, (key) => key.user) + sessionKeys: SessionKey[]; } @Injectable() diff --git a/idp/src/users/users.service.ts b/idp/src/users/users.service.ts index ccef5f1..f0faddb 100644 --- a/idp/src/users/users.service.ts +++ b/idp/src/users/users.service.ts @@ -9,6 +9,9 @@ import { } from 'src/model/auth-code.entity'; import { JwtService } from '@nestjs/jwt'; 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'; @Injectable() export class UsersService { @@ -17,20 +20,27 @@ export class UsersService { private readonly jwtService: JwtService, private userRepo: UserRepository, private tokenRepo: AuthorizationCodeRepository, + private sessionRepo: SessionKeyRepository, private logger: CustomLogger, ) {} - async createUser(username: string, password: string): Promise { - const hashedPassword = await bcrypt.hash(password, 10); + async createUser(userDto: CreateUserDto): Promise { + const hashedPassword = await bcrypt.hash(userDto.password, 10); const user: User = { id: uuidv4(), - username, + username: userDto.username, password: hashedPassword, isActive: true, authorizationCodes: [], + firstName: userDto.firstName, + lastName: userDto.lastName, + sessionKeys: [], }; const u = this.userRepo.create(user); const uu = await this.userRepo.save(u); + this.logger.log( + `User ${userDto.username} created for client ${userDto.clientId}`, + ); return uu; } @@ -38,7 +48,7 @@ export class UsersService { username: string, password: string, clientId: string, - ): Promise<{ code: string }> { + ): Promise<{ code: string; session_key: string }> { const user = await this.userRepo.findByUsername(username); if (!user) { this.logger.error(`User ${username} not found`); @@ -57,7 +67,17 @@ export class UsersService { this.logger.error(`Client ${clientId} not found`); throw new HttpException('Invalid client', 401); } + const token = await this.createAuthToken(user, client); + const s = this.sessionRepo.create({ + user, + }); + const session = await this.sessionRepo.save(s); + + return { code: token.code, session_key: session.id }; + } + + async createAuthToken(user: User, client: Client) { const token: AuthorizationCode = { code: uuidv4(), client, @@ -65,7 +85,35 @@ export class UsersService { }; const t = this.tokenRepo.create(token); await this.tokenRepo.save(t); - return { code: token.code }; + return token; + } + + async loginWithSessionKey(sessionKey: string, clientId: string) { + const client = await this.clientService.getClientById(clientId); + + if (!client) { + this.logger.error(`Client ${clientId} not found`); + throw new HttpException('Invalid client', 401); + } + + const session = await this.sessionRepo.findOneByOrFail({ id: sessionKey }); + if (!session) { + throw new HttpException('Invalid session key', 401); + } + + const user = await this.userRepo.findById(session.user.id); + + if (!user) { + throw new HttpException('Invalid session key', 401); + } + + if (!user.isActive) { + throw new HttpException('User is not active', 401); + } + + const token = this.createAuthToken(user, client); + + return token; } async verifyToken({ clientId, clientSecret, code, grantType }: any) { diff --git a/idp_client/src/app/app.routes.ts b/idp_client/src/app/app.routes.ts index e234520..84377bd 100644 --- a/idp_client/src/app/app.routes.ts +++ b/idp_client/src/app/app.routes.ts @@ -1,7 +1,9 @@ import { Routes } from '@angular/router'; -import { LoginComponent } from './login/login.component'; +import { LoginComponent } from './auth/login/login.component'; +import { RegisterComponent } from './auth/register/register.component'; export const routes: Routes = [ { path: 'login', component: LoginComponent }, + { path: 'register', component: RegisterComponent }, { path: '', component: LoginComponent }, ]; diff --git a/idp_client/src/app/login/login.component.scss b/idp_client/src/app/auth/auth.scss similarity index 91% rename from idp_client/src/app/login/login.component.scss rename to idp_client/src/app/auth/auth.scss index bea0c25..59164ef 100644 --- a/idp_client/src/app/login/login.component.scss +++ b/idp_client/src/app/auth/auth.scss @@ -144,6 +144,12 @@ body { box-shadow: 0px 2px 2px #5C5696; cursor: pointer; transition: .2s; + + &:disabled { + background: #e3e3e3; + color: grey; + pointer-events: none; + } } .login__submit:active, @@ -184,4 +190,16 @@ body { .social-login__icon:hover { transform: scale(1.5); -} \ No newline at end of file +} + +.login-register { + margin-top: 24px; + color: white; + text-align: end; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + diff --git a/idp_client/src/app/login/login.component.html b/idp_client/src/app/auth/login/login.component.html similarity index 88% rename from idp_client/src/app/login/login.component.html rename to idp_client/src/app/auth/login/login.component.html index 9690b21..15dd8a2 100644 --- a/idp_client/src/app/login/login.component.html +++ b/idp_client/src/app/auth/login/login.component.html @@ -11,10 +11,11 @@ - + +
diff --git a/idp_client/src/app/login/login.component.ts b/idp_client/src/app/auth/login/login.component.ts similarity index 63% rename from idp_client/src/app/login/login.component.ts rename to idp_client/src/app/auth/login/login.component.ts index 2c1cd7e..aa6cbd4 100644 --- a/idp_client/src/app/login/login.component.ts +++ b/idp_client/src/app/auth/login/login.component.ts @@ -1,9 +1,9 @@ import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { CommonModule } from '@angular/common'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { environment } from '../../environments/environment'; +import { environment } from '../../../environments/environment'; import { HotToastService } from '@ngxpert/hot-toast'; @Component({ @@ -11,12 +11,13 @@ import { HotToastService } from '@ngxpert/hot-toast'; standalone: true, imports: [CommonModule, FormsModule, ReactiveFormsModule], templateUrl: './login.component.html', - styleUrl: './login.component.scss' + styleUrl: '../auth.scss' }) export class LoginComponent { private http: HttpClient = inject(HttpClient); private route: ActivatedRoute = inject(ActivatedRoute); private toast: HotToastService = inject(HotToastService); + private router: Router = inject(Router); redirectUri = null; client: string = ""; @@ -29,6 +30,27 @@ export class LoginComponent { constructor() { this.getclient(); + this.loginWithSessionId(); + } + + loginWithSessionId() { + const id = window.localStorage.getItem("auth_sesion_key"); + if (!id ||id.length < 2) { return; } + + this.toast.loading('Logging in...'); + this.http.post(environment.api_url + 'auth/login-with-session-id', { + code: id, + client_id: this.client_id + }).subscribe({ + next: (data) => { + if (data["code"] != null) { + location.href = this.redirectUri + "?code=" + data["code"]; + } + }, + error: (error) => { + console.error(error); + } + }); } @@ -36,15 +58,11 @@ export class LoginComponent { const params = (this.route.snapshot.queryParamMap as any)["params"]; this.redirectUri = params.redirect_uri; this.client_id = params.client_id; - this.route.snapshot.queryParamMap.keys.forEach((key) => { - console.log(key, this.route.snapshot.queryParamMap.get(key)); - }); this.http.get(environment.api_url + 'auth/', { params }).subscribe({ next: (client) => { - console.log(client) this.client = client.clientName; }, error: (error) => { @@ -55,9 +73,11 @@ export class LoginComponent { } login() { + this.toast.loading('Logging in...'); this.http.post(environment.api_url + 'auth/login?'+ 'client_id=' + this.client_id, this.loginForm.value).subscribe({ next: (data) => { if (data["code"] != null) { + window.localStorage.setItem("auth_sesion_key", data["session_key"]); location.href = this.redirectUri + "?code=" + data["code"]; } }, @@ -67,4 +87,8 @@ export class LoginComponent { } }) } + + toRegister() { + this.router.navigate(['/register'], { queryParams: this.route.snapshot.queryParams }); + } } diff --git a/idp_client/src/app/auth/register/register.component.html b/idp_client/src/app/auth/register/register.component.html new file mode 100644 index 0000000..68529cb --- /dev/null +++ b/idp_client/src/app/auth/register/register.component.html @@ -0,0 +1,40 @@ +
+
+
+

{{ client }}

+ +
+
+ + + + +
+
+
\ No newline at end of file diff --git a/idp_client/src/app/auth/register/register.component.ts b/idp_client/src/app/auth/register/register.component.ts new file mode 100644 index 0000000..5d3dab0 --- /dev/null +++ b/idp_client/src/app/auth/register/register.component.ts @@ -0,0 +1,80 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, inject } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { HotToastService } from '@ngxpert/hot-toast'; +import { environment } from '../../../environments/environment'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-register', + standalone: true, + imports: [CommonModule, FormsModule, ReactiveFormsModule], + templateUrl: './register.component.html', + styleUrl: '../auth.scss' +}) +export class RegisterComponent { + private http: HttpClient = inject(HttpClient); + private route: ActivatedRoute = inject(ActivatedRoute); + private toast: HotToastService = inject(HotToastService); + private router: Router = inject(Router); + + redirectUri = null; + client: string = ""; + client_id = null; + + registerForm = new FormGroup({ + username: new FormControl('', [Validators.required, Validators.email, Validators.maxLength(100)]), + password: new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(20)]), + repeatPassword: new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(20)]), + firstName: new FormControl('', [Validators.required, Validators.maxLength(100)]), + lastName: new FormControl('', [Validators.required, Validators.maxLength(100)]), + }) + + constructor() { + this.getclient(); + } + + + getclient() { + const params = (this.route.snapshot.queryParamMap as any)["params"]; + this.redirectUri = params.redirect_uri; + this.client_id = params.client_id; + this.http.get(environment.api_url + 'auth/', { + params + }).subscribe({ + next: (client) => { + this.client = client.clientName; + }, + error: (error) => { + console.error(error); + this.toast.error('Invalid client'); + } + }) + } + + register() { + if (this.registerForm.value.password != this.registerForm.value.repeatPassword) { + this.toast.error('Passwords do not match'); + return; + } + this.http.post(environment.api_url + 'auth/register?'+ 'client_id=' + this.client_id, this.registerForm.value).pipe( + this.toast.observe({ + loading: 'Registering...', + success: 'Registration successfull' + }) + ).subscribe({ + next: () => { + this.router.navigate(['/login'], { queryParams: this.route.snapshot.queryParams }); + }, + error: (error) => { + console.error(error); + this.toast.error('Registration not successfull'); + } + }) + } + + toLogin() { + this.router.navigate(['/login'], { queryParams: this.route.snapshot.queryParams }); + } +} diff --git a/idp_client/src/app/login/login.component.spec.ts b/idp_client/src/app/login/login.component.spec.ts deleted file mode 100644 index 18f3685..0000000 --- a/idp_client/src/app/login/login.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { LoginComponent } from './login.component'; - -describe('LoginComponent', () => { - let component: LoginComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [LoginComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(LoginComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -});