diff --git a/backend/codiki-launcher/src/main/resources/application-local.yml b/backend/codiki-launcher/src/main/resources/application-local.yml index 89d5304..be47a7a 100644 --- a/backend/codiki-launcher/src/main/resources/application-local.yml +++ b/backend/codiki-launcher/src/main/resources/application-local.yml @@ -2,7 +2,11 @@ application: pictures: path: /home/florian/Developpement/codiki-hexagonal/backend/pictures-folder/ temp-path : /home/florian/Developpement/codiki-hexagonal/backend/pictures-folder/temp/ - + security: + jwt: + expirationDelayInMinutes: 1 + refreshToken: + expirationDelayInDays: 7 server: port: 8987 diff --git a/backend/rest-client-collection/Codiki/Users/Refresh token.bru b/backend/rest-client-collection/Codiki/Users/Refresh token.bru new file mode 100644 index 0000000..1f4d414 --- /dev/null +++ b/backend/rest-client-collection/Codiki/Users/Refresh token.bru @@ -0,0 +1,11 @@ +meta { + name: Refresh token + type: http + seq: 4 +} + +post { + url: {{url}}/api/users/refresh-token + body: none + auth: none +} diff --git a/frontend/src/app/core/interceptor/jwt.interceptor.ts b/frontend/src/app/core/interceptor/jwt.interceptor.ts index 84e9da1..6165609 100644 --- a/frontend/src/app/core/interceptor/jwt.interceptor.ts +++ b/frontend/src/app/core/interceptor/jwt.interceptor.ts @@ -8,6 +8,10 @@ export class JwtInterceptor implements HttpInterceptor { private readonly authenticationService = inject(AuthenticationService); intercept(request: HttpRequest, next: HttpHandler): Observable> { + if (this.authenticationService.isTokenExpired()) { + this.authenticationService.renewToken(); + } + const jwt = this.authenticationService.getToken(); if (jwt) { @@ -16,6 +20,8 @@ export class JwtInterceptor implements HttpInterceptor { }); return next.handle(cloned); + } else { + this.authenticationService.unauthenticate(); } return next.handle(request); diff --git a/frontend/src/app/core/rest-services/user/model/refresh-token.model.ts b/frontend/src/app/core/rest-services/user/model/refresh-token.model.ts new file mode 100644 index 0000000..818e225 --- /dev/null +++ b/frontend/src/app/core/rest-services/user/model/refresh-token.model.ts @@ -0,0 +1,3 @@ +export interface RefreshTokenRequest { + refreshTokenValue: string; +} \ No newline at end of file diff --git a/frontend/src/app/core/rest-services/user/user.rest-service.ts b/frontend/src/app/core/rest-services/user/user.rest-service.ts index 2aeb9ea..c89f47d 100644 --- a/frontend/src/app/core/rest-services/user/user.rest-service.ts +++ b/frontend/src/app/core/rest-services/user/user.rest-service.ts @@ -3,6 +3,7 @@ import { Injectable, inject } from "@angular/core"; import { LoginRequest, LoginResponse } from "./model/login.model"; import { lastValueFrom } from "rxjs"; import { SigninRequest } from "./model/signin.model"; +import { RefreshTokenRequest } from "./model/refresh-token.model"; @Injectable({ providedIn: 'root' @@ -17,4 +18,8 @@ export class UserRestService { signin(request: SigninRequest): Promise { return lastValueFrom(this.httpClient.post('/api/users', request)); } + + refreshToken(request: RefreshTokenRequest): Promise { + return lastValueFrom(this.httpClient.post('/api/users/refresh-token', request)); + } } \ No newline at end of file diff --git a/frontend/src/app/core/service/authentication.service.ts b/frontend/src/app/core/service/authentication.service.ts index 9361672..c4b1fbd 100644 --- a/frontend/src/app/core/service/authentication.service.ts +++ b/frontend/src/app/core/service/authentication.service.ts @@ -1,7 +1,10 @@ -import { Injectable } from "@angular/core"; +import { inject, Injectable } from "@angular/core"; import { User } from "../model/User"; +import { UserRestService } from "../rest-services/user/user.rest-service"; +import { RefreshTokenRequest } from "../rest-services/user/model/refresh-token.model"; const JWT_PARAM = 'jwt'; +const REFRESH_TOKEN_PARAM = 'refresh-token'; interface UserDetails { sub: string; @@ -15,8 +18,11 @@ interface UserDetails { providedIn: 'root' }) export class AuthenticationService { - authenticate(token: string): void { + private userRestService = inject(UserRestService); + + authenticate(token: string, refreshToken: string): void { localStorage.setItem(JWT_PARAM, token); + localStorage.setItem(REFRESH_TOKEN_PARAM, refreshToken); } unauthenticate(): void { @@ -44,6 +50,34 @@ export class AuthenticationService { return localStorage.getItem(JWT_PARAM) ?? undefined; } + isTokenExpired(): boolean { + let result = false; + + const userDetails = this.extractUserDetails(); + + if (userDetails) { + const expirationDate = new Date(userDetails.exp * 1000); + const now = new Date(); + + result = expirationDate < now; + } + + return result; + } + + renewToken(): void { + const refreshToken = localStorage.getItem(REFRESH_TOKEN_PARAM); + if (refreshToken) { + const request: RefreshTokenRequest = { + refreshTokenValue: refreshToken + }; + this.userRestService.refreshToken(request) + .then(refreshTokenResponse => { + this.authenticate(refreshTokenResponse.accessToken, refreshTokenResponse.refreshToken); + }); + } + } + private extractUserFromLocalStorage(): User | undefined { let result: User | undefined = undefined; diff --git a/frontend/src/app/pages/login/login.service.ts b/frontend/src/app/pages/login/login.service.ts index 209f90a..c423831 100644 --- a/frontend/src/app/pages/login/login.service.ts +++ b/frontend/src/app/pages/login/login.service.ts @@ -65,7 +65,7 @@ export class LoginService { this.userRestService .login(state.request) .then((response) => { - this.authenticationService.authenticate(response.accessToken); + this.authenticationService.authenticate(response.accessToken, response.refreshToken); this.snackBar.open('Authentication succeeded!', 'Close', { duration: 5000, });