Compare commits
6 Commits
fc73293122
...
730aa5f450
Author | SHA1 | Date | |
---|---|---|---|
|
730aa5f450 | ||
|
dda3e5fcfd | ||
|
5e8e875a37 | ||
|
86e7dc7c75 | ||
|
6235fe7e68 | ||
|
249d00177c |
@ -1,14 +1,32 @@
|
|||||||
package enseirb.myinpulse.api;
|
package enseirb.myinpulse.api;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@RestController
|
@RestController
|
||||||
public class GetUserInfo {
|
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")
|
@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;
|
return Math.random() > 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package enseirb.myinpulse.exceptions;
|
||||||
|
|
||||||
|
public class RoleNotFoudException extends RuntimeException {
|
||||||
|
public RoleNotFoudException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package enseirb.myinpulse.exceptions;
|
||||||
|
|
||||||
|
public class UserNotFoundException extends RuntimeException {
|
||||||
|
public UserNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
package enseirb.myinpulse.security;
|
||||||
|
|
||||||
import static java.util.stream.Collectors.toSet;
|
|
||||||
|
|
||||||
import org.springframework.core.convert.converter.Converter;
|
import org.springframework.core.convert.converter.Converter;
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
@ -16,37 +18,43 @@ import java.util.Map;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class KeycloakJwtRolesConverter implements Converter<Jwt, AbstractAuthenticationToken> {
|
import static java.util.stream.Collectors.toSet;
|
||||||
/** 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 class KeycloakJwtRolesConverter implements Converter<Jwt, AbstractAuthenticationToken> {
|
||||||
|
/**
|
||||||
|
* 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_";
|
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";
|
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";
|
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";
|
private static final String CLAIM_ROLES = "roles";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AbstractAuthenticationToken convert(Jwt source) {
|
public AbstractAuthenticationToken convert(Jwt source) {
|
||||||
return new JwtAuthenticationToken(
|
return new JwtAuthenticationToken(source, Stream.concat(new JwtGrantedAuthoritiesConverter().convert(source)
|
||||||
source,
|
.stream(), tokenRolesExtractor(source).stream())
|
||||||
Stream.concat(
|
|
||||||
new JwtGrantedAuthoritiesConverter().convert(source).stream(),
|
|
||||||
TEMPORARNAME(source).stream())
|
|
||||||
.collect(toSet()));
|
.collect(toSet()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the realm and resource level roles from a JWT token distinguishing between them
|
* Extracts the realm and resource level roles from a JWT token distinguishing between them using prefixes.
|
||||||
* using prefixes.
|
|
||||||
*/
|
*/
|
||||||
public Collection<GrantedAuthority> TEMPORARNAME(Jwt jwt) {
|
public Collection<GrantedAuthority> tokenRolesExtractor(Jwt jwt) {
|
||||||
// Collection that will hold the extracted roles
|
// Collection that will hold the extracted roles
|
||||||
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
|
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
|
||||||
|
|
||||||
@ -61,8 +69,7 @@ public class KeycloakJwtRolesConverter implements Converter<Jwt, AbstractAuthent
|
|||||||
// Check if any roles are present
|
// Check if any roles are present
|
||||||
if (roles != null && !roles.isEmpty()) {
|
if (roles != null && !roles.isEmpty()) {
|
||||||
// Iterate of the roles and add them to the granted authorities
|
// Iterate of the roles and add them to the granted authorities
|
||||||
Collection<GrantedAuthority> realmRoles =
|
Collection<GrantedAuthority> realmRoles = roles.stream()
|
||||||
roles.stream()
|
|
||||||
// Prefix all realm roles with "ROLE_realm_"
|
// Prefix all realm roles with "ROLE_realm_"
|
||||||
.map(role -> new SimpleGrantedAuthority(PREFIX_REALM_ROLE + role))
|
.map(role -> new SimpleGrantedAuthority(PREFIX_REALM_ROLE + role))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@ -71,33 +78,24 @@ public class KeycloakJwtRolesConverter implements Converter<Jwt, AbstractAuthent
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resource (client) roles
|
// Resource (client) roles
|
||||||
// A user might have access to multiple resources all containing their own roles. Therefore,
|
// A user might have access to multiple resources all containing their own roles. Therefore, it is a map of
|
||||||
// it is a map of
|
|
||||||
// resource each possibly containing a "roles" property.
|
// resource each possibly containing a "roles" property.
|
||||||
Map<String, Map<String, Collection<String>>> resourceAccess =
|
Map<String, Map<String, Collection<String>>> resourceAccess = jwt.getClaim(CLAIM_RESOURCE_ACCESS);
|
||||||
jwt.getClaim(CLAIM_RESOURCE_ACCESS);
|
|
||||||
|
|
||||||
// Check if resources are assigned
|
// Check if resources are assigned
|
||||||
if (resourceAccess != null && !resourceAccess.isEmpty()) {
|
if (resourceAccess != null && !resourceAccess.isEmpty()) {
|
||||||
// Iterate of all the resources
|
// Iterate of all the resources
|
||||||
resourceAccess.forEach(
|
resourceAccess.forEach((resource, resourceClaims) -> {
|
||||||
(resource, resourceClaims) -> {
|
|
||||||
// Iterate of the "roles" claim inside the resource claims
|
// Iterate of the "roles" claim inside the resource claims
|
||||||
resourceClaims
|
resourceClaims.get(CLAIM_ROLES).forEach(
|
||||||
.get(CLAIM_ROLES)
|
// Add the role to the granted authority prefixed with ROLE_ and the name of the resource
|
||||||
.forEach(
|
role -> grantedAuthorities.add(new SimpleGrantedAuthority(PREFIX_RESOURCE_ROLE + resource + "_" + role))
|
||||||
// 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;
|
return grantedAuthorities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
*
|
||||||
|
* <p>Set a keycloak role to a keycloak user.
|
||||||
|
*
|
||||||
|
* <p>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();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package enseirb.myinpulse.utils.keycloak.datatypes;
|
||||||
|
|
||||||
|
public class RoleRepresentation {
|
||||||
|
public String id;
|
||||||
|
public String name;
|
||||||
|
public String description;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package enseirb.myinpulse.utils.keycloak.datatypes;
|
||||||
|
|
||||||
|
public class UserRepresentation {
|
||||||
|
public String id;
|
||||||
|
public String name;
|
||||||
|
}
|
@ -14,7 +14,8 @@ axiosInstance.interceptors.response.use(
|
|||||||
async (error) => {
|
async (error) => {
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config;
|
||||||
if (
|
if (
|
||||||
error.response.status === 401 &&
|
((error.response && error.response.status === 401) ||
|
||||||
|
error.code == "ERR_NETWORK") &&
|
||||||
!originalRequest._retry &&
|
!originalRequest._retry &&
|
||||||
store.authenticated
|
store.authenticated
|
||||||
) {
|
) {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { store } from "../main.ts";
|
import { store } from "../main.ts";
|
||||||
import { callApi } from "@/services/api.ts";
|
import { callApi } from "@/services/api.ts";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const CustomRequest = ref("");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -55,6 +58,14 @@ import { callApi } from "@/services/api.ts";
|
|||||||
<td>res</td>
|
<td>res</td>
|
||||||
<td id="3"></td>
|
<td id="3"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input v-model="CustomRequest" placeholder="edit me" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button @click="callApi(CustomRequest)">call</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user