summaryrefslogtreecommitdiff
path: root/microservices/02-api-gateway/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'microservices/02-api-gateway/src/main/java')
-rw-r--r--microservices/02-api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java13
-rw-r--r--microservices/02-api-gateway/src/main/java/com/example/apigateway/config/RoleHeaderRelayFilter.java69
-rw-r--r--microservices/02-api-gateway/src/main/java/com/example/apigateway/config/SecurityConfig.java93
-rw-r--r--microservices/02-api-gateway/src/main/java/com/example/apigateway/security/controllers/UserRestController.java36
-rw-r--r--microservices/02-api-gateway/src/main/java/com/example/apigateway/security/dto/UserResonseDto.java16
5 files changed, 227 insertions, 0 deletions
diff --git a/microservices/02-api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java b/microservices/02-api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java
new file mode 100644
index 0000000..bdf050a
--- /dev/null
+++ b/microservices/02-api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java
@@ -0,0 +1,13 @@
+package com.example.apigateway;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ApiGatewayApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ApiGatewayApplication.class, args);
+ }
+
+}
diff --git a/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/RoleHeaderRelayFilter.java b/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/RoleHeaderRelayFilter.java
new file mode 100644
index 0000000..e3bb241
--- /dev/null
+++ b/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/RoleHeaderRelayFilter.java
@@ -0,0 +1,69 @@
+package com.example.apigateway.config;
+
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.util.stream.Collectors;
+
+/**
+ * Once the roles are part of the Gateway's session principal, you often need to
+ * forward them down to your resource microservices. You can do this by creating
+ * a GlobalFilter that reads the authenticated roles and places them into custom
+ * HTTP headers.
+ *
+ * RoleHeaderRelayFilter
+ */
+@Component
+public class RoleHeaderRelayFilter implements GlobalFilter, Ordered {
+
+ @Override
+ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+ return ReactiveSecurityContextHolder.getContext()
+ .map(SecurityContext::getAuthentication)
+ .flatMap(authentication -> {
+ if (authentication != null && authentication.getPrincipal() instanceof OAuth2User) {
+ OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
+
+ // Extract username (adjust the attribute key based on your IdP provider)
+ String username = oAuth2User.getAttribute("preferred_username");
+ if (username == null) {
+ username = oAuth2User.getName();
+ }
+
+ // Extract roles/authorities
+ String roles = oAuth2User.getAuthorities().stream()
+ .map(grantedAuthority -> grantedAuthority.getAuthority())
+ .collect(Collectors.joining(","));
+
+ // Mutate the request with new headers
+ ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
+ .header("X-User-Roles", roles)
+ .header("X-User-Name", username) // Changed 'U-User-Name' to standard 'X-' format for consistency, or use your exact key
+ .build();
+
+ // Proceed with the mutated exchange
+ return chain.filter(exchange.mutate().request(mutatedRequest).build());
+ }
+
+ // If not authenticated via OAuth2User, just pass through
+ return chain.filter(exchange);
+ })
+ .switchIfEmpty(chain.filter(exchange)); // Handle cases where SecurityContext is empty
+ }
+
+ @Override
+ public int getOrder() {
+ // Run after the Security Web Filter loads the context, but before routing
+ return Ordered.LOWEST_PRECEDENCE - 1;
+ }
+}
diff --git a/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/SecurityConfig.java b/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/SecurityConfig.java
new file mode 100644
index 0000000..c503032
--- /dev/null
+++ b/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/SecurityConfig.java
@@ -0,0 +1,93 @@
+package com.example.apigateway.config;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
+import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
+import org.springframework.util.StringUtils;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import com.example.apigateway.security.dto.UserResonseDto;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * Ref:
+ * https://docs.spring.io/spring-security/reference/reactive/oauth2/login/advanced.html
+ */
+@Configuration
+@EnableWebFluxSecurity
+public class SecurityConfig {
+
+ private final WebClient webClient = WebClient.create(); // WebClient configured to point to your backend service
+
+ @Value("${app.userRolesUrl}")
+ private String userRolesUrl;
+
+ @Bean
+ public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
+ final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
+
+ return (userRequest) -> {
+ // Delegate to the default implementation for loading a user
+ return delegate.loadUser(userRequest)
+ .flatMap((oidcUser) -> {
+ String username = oidcUser.getAttribute("sub");
+
+ return fetchRolesFromBackend(username)
+ .map(roles -> {
+ Set<GrantedAuthority> authorities = roles.stream()
+ .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
+ .collect(Collectors.toSet());
+
+ // Keep original identity scopes if needed
+ authorities.addAll(oidcUser.getAuthorities());
+
+ ProviderDetails providerDetails = userRequest.getClientRegistration()
+ .getProviderDetails();
+ String userNameAttributeName = providerDetails.getUserInfoEndpoint()
+ .getUserNameAttributeName();
+ if (StringUtils.hasText(userNameAttributeName)) {
+ return new DefaultOidcUser(authorities, oidcUser.getIdToken(),
+ oidcUser.getUserInfo(), userNameAttributeName);
+ } else {
+ return new DefaultOidcUser(authorities, oidcUser.getIdToken(),
+ oidcUser.getUserInfo());
+ }
+ });
+ });
+ };
+ }
+
+ private Mono<List<String>> fetchRolesFromBackend(String username) {
+ Mono<List<String>> response = this.webClient.get()
+ .uri(userRolesUrl, username)
+ .retrieve()
+ .bodyToMono(UserResonseDto.class)
+ .map(UserResonseDto::getRoles)
+ .onErrorReturn(List.of("")); // Can return a String indicating a fallback role on backend failure
+
+ return response;
+ }
+}
diff --git a/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/controllers/UserRestController.java b/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/controllers/UserRestController.java
new file mode 100644
index 0000000..5d6581c
--- /dev/null
+++ b/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/controllers/UserRestController.java
@@ -0,0 +1,36 @@
+package com.example.apigateway.security.controllers;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import reactor.core.publisher.Mono;
+
+@RestController
+public class UserRestController {
+
+ @GetMapping("/api/security/me")
+ public Mono<Map<String, Object>> getLoggedInUser(@AuthenticationPrincipal OAuth2User principal) {
+ if (principal == null) {
+ return Mono.just(Map.of());
+ }
+
+ // Filter roles starting with "ROLE_"
+ List<String> roles = principal.getAuthorities().stream()
+ .map(grantedAuthority -> grantedAuthority.getAuthority())
+ .filter(authority -> authority.startsWith("ROLE_"))
+ .map(s -> s.substring(5)) // Cut ROLE_ part
+ .collect(Collectors.toList());
+
+ return Mono.just(Map.of(
+ "username", principal.getAttribute("sub"),
+ // "name", principal.getName(),
+ // "email", principal.getEmail(),
+ "roles", roles));
+ }
+}
diff --git a/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/dto/UserResonseDto.java b/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/dto/UserResonseDto.java
new file mode 100644
index 0000000..d553d9a
--- /dev/null
+++ b/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/dto/UserResonseDto.java
@@ -0,0 +1,16 @@
+package com.example.apigateway.security.dto;
+
+import java.util.List;
+
+public class UserResonseDto {
+ private List<String> roles;
+
+ // Getters and Setters
+ public List<String> getRoles() {
+ return roles;
+ }
+
+ public void setRoles(List<String> roles) {
+ this.roles = roles;
+ }
+}