summaryrefslogtreecommitdiff
path: root/microservices/02-api-gateway/src/main
diff options
context:
space:
mode:
authorKamal Wickramanayake <kamal@inbox.lk>2026-07-03 19:02:36 +0530
committerKamal Wickramanayake <kamal@inbox.lk>2026-07-03 19:02:36 +0530
commitaa122113ade36f02dc8fdbebdf1232b5c4b8742c (patch)
treec345b83db6c769bf5e72a09e3f19d43431961695 /microservices/02-api-gateway/src/main
parentb221a83ecef1dd7f9583d5107017d24e668205a6 (diff)
Microservice sample projects
Diffstat (limited to 'microservices/02-api-gateway/src/main')
-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
-rw-r--r--microservices/02-api-gateway/src/main/resources/application.yaml89
6 files changed, 316 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;
+ }
+}
diff --git a/microservices/02-api-gateway/src/main/resources/application.yaml b/microservices/02-api-gateway/src/main/resources/application.yaml
new file mode 100644
index 0000000..b444233
--- /dev/null
+++ b/microservices/02-api-gateway/src/main/resources/application.yaml
@@ -0,0 +1,89 @@
+server:
+ port: 8050
+ address: 127.0.0.1
+
+app:
+ userRolesUrl: "http://localhost:8052/myservices/api/security/users/{username}/roles"
+
+# logging:
+# level:
+# org.springframework.security: trace
+
+spring:
+ application:
+ name: api-gateway
+
+ cloud:
+ gateway:
+ server:
+ webflux:
+ globalcors:
+ cors-configurations:
+ '[/**]':
+ allowedOrigins:
+ - "http://localhost:5173"
+ allowedMethods: "*"
+ allowedHeaders: "*"
+ allowCredentials: true
+ routes:
+ # Frontend tries to access this URL when the login link is clicked.
+ # If user is not logged in, user will be redirected to OAuth2 server.
+ # If logged in, frontend will be reloaded.
+ - id: frontend
+ uri: no://op # CRITICAL: Prevents forwarding to a backend server
+ predicates:
+ - Path=/ui
+ filters:
+ - RedirectTo=302, http://localhost:5173/
+ # Forward requests to backend service (OAuth2 resource server)
+ - id: backend-service
+ uri: http://localhost:8052
+ predicates:
+ - Path=/api/**
+ filters:
+ # 1. ALWAYS strip incoming spoof headers from the public web first
+ - RemoveRequestHeader=X-User-Id
+ - RemoveRequestHeader=X-User-Name
+ - RemoveRequestHeader=X-User-Roles
+ - RemoveRequestHeader=Authorization
+ - PrefixPath=/myservices
+ - TokenRelay # Add oauth (or jwt) token to request header before forwarding
+
+ security:
+ oauth2:
+ client:
+ provider:
+ platform-auth-server:
+ issuer-uri: http://127.0.0.1:8051
+ registration:
+ api-gateway:
+ provider: platform-auth-server
+ client-id: api-gateway
+ client-secret: "apiGatewayPassword1234"
+ client-authentication-method: client_secret_basic
+ authorization-grant-type: authorization_code
+ redirect-uri: http://localhost:5173/bff/login/oauth2/code/api-gateway
+ scope:
+ - openid
+ - profile
+com:
+ c4-soft:
+ springaddons:
+ oidc:
+ ops:
+ - iss: http://127.0.0.1:8051
+ client:
+ client-uri: http://localhost:5173
+ login-uri: /bff/oauth2/authorization/api-gateway
+ security-matchers:
+ - /**
+ permit-all:
+ - /login/**
+ - /oauth2/**
+ - /
+ - /api/**
+ csrf: cookie-accessible-from-js
+ oauth2-redirections:
+ rp-initiated-logout: ACCEPTED
+ post-logout-redirect-host: http://localhost:5173
+ post-logout-redirect-path: /