From 249d00177ce044306b461cefe17dadbeffa559f8 Mon Sep 17 00:00:00 2001 From: Pierre Tellier Date: Tue, 11 Feb 2025 10:00:11 +0100 Subject: [PATCH 1/4] feat: interraction between the backend and keycloak --- .../enseirb/myinpulse/api/GetUserInfo.java | 11 +-- .../exceptions/RoleNotFoudException.java | 7 ++ .../exceptions/UserNotFoundException.java | 7 ++ .../security/KeycloakJwtRolesConverter.java | 11 ++- .../enseirb/myinpulse/utils/KeycloakApi.java | 77 +++++++++++++++++++ front/MyINPulse-front/src/services/api.ts | 3 +- front/MyINPulse-front/src/views/test.vue | 73 ++++++++++++++++++ 7 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 MyINPulse-back/src/main/java/enseirb/myinpulse/exceptions/RoleNotFoudException.java create mode 100644 MyINPulse-back/src/main/java/enseirb/myinpulse/exceptions/UserNotFoundException.java create mode 100644 MyINPulse-back/src/main/java/enseirb/myinpulse/utils/KeycloakApi.java create mode 100644 front/MyINPulse-front/src/views/test.vue diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/api/GetUserInfo.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/api/GetUserInfo.java index 50262e1..3b4f04a 100644 --- a/MyINPulse-back/src/main/java/enseirb/myinpulse/api/GetUserInfo.java +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/api/GetUserInfo.java @@ -2,13 +2,13 @@ package enseirb.myinpulse.api; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.*; +import javax.management.relation.RoleNotFoundException; import java.security.Principal; + + @SpringBootApplication @RestController public class GetUserInfo { @@ -22,7 +22,8 @@ public class GetUserInfo { @CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS}) @GetMapping("/random") - public boolean rand(){ + public boolean rand(@RequestHeader("Authorization") String token) throws RoleNotFoundException { + System.err.println(token); System.err.println("HELLO"); return Math.random() > 0.5; } diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/exceptions/RoleNotFoudException.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/exceptions/RoleNotFoudException.java new file mode 100644 index 0000000..22e4c23 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/exceptions/RoleNotFoudException.java @@ -0,0 +1,7 @@ +package enseirb.myinpulse.exceptions; + +public class RoleNotFoudException extends RuntimeException { + public RoleNotFoudException(String message) { + super(message); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/exceptions/UserNotFoundException.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/exceptions/UserNotFoundException.java new file mode 100644 index 0000000..983e766 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/exceptions/UserNotFoundException.java @@ -0,0 +1,7 @@ +package enseirb.myinpulse.exceptions; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/security/KeycloakJwtRolesConverter.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/security/KeycloakJwtRolesConverter.java index fafbef5..08c5cab 100644 --- a/MyINPulse-back/src/main/java/enseirb/myinpulse/security/KeycloakJwtRolesConverter.java +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/security/KeycloakJwtRolesConverter.java @@ -1,3 +1,7 @@ +/* + * Source: https://github.com/ChristianHuff-DEV/secure-spring-rest-api-using-keycloak/blob/main/src/main/java/io/betweendata/RestApi/security/oauth2/KeycloakJwtRolesConverter.java + * edited by Pierre Tellier + */ package enseirb.myinpulse.security; import org.springframework.core.convert.converter.Converter; @@ -41,17 +45,16 @@ public class KeycloakJwtRolesConverter implements Converter TEMPORARNAME(Jwt jwt) { + public Collection tokenRolesExtractor(Jwt jwt) { // Collection that will hold the extracted roles Collection grantedAuthorities = new ArrayList<>(); diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/KeycloakApi.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/KeycloakApi.java new file mode 100644 index 0000000..e91e9e1 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/KeycloakApi.java @@ -0,0 +1,77 @@ +package enseirb.myinpulse.utils; + +import enseirb.myinpulse.exceptions.UserNotFoundException; +import org.springframework.web.client.RestClient; + +import javax.management.relation.RoleNotFoundException; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +public class KeycloakApi { + + static final String keycloakUrl = "http://localhost:7080"; + static final String realmName = "test"; + + /** + * Uses Keycloak API to retrieve a role representation of a role by its name + * @param roleName name of the role + * @param bearer authorization header used by the client to authenticate to keycloak + */ + static public RoleRepresentation getRoleRepresentationByName(String roleName, String bearer) throws RoleNotFoundException { + RoleRepresentation[] response = RestClient.builder().baseUrl(keycloakUrl) + .defaultHeader("Authorization", bearer) + .build() + .get() + .uri("/admin/realms/{realmName}/roles/{roleName}", realmName, roleName) + .retrieve() + .body(RoleRepresentation[].class); + + if (response == null || response.length == 0) { + throw new RoleNotFoundException("Role not found"); + } + return response[0]; + } + + static public String getUserIdByName(String username, String bearer) throws UserNotFoundException { + UserRepresentation[] response = RestClient.builder().baseUrl(keycloakUrl) + .defaultHeader("Authorization", bearer) + .build() + .get() + .uri("/admin/realms/{realmName}/users?username={username}", realmName, username) + .retrieve() + .body(UserRepresentation[].class); + + if (response == null || response.length == 0) { + throw new UserNotFoundException("User not found"); + } + return response[0].id; + } + + static public void setRoleToUser(String username, String roleName, String bearer) throws RoleNotFoundException, UserNotFoundException { + RoleRepresentation roleRepresentation = getRoleRepresentationByName(roleName, bearer); + String userId = getUserIdByName(username, bearer); + + + RestClient.builder().baseUrl(keycloakUrl) + .defaultHeader("Authorization", bearer) + .build() + .post() + .uri("/admin/realms/${realmName}/users/${userId}/role-mappings/realm", realmName, userId) + .body(roleRepresentation) + .contentType(APPLICATION_JSON) + .retrieve(); + } +} + + +class RoleRepresentation { + public String id; + public String name; + public String description; +} + +class UserRepresentation { + public String id; + public String name; +} + diff --git a/front/MyINPulse-front/src/services/api.ts b/front/MyINPulse-front/src/services/api.ts index 091455c..5c4fc7b 100644 --- a/front/MyINPulse-front/src/services/api.ts +++ b/front/MyINPulse-front/src/services/api.ts @@ -14,7 +14,8 @@ axiosInstance.interceptors.response.use( async (error) => { const originalRequest = error.config; if ( - error.response.status === 401 && + ((error.response && error.response.status === 401) || + error.code == "ERR_NETWORK") && !originalRequest._retry && store.authenticated ) { diff --git a/front/MyINPulse-front/src/views/test.vue b/front/MyINPulse-front/src/views/test.vue new file mode 100644 index 0000000..9d98958 --- /dev/null +++ b/front/MyINPulse-front/src/views/test.vue @@ -0,0 +1,73 @@ + + + + + From 6235fe7e6811b6f89ed2d662791cc1d6a04d12e2 Mon Sep 17 00:00:00 2001 From: Pierre Tellier Date: Tue, 18 Feb 2025 12:07:07 +0100 Subject: [PATCH 2/4] feat: separated class definition --- .../enseirb/myinpulse/utils/KeycloakApi.java | 77 ------------------- .../datatypes/RoleRepresentation.java | 7 ++ .../datatypes/UserRepresentation.java | 6 ++ 3 files changed, 13 insertions(+), 77 deletions(-) delete mode 100644 MyINPulse-back/src/main/java/enseirb/myinpulse/utils/KeycloakApi.java create mode 100644 MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/datatypes/RoleRepresentation.java create mode 100644 MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/datatypes/UserRepresentation.java diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/KeycloakApi.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/KeycloakApi.java deleted file mode 100644 index e91e9e1..0000000 --- a/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/KeycloakApi.java +++ /dev/null @@ -1,77 +0,0 @@ -package enseirb.myinpulse.utils; - -import enseirb.myinpulse.exceptions.UserNotFoundException; -import org.springframework.web.client.RestClient; - -import javax.management.relation.RoleNotFoundException; - -import static org.springframework.http.MediaType.APPLICATION_JSON; - -public class KeycloakApi { - - static final String keycloakUrl = "http://localhost:7080"; - static final String realmName = "test"; - - /** - * Uses Keycloak API to retrieve a role representation of a role by its name - * @param roleName name of the role - * @param bearer authorization header used by the client to authenticate to keycloak - */ - static public RoleRepresentation getRoleRepresentationByName(String roleName, String bearer) throws RoleNotFoundException { - RoleRepresentation[] response = RestClient.builder().baseUrl(keycloakUrl) - .defaultHeader("Authorization", bearer) - .build() - .get() - .uri("/admin/realms/{realmName}/roles/{roleName}", realmName, roleName) - .retrieve() - .body(RoleRepresentation[].class); - - if (response == null || response.length == 0) { - throw new RoleNotFoundException("Role not found"); - } - return response[0]; - } - - static public String getUserIdByName(String username, String bearer) throws UserNotFoundException { - UserRepresentation[] response = RestClient.builder().baseUrl(keycloakUrl) - .defaultHeader("Authorization", bearer) - .build() - .get() - .uri("/admin/realms/{realmName}/users?username={username}", realmName, username) - .retrieve() - .body(UserRepresentation[].class); - - if (response == null || response.length == 0) { - throw new UserNotFoundException("User not found"); - } - return response[0].id; - } - - static public void setRoleToUser(String username, String roleName, String bearer) throws RoleNotFoundException, UserNotFoundException { - RoleRepresentation roleRepresentation = getRoleRepresentationByName(roleName, bearer); - String userId = getUserIdByName(username, bearer); - - - RestClient.builder().baseUrl(keycloakUrl) - .defaultHeader("Authorization", bearer) - .build() - .post() - .uri("/admin/realms/${realmName}/users/${userId}/role-mappings/realm", realmName, userId) - .body(roleRepresentation) - .contentType(APPLICATION_JSON) - .retrieve(); - } -} - - -class RoleRepresentation { - public String id; - public String name; - public String description; -} - -class UserRepresentation { - public String id; - public String name; -} - diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/datatypes/RoleRepresentation.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/datatypes/RoleRepresentation.java new file mode 100644 index 0000000..a6252a9 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/datatypes/RoleRepresentation.java @@ -0,0 +1,7 @@ +package enseirb.myinpulse.utils.keycloak.datatypes; + +public class RoleRepresentation { + public String id; + public String name; + public String description; +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/datatypes/UserRepresentation.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/datatypes/UserRepresentation.java new file mode 100644 index 0000000..f2c3522 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/datatypes/UserRepresentation.java @@ -0,0 +1,6 @@ +package enseirb.myinpulse.utils.keycloak.datatypes; + +public class UserRepresentation { + public String id; + public String name; +} From 86e7dc7c757412cc22be9f055887992f06b7b76a Mon Sep 17 00:00:00 2001 From: Pierre Tellier Date: Tue, 18 Feb 2025 16:37:55 +0100 Subject: [PATCH 3/4] fix: removed the test file that was causing the linter to fail --- front/MyINPulse-front/src/views/test.vue | 73 ------------------------ 1 file changed, 73 deletions(-) delete mode 100644 front/MyINPulse-front/src/views/test.vue diff --git a/front/MyINPulse-front/src/views/test.vue b/front/MyINPulse-front/src/views/test.vue deleted file mode 100644 index 9d98958..0000000 --- a/front/MyINPulse-front/src/views/test.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - - - From 5e8e875a37c26bb7672b27801b6ad5ff2fb2bb02 Mon Sep 17 00:00:00 2001 From: Pierre Tellier Date: Tue, 18 Feb 2025 16:45:41 +0100 Subject: [PATCH 4/4] feat: added user deletion and custom api call in the frontend --- .../myinpulse/utils/keycloak/KeycloakApi.java | 135 ++++++++++++++++++ .../src/views/testComponent.vue | 11 ++ 2 files changed, 146 insertions(+) create mode 100644 MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/KeycloakApi.java diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/KeycloakApi.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/KeycloakApi.java new file mode 100644 index 0000000..ff6dde2 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/utils/keycloak/KeycloakApi.java @@ -0,0 +1,135 @@ +package enseirb.myinpulse.utils.keycloak; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +import enseirb.myinpulse.exceptions.UserNotFoundException; +import enseirb.myinpulse.utils.keycloak.datatypes.RoleRepresentation; +import enseirb.myinpulse.utils.keycloak.datatypes.UserRepresentation; + +import org.springframework.web.client.RestClient; + +import javax.management.relation.RoleNotFoundException; + +public class KeycloakApi { + + static final String keycloakUrl; + static final String realmName; + + static { + if (System.getenv("VITE_KEYCLOAK_URL") == null) { + System.exit(-1); + } + keycloakUrl = System.getenv("VITE_KEYCLOAK_URL"); + } + + static { + if (System.getenv("VITE_KEYCLOAK_REALM") == null) { + System.exit(-1); + } + realmName = System.getenv("VITE_KEYCLOAK_REALM"); + } + + /** + * Uses Keycloak API to retrieve a role representation of a role by its name + * + * @param roleName name of the role + * @param bearer authorization header used by the client to authenticate to keycloak + */ + public static RoleRepresentation getRoleRepresentationByName(String roleName, String bearer) + throws RoleNotFoundException { + RoleRepresentation[] response = + RestClient.builder() + .baseUrl(keycloakUrl) + .defaultHeader("Authorization", bearer) + .build() + .get() + .uri("/admin/realms/{realmName}/roles/{roleName}", realmName, roleName) + .retrieve() + .body(RoleRepresentation[].class); + + if (response == null || response.length == 0) { + throw new RoleNotFoundException("Role not found"); + } + return response[0]; + } + + /** + * Use keycloak API to to retreive a userID via his name or email. + * + * @param username username or mail of the user + * @param bearer bearer of the user, allowing access to database + * @return the userid, as a String + * @throws UserNotFoundException + */ + public static String getUserIdByName(String username, String bearer) + throws UserNotFoundException { + UserRepresentation[] response = + RestClient.builder() + .baseUrl(keycloakUrl) + .defaultHeader("Authorization", bearer) + .build() + .get() + .uri( + "/admin/realms/{realmName}/users?username={username}", + realmName, + username) + .retrieve() + .body(UserRepresentation[].class); + + if (response == null || response.length == 0) { + throw new UserNotFoundException("User not found"); + } + return response[0].id; + } + + /** + * TODO: check for error + * + *

Set a keycloak role to a keycloak user. + * + *

Usual roles should be `MyINPulse-admin` and `MyINPulse-entrepreneur` + * + * @param username + * @param roleName + * @param bearer + * @throws RoleNotFoundException + * @throws UserNotFoundException + */ + public static void setRoleToUser(String username, String roleName, String bearer) + throws RoleNotFoundException, UserNotFoundException { + RoleRepresentation roleRepresentation = getRoleRepresentationByName(roleName, bearer); + String userId = getUserIdByName(username, bearer); + + RestClient.builder() + .baseUrl(keycloakUrl) + .defaultHeader("Authorization", bearer) + .build() + .post() + .uri( + "/admin/realms/${realmName}/users/${userId}/role-mappings/realm", + realmName, + userId) + .body(roleRepresentation) + .contentType(APPLICATION_JSON) + .retrieve(); + } + + /** + * Delete a user from Keycloak database. TODO: check the bearer permission. + * + * @param username + * @param bearer + * @throws UserNotFoundException + */ + public static void deleteUser(String username, String bearer) throws UserNotFoundException { + String userId = getUserIdByName(username, bearer); + + RestClient.builder() + .baseUrl(keycloakUrl) + .defaultHeader("Authorization", bearer) + .build() + .delete() + .uri("/admin/realms/${realmName}/users/${userId}", realmName, userId) + .retrieve(); + } +} diff --git a/front/MyINPulse-front/src/views/testComponent.vue b/front/MyINPulse-front/src/views/testComponent.vue index 9ba1d5a..a73d879 100644 --- a/front/MyINPulse-front/src/views/testComponent.vue +++ b/front/MyINPulse-front/src/views/testComponent.vue @@ -1,6 +1,9 @@