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 ad008f0..7baea8e 100644 --- a/MyINPulse-back/src/main/java/enseirb/myinpulse/api/GetUserInfo.java +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/api/GetUserInfo.java @@ -1,14 +1,32 @@ package enseirb.myinpulse.api; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import javax.management.relation.RoleNotFoundException; +import java.security.Principal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; + + @SpringBootApplication @RestController public class GetUserInfo { + // TODO: understand how to get data + @GetMapping("/getUserInfo") + public Object user(Principal principal) { + System.out.println("GetUserInfo + " + principal); + System.out.println(SecurityContextHolder.getContext().getAuthentication()); + return SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + } + + @CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS}) @GetMapping("/unauth/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 c73b82a..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,7 +1,9 @@ +/* + * 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 static java.util.stream.Collectors.toSet; - import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -16,37 +18,43 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; -public class KeycloakJwtRolesConverter implements Converter { - /** Prefix used for realm level roles. */ - public static final String PREFIX_REALM_ROLE = "ROLE_REALM_"; +import static java.util.stream.Collectors.toSet; - /** Prefix used in combination with the resource (client) name for resource level roles. */ + +public class KeycloakJwtRolesConverter implements Converter { + /** + * Prefix used for realm level roles. + */ + public static final String PREFIX_REALM_ROLE = "ROLE_REALM_"; + /** + * Prefix used in combination with the resource (client) name for resource level roles. + */ public static final String PREFIX_RESOURCE_ROLE = "ROLE_"; - /** Name of the claim containing the realm level roles */ + /** + * Name of the claim containing the realm level roles + */ private static final String CLAIM_REALM_ACCESS = "realm_access"; - - /** Name of the claim containing the resources (clients) the user has access to. */ + /** + * Name of the claim containing the resources (clients) the user has access to. + */ private static final String CLAIM_RESOURCE_ACCESS = "resource_access"; - - /** Name of the claim containing roles. (Applicable to realm and resource level.) */ + /** + * Name of the claim containing roles. (Applicable to realm and resource level.) + */ private static final String CLAIM_ROLES = "roles"; @Override public AbstractAuthenticationToken convert(Jwt source) { - return new JwtAuthenticationToken( - source, - Stream.concat( - new JwtGrantedAuthoritiesConverter().convert(source).stream(), - TEMPORARNAME(source).stream()) - .collect(toSet())); + return new JwtAuthenticationToken(source, Stream.concat(new JwtGrantedAuthoritiesConverter().convert(source) + .stream(), tokenRolesExtractor(source).stream()) + .collect(toSet())); } /** - * Extracts the realm and resource level roles from a JWT token distinguishing between them - * using prefixes. + * Extracts the realm and resource level roles from a JWT token distinguishing between them using prefixes. */ - public Collection TEMPORARNAME(Jwt jwt) { + public Collection tokenRolesExtractor(Jwt jwt) { // Collection that will hold the extracted roles Collection grantedAuthorities = new ArrayList<>(); @@ -61,43 +69,33 @@ public class KeycloakJwtRolesConverter implements Converter realmRoles = - roles.stream() - // Prefix all realm roles with "ROLE_realm_" - .map(role -> new SimpleGrantedAuthority(PREFIX_REALM_ROLE + role)) - .collect(Collectors.toList()); + Collection realmRoles = roles.stream() + // Prefix all realm roles with "ROLE_realm_" + .map(role -> new SimpleGrantedAuthority(PREFIX_REALM_ROLE + role)) + .collect(Collectors.toList()); grantedAuthorities.addAll(realmRoles); } } // Resource (client) roles - // A user might have access to multiple resources all containing their own roles. Therefore, - // it is a map of + // A user might have access to multiple resources all containing their own roles. Therefore, it is a map of // resource each possibly containing a "roles" property. - Map>> resourceAccess = - jwt.getClaim(CLAIM_RESOURCE_ACCESS); + Map>> resourceAccess = jwt.getClaim(CLAIM_RESOURCE_ACCESS); // Check if resources are assigned if (resourceAccess != null && !resourceAccess.isEmpty()) { // Iterate of all the resources - resourceAccess.forEach( - (resource, resourceClaims) -> { - // Iterate of the "roles" claim inside the resource claims - resourceClaims - .get(CLAIM_ROLES) - .forEach( - // Add the role to the granted authority prefixed with ROLE_ - // and the name of the resource - role -> - grantedAuthorities.add( - new SimpleGrantedAuthority( - PREFIX_RESOURCE_ROLE - + resource - + "_" - + role))); - }); + resourceAccess.forEach((resource, resourceClaims) -> { + // Iterate of the "roles" claim inside the resource claims + resourceClaims.get(CLAIM_ROLES).forEach( + // Add the role to the granted authority prefixed with ROLE_ and the name of the resource + role -> grantedAuthorities.add(new SimpleGrantedAuthority(PREFIX_RESOURCE_ROLE + resource + "_" + role)) + ); + }); } return grantedAuthorities; } + + } 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/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; +} 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/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 @@