feat: configured authentification

This commit is contained in:
Pierre Tellier 2025-02-07 23:06:59 +01:00
parent edd4993f3f
commit dbf06d8c64
6 changed files with 161 additions and 67 deletions

View File

@ -3,7 +3,7 @@ help:
clean: clean:
@cp config/prod.docker-compose.yaml docker-compose.yaml @cp config/prod.docker-compose.yaml docker-compose.yaml
@docker compose down 2> /dev/null @docker compose down
@rm -f docker-compose.yaml @rm -f docker-compose.yaml
@rm -f .env @rm -f .env
@rm -f front/MyINPulse-front/.env @rm -f front/MyINPulse-front/.env
@ -21,14 +21,14 @@ dev-front: clean vite
@cp config/frontdev.front.env front/MyINPulse-front/.env @cp config/frontdev.front.env front/MyINPulse-front/.env
@cp config/frontdev.main.env .env @cp config/frontdev.main.env .env
@cp config/frontdev.docker-compose.yaml docker-compose.yaml @cp config/frontdev.docker-compose.yaml docker-compose.yaml
@docker compose up -d @docker compose up -d --build
@cd ./front/MyINPulse-front/ && npm run dev @cd ./front/MyINPulse-front/ && npm run dev
prod: clean prod: clean
@cp config/prod.front.env front/MyINPulse-front/.env @cp config/prod.front.env front/MyINPulse-front/.env
@cp config/prod.main.env .env @cp config/prod.main.env .env
@cp config/frontdev.docker-compose.yaml docker-compose.yaml @cp config/frontdev.docker-compose.yaml docker-compose.yaml
@docker compose up -d @docker compose up -d --build
@ -36,6 +36,6 @@ dev-back:
@cp config/backdev.front.env front/MyINPulse-front/.env @cp config/backdev.front.env front/MyINPulse-front/.env
@cp config/backdev.main.env .env @cp config/backdev.main.env .env
@cp config/backdev.docker-compose.yaml docker-compose.yaml @cp config/backdev.docker-compose.yaml docker-compose.yaml
@docker compose up -d @docker compose up -d --build
@echo "cd MyINPulse-back" @echo "cd MyINPulse-back"
@echo "./gradlew bootRun --args='--server.port=8081'" @echo "./gradlew bootRun --args='--server.port=8081'"

View File

@ -1,22 +1,22 @@
package enseirb.myinpulse; package enseirb.myinpulse;
import enseirb.myinpulse.security.KeycloakJwtRolesConverter;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer; import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays; import java.util.*;
import java.util.stream.Collectors;
import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope; import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
@SpringBootApplication @SpringBootApplication
public class MyinpulseApplication { public class MyinpulseApplication {
@ -25,34 +25,5 @@ public class MyinpulseApplication {
SpringApplication.run(MyinpulseApplication.class, args); SpringApplication.run(MyinpulseApplication.class, args);
} }
// CORS configuration
// TODO: make sure to only accept our own domains
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type",
"x-auth-token")); // Do not remove, this fixes the CORS errors when unauthenticated
//configuration.setExposedHeaders(Arrays.asList("x-auth-token"));
UrlBasedCorsConfigurationSource source = new
UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
// TODO: add unauthenticated endpoint with server status
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/random2").access(hasScope("contacts"))
.requestMatchers("/getUserInfo").access(hasScope("messages"))
.anyRequest().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
} }

View File

@ -0,0 +1,50 @@
package enseirb.myinpulse.config;
import enseirb.myinpulse.security.KeycloakJwtRolesConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
@Configuration
public class WebSecurityCustomConfiguration {
// CORS configuration
// TODO: make sure to only accept our own domains
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type",
"x-auth-token")); // Do not remove, this fixes the CORS errors when unauthenticated
UrlBasedCorsConfigurationSource source = new
UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/random2").access(hasRole("REALM_MyINPulse-entrepreneur"))
.requestMatchers("/random").access(hasRole("REALM_MyINPulse-admin"))
.requestMatchers("/random3").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.
jwtAuthenticationConverter(new KeycloakJwtRolesConverter())));
return http.build();
}
}

View File

@ -0,0 +1,98 @@
package enseirb.myinpulse.security;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toSet;
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_";
/**
* 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.
*/
private static final String CLAIM_RESOURCE_ACCESS = "resource_access";
/**
* 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()));
}
/**
* Extracts the realm and resource level roles from a JWT token distinguishing between them using prefixes.
*/
public Collection<GrantedAuthority> TEMPORARNAME(Jwt jwt) {
// Collection that will hold the extracted roles
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
// Realm roles
// Get the part of the access token that holds the roles assigned on realm level
Map<String, Collection<String>> realmAccess = jwt.getClaim(CLAIM_REALM_ACCESS);
// Verify that the claim exists and is not empty
if (realmAccess != null && !realmAccess.isEmpty()) {
// From the realm_access claim get the roles
Collection<String> roles = realmAccess.get(CLAIM_ROLES);
// Check if any roles are present
if (roles != null && !roles.isEmpty()) {
// Iterate of the roles and add them to the granted authorities
Collection<GrantedAuthority> 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
// resource each possibly containing a "roles" property.
Map<String, Map<String, Collection<String>>> 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))
);
});
}
return grantedAuthorities;
}
}

View File

@ -1,3 +1,4 @@
spring.application.name=myinpulse spring.application.name=myinpulse
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:7080/realms/test/protocol/openid-connect/certs spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:7080/realms/test/protocol/openid-connect/certs
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:7080/realms/test spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:7080/realms/test
logging.level.org.springframework.security=DEBUG

View File

@ -1,26 +0,0 @@
import axios from "axios";
const backendUrl = "http://localhost:8080/"
// TODO: spawn a error modal
function defaultApiErrorHandler(err: String){
console.log(err)
}
function defaultApiSuccessHandler(response: () => void){
console.log(response)
}
/*
function callApi(endpoint: string, onSuccessHandler: () => void, onErrorHandler: (err: String) => void): void {
console.log("callApi: "+ endpoint)
axios.get(backendUrl + endpoint).then(
onSuccessHandler == null ? defaultApiSuccessHandler : onSuccessHandler
).catch(
(err) => {
onErrorHandler == null ? defaultApiErrorHandler(err): onErrorHandler(err);
}
)
}
export {callApi}
*/