From ca9a88a1870a8149d0d085651abe6abe0801d4fd Mon Sep 17 00:00:00 2001 From: Mikhail Niedre Date: Mon, 29 Dec 2025 14:12:20 +0500 Subject: [PATCH 1/9] nginx ssl --- Makefile | 6 +- compose.yaml | 59 ++++---- etc/keystore.p12 | Bin 6119 -> 0 bytes etc/truststore.jks | Bin 2566 -> 0 bytes .../spring/security/JwtDecoderConfig.java | 143 +----------------- .../brothers/spring/security/RestConfig.java | 42 +---- .../spring/security/SecurityConfig.java | 23 +-- .../src/main/resources/application.yml | 33 ++-- .../src/main/resources/truststore.p12 | Bin 1286 -> 0 bytes .../authserver/security/SecurityConfig.java | 9 +- .../src/main/resources/application.yml | 23 +-- .../src/main/resources/keystore.p12 | Bin 2786 -> 0 bytes nginx/README-ru.txt | 8 + nginx/README.txt | 4 + nginx/SETUP.md | 51 +++++++ nginx/certs/server.crt | 19 +++ nginx/certs/server.key | 28 ++++ nginx/nginx.conf | 35 +++++ 18 files changed, 213 insertions(+), 270 deletions(-) delete mode 100644 etc/keystore.p12 delete mode 100644 etc/truststore.jks delete mode 100644 hello-sample-app/src/main/resources/truststore.p12 delete mode 100644 hello-sample-sas/src/main/resources/keystore.p12 create mode 100644 nginx/README-ru.txt create mode 100644 nginx/README.txt create mode 100644 nginx/SETUP.md create mode 100644 nginx/certs/server.crt create mode 100644 nginx/certs/server.key create mode 100644 nginx/nginx.conf diff --git a/Makefile b/Makefile index 83d715a..27e3bb9 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ # DOCKER COMPOSE COMMANDS up: - @docker compose --project-name hello-spring-auth up --build --detach + @docker compose up --build --detach down: - @docker compose --project-name hello-spring-auth down + @docker compose down downv: - @docker compose --project-name hello-spring-auth down --remove-orphans -v + @docker compose down --remove-orphans -v logs: @docker compose logs -f diff --git a/compose.yaml b/compose.yaml index 248da1b..d41d299 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,64 +1,65 @@ +name: ${COMPOSE_PROJECT_NAME:-hello-spring-auth} + services: -# sas-gradle-builder: -# image: gradle:8.14.2-jdk21-ubi-minimal -# container_name: sas-gradle-builder -# working_dir: /home/gradle/project -# volumes: -# - ./hello-sample-sas:/home/gradle/project # Mount the current directory to `/app` in the container -# - sas-data:/home/gradle/project/build/libs # Persist built JAR files -# - sas-gradle-cache:/home/gradle/.gradle # Reuse Gradle cache for faster builds -# command: gradle bootJar - hello-sample-sas: + nginx: + image: nginx:1.25-alpine + container_name: nginx-proxy + ports: + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/certs:/etc/nginx/certs:ro + depends_on: + sas: + condition: service_healthy + app: + condition: service_healthy + networks: + network: { } + + sas: build: context: ./hello-sample-sas dockerfile: Dockerfile container_name: auth-server - ports: - - "$SAS_SERVER_PORT:$SAS_SERVER_PORT" + expose: + - "$SAS_SERVER_PORT" env_file: - .env environment: - GITHUB_CLIENT_ID=$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_ID - GITHUB_CLIENT_SECRET=$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_SECRET healthcheck: - test: [ "CMD-SHELL", "curl -f http://localhost:$SAS_SERVER_PORT$MANAGEMENT_ENDPOINTS_WEB_BASE_PATH/health || exit 1" ] + test: [ "CMD-SHELL", "curl -f http://localhost:$SAS_SERVER_PORT/auth$MANAGEMENT_ENDPOINTS_WEB_BASE_PATH/health || exit 1" ] interval: 20s timeout: 10s retries: 3 start_period: 10s networks: - hello-network: { } + network: { } - hello-sample-app: + app: build: context: ./hello-sample-app dockerfile: Dockerfile container_name: sample-app - ports: - - "$APP_SERVER_PORT:$APP_SERVER_PORT" + expose: + - "$APP_SERVER_PORT" env_file: - .env depends_on: - hello-sample-sas: + sas: condition: service_healthy healthcheck: - test: [ "CMD-SHELL", "curl -f http://localhost:$APP_SERVER_PORT$MANAGEMENT_ENDPOINTS_WEB_BASE_PATH/health || exit 1" ] + test: [ "CMD-SHELL", "curl -f http://localhost:$APP_SERVER_PORT/api$MANAGEMENT_ENDPOINTS_WEB_BASE_PATH/health || exit 1" ] interval: 20s timeout: 10s retries: 3 start_period: 10s restart: on-failure -# volumes: -# - app-gradle-cache:/home/gradle/.gradle # Reuse Gradle cache for faster builds networks: - hello-network: { } - -#volumes: -# sas-gradle-cache: -# sas-data: -# app-gradle-cache: -# app-data: + network: { } networks: - hello-network: \ No newline at end of file + network: \ No newline at end of file diff --git a/etc/keystore.p12 b/etc/keystore.p12 deleted file mode 100644 index e119d220827aeb9110f29f2d70ef166076f22fc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6119 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 diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/security/JwtDecoderConfig.java b/hello-sample-app/src/main/java/cane/brothers/spring/security/JwtDecoderConfig.java index d08a239..a35628b 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/security/JwtDecoderConfig.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/security/JwtDecoderConfig.java @@ -1,160 +1,21 @@ package cane.brothers.spring.security; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.jwk.source.JWKSourceBuilder; -import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jose.util.DefaultResourceRetriever; -import com.nimbusds.jose.util.ResourceRetriever; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; -import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.web.client.RestTemplate; import java.net.MalformedURLException; -import java.time.Duration; @Configuration public class JwtDecoderConfig { @Bean - // RestTemplateBuilder builder public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) throws MalformedURLException { -// JWKSource jwkSource = JWKSourceBuilder.create(new URL(properties.getJwt().getJwkSetUri())) -// .cache(false) -// .rateLimited(false) -// .refreshAheadCache(false) -// // .retrying(true) -// .build(); -// JWSKeySelector jwsKeySelector = -// new JWSVerificationKeySelector<>(JWS_ALGORITHMS, jwkSource); - - // Создаем источник JWK -// JwkSetSource jwkSetSource = new RefreshAheadCachingJwkSetSource<>( -// new ImmutableJwkSet<>(JwkSet.load(new URL(properties.getJwt().getJwkSetUri()))), -// Duration.ofMinutes(15) // Период обновления кэша -// ); -// RestOperations rest = builder -// .connectTimeout(Duration.ofSeconds(15)) -// .readTimeout(Duration.ofSeconds(30)) -// .build(); -// -// var jwtDecoder = NimbusJwtDecoder -//// .withIssuerLocation(properties.getJwt().getIssuerUri()) -// .withJwkSetUri(properties.getJwt().getJwkSetUri()) -// .restOperations(rest) -//// .jwtProcessorCustomizer(processor -> -//// processor.setJWSKeySelector(jwsKeySelector)) -// .build(); -// jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(properties.getJwt().getIssuerUri())); -// return jwtDecoder; - - -// URL jwkSetURL = new URL(properties.getJwt().getJwkSetUri()); -// var connectionTimeout = 20000; -// var readTimeout = 20000; -// ResourceRetriever resourceRetriever = new DefaultResourceRetriever(connectionTimeout, readTimeout); -// JWKSource jwkSource = new RemoteJWKSet<>(jwkSetURL, resourceRetriever); -// -// Set jwsAlgs = new HashSet<>(); -// jwsAlgs.addAll(JWSAlgorithm.Family.RSA); -// ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); -// JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>(jwsAlgs, jwkSource); -// jwtProcessor.setJWSKeySelector(jwsKeySelector); -// // Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it -// // instead -// jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { -// }); -// -// return new NimbusJwtDecoder(jwtProcessor); -// return NimbusJwtDecoder.withJwkSetUri(properties.getJwt().getJwkSetUri()) -// .restOperations(restTemplate(builder, -// Duration.ofMillis(connectionTimeout), -// Duration.ofMillis(readTimeout))) -// .build(); -// } - -// -// var jwkSetUri = properties.getJwt().getJwkSetUri(); - - // 1. Создаем основной источник JWKSet, который будет обращаться к удаленному URI -// JWKSource remoteJWKSet = new RemoteJWKSet(new URL(jwkSetUri)); - - // 2. Создаем кэширующий источник, который будет обновлять ключи заранее - // Мы используем RefreshAheadCachingJWKSetSource, который обертывает RemoteJWKSet - // и обеспечивает "опережающее" кэширование. -// JWKSource refreshAheadJWKSource = new RefreshAheadCachingJWKSetSource( -// remoteJWKSet, TIME_TO_LIVE_SECONDS, -// new DefaultJWKSetCache(TIME_TO_LIVE_SECONDS, REFRESH_AHEAD_TIME_SECONDS, TimeUnit.SECONDS) -// ); - -// JWKSetCache jwkSetCache = new JWKSetCache() { -// private final Cache cache = new ConcurrentMapCache( -// "jwkSetCache"/*, CacheBuilder.newBuilder()...build()*/, false); -// -// @Override -// public void put(JWKSet jwkSet) { -// this.cache.put(jwkSetUri, jwkSet); -// } -// -// @Override -// public JWKSet get() { -// return this.cache.get(jwkSetUri, JWKSet.class); -// } -// -// @Override -// public boolean requiresRefresh() { -// return this.cache.get(jwkSetUri) == null; -// } -// }; - -// var jwkSetCache = new ConcurrentMapCache("jwkSetCache", CacheBuilder.newBuilder() -// .expireAfterWrite(Duration.ofMinutes(30)) -// .build().asMap(), false); -// -// // 3. Создаем NimbusJwtDecoder, используя наш кастомный JWKSource -// NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(refreshAheadJWKSource); - - - // Создаем RemoteJWKSet, который по умолчанию имеет кэширование - // Он автоматически считывает заголовки Cache-Control и Last-Modified -// JWKSource jwkSource = new RemoteJWKSet<>(new URL(properties.getJwt().getJwkSetUri())); - - // Если вы хотите настроить кэш вручную (не рекомендуется, если заголовки HTTP работают) - // Вы можете создать CachingJWKSetSource - // CachingJWKSetSource cachingJwkSource = new CachingJWKSetSource<>(jwkSource, 1, 10, TimeUnit.MINUTES); -// RefreshAheadCachingJWKSetSource - -// Cache cache = new NoOpCache("sas"); - -// JWKSourceBuilder.create(new NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder.SpringJWKSource<>(this.restOperations, this.cache, jwkSetUri)) -// .refreshAheadCache(false) -// .rateLimited(false) -// .cache(this.cache instanceof NoOpCache) -// .build(); - - -// NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder -// .withJwkSetUri(jwkSetUri) -// .cache(null) -// .restOperations() -// .jwtProcessorCustomizer((processor) -> { -// JWSVerificationKeySelector selector = -// new JWSVerificationKeySelector<>(jwsAlgs, jwkSource); -// processor.setJWSKeySelector(selector); -// return processor; -// }) -// .jwsAlgorithm(SignatureAlgorithm.RS256) -// .build(); - - // настроить JWS-валидатор, чтобы он ожидал конкретный алгоритм -// jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(jwkSetUri)); - - return NimbusJwtDecoder.withIssuerLocation(properties.getJwt().getIssuerUri()) - .restOperations() + // jwt decoder required for validating tokens + return NimbusJwtDecoder.withIssuerLocation(properties.getJwt().getIssuerUri()) .build(); } } diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/security/RestConfig.java b/hello-sample-app/src/main/java/cane/brothers/spring/security/RestConfig.java index 3e8816c..ef65ff0 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/security/RestConfig.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/security/RestConfig.java @@ -1,65 +1,31 @@ package cane.brothers.spring.security; -import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; -import org.apache.hc.core5.util.TimeValue; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestOperations; -import org.springframework.web.client.RestTemplate; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; -import java.security.KeyStore; -import java.security.cert.CertificateException; import java.time.Duration; -Configuration +@Configuration public class RestConfig { @Bean - public RestOperations restOperations() throws CertificateException { - - // Load the truststore - KeyStore trustStore = KeyStore.getInstance("PKCS12"); - try (var trustStoreStream = getClass().getResourceAsStream("/truststore.p12")) { - if (trustStoreStream == null) { - throw new CertificateException("Truststore not found"); - } - trustStore.load(trustStoreStream, "your-truststore-password".toCharArray()); - } - - // Create SSLContext with the truststore - var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(trustStore); - var sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - - + public RestOperations restOperations() { return new RestTemplateBuilder() - .requestFactory(() -> new HttpComponentsClientHttpRequestFactory( - HttpClient.newBuilder() - .sslContext(sslContext) - .build())); - } - - private RestTemplate restTemplate(RestTemplateBuilder builder, ClientHttpRequestFactory clientHttpRequestFactory) { - return builder -// .connectTimeout(connectionTimeout) -// .readTimeout(readTimeout) - .requestFactory(() -> clientHttpRequestFactory) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) .build(); } @Bean public CloseableHttpClient httpClient() { // Configure connection manager with TTL - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(TimeValue.ofMinutes(5)); + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(100); // Maximum total connections connectionManager.setDefaultMaxPerRoute(20); // Maximum connections per route diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java b/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java index 481d7bc..ac5caa8 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java @@ -1,8 +1,8 @@ package cane.brothers.spring.security; -import com.nimbusds.jose.JWSAlgorithm; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -19,26 +19,16 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Collections; -import java.util.Set; - @Configuration @EnableWebSecurity @RequiredArgsConstructor class SecurityConfig { - // Время жизни кэша, в секундах -// private static final long TIME_TO_LIVE_SECONDS = 3600; // 1 час - - // Настройки кэширования токенов - // Время, за которое нужно обновить кэш до его истечения, в секундах -// private static final long REFRESH_AHEAD_TIME_SECONDS = 600; // 10 минут -// private static final long CACHE_REFRESH_TIME_SECONDS = 600; // 10 минут - // Поддерживаемые алгоритмы JWS -// private static final Set JWS_ALGORITHMS = Collections.singleton(JWSAlgorithm.RS256); - // private final JwtAuthenticationConverter jwtAuthenticationConverter; private final JwtDecoder jwtDecoder; + @Value("${server.servlet.context-path:/}") + private String contextPath; + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable); @@ -52,8 +42,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http.authorizeHttpRequests(auth -> auth // Разрешить доступ к эндпоинтам Swagger UI без авторизации - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api/sample/**").authenticated() + .requestMatchers(contextPath + "/v3/api-docs/**", contextPath + "/swagger-ui/**", contextPath + "/swagger-ui.html").permitAll() + .requestMatchers(contextPath + "/sample/**").authenticated() .anyRequest().permitAll() ); // throw Access Denied exception only once @@ -64,7 +54,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti // authentication http.oauth2ResourceServer(c -> -// c.jwt(j -> j.jwtAuthenticationConverter(jwtAuthenticationConverter)) c.jwt(jwt -> jwt.decoder(jwtDecoder)) // c.jwt(Customizer.withDefaults()) ); diff --git a/hello-sample-app/src/main/resources/application.yml b/hello-sample-app/src/main/resources/application.yml index b795b91..5db3279 100644 --- a/hello-sample-app/src/main/resources/application.yml +++ b/hello-sample-app/src/main/resources/application.yml @@ -16,11 +16,11 @@ logging: management: endpoints: web: - base-path: /management +# base-path: /management exposure: include: health, info cors: - allowed-origins: "${APP_SERVER},${SAS_SERVER}" + allowed-origins: "${APP_SERVER:https://localhost/api},${SAS_SERVER:https://localhost/auth}" allowed-methods: "OPTIONS,GET,POST,PUT,DELETE,HEAD" allowed-headers: "Accept,Authorization,Content-Type,X-Requested-With,Origin" allow-credentials: true @@ -28,32 +28,23 @@ management: server: port: ${APP_SERVER_PORT:8080} - ssl: - bundle: sas-client + servlet: + context-path: /api spring: application: name: hello-sample-app - ssl: - bundle: - jks: - sas-client: - truststore: - location: classpath:truststore.p12 - password: "${TRUSTSTORE_PASSWORD}" - type: PKCS12 security: oauth2: resourceserver: jwt: jws-algorithms: RS256 - trusted-ssl-bundle: truststore-client - jwk-set-uri: ${SAS_SERVER}/oauth2/jwks - issuer-uri: ${SAS_SERVER} + jwk-set-uri: ${SAS_SERVER:http://hello-sample-sas:9000/auth}/oauth2/jwks + issuer-uri: ${SAS_SERVER:http://hello-sample-sas:9000/auth} # Увеличиваем таймаут на установление соединения (в миллисекундах) - jwk-set-uri-connect-timeout: 30000 + jwk-set-uri-connect-timeout: 3000 # Увеличиваем таймаут на чтение ответа - jwk-set-uri-read-timeout: 30000 + jwk-set-uri-read-timeout: 3000 # Устанавливаем таймаут для обновления кэша. # Если сервер авторизации долго генерирует ключи, # это может помочь. @@ -66,13 +57,13 @@ springdoc: path: /swagger-ui docExpansion: none oauth: - issuer-url: ${SAS_SERVER} + issuer-url: ${SAS_SERVER:https://localhost/auth} appName: hello-spring-oauth2 client-id: "swagger-ui" use-pkce-with-authorization-code-grant: true scopes: openid,api.read - oauth2-redirect-url: ${APP_SERVER}/swagger-ui/oauth2-redirect.html + oauth2-redirect-url: ${APP_SERVER:https://localhost/api}/swagger-ui/oauth2-redirect.html show-actuator: false oAuthFlow: - authorizationUrl: ${SAS_SERVER}/oauth2/authorize - tokenUrl: ${SAS_SERVER}/oauth2/token + authorizationUrl: ${SAS_SERVER:https://localhost/auth}/oauth2/authorize + tokenUrl: ${SAS_SERVER:https://localhost/auth}/oauth2/token diff --git a/hello-sample-app/src/main/resources/truststore.p12 b/hello-sample-app/src/main/resources/truststore.p12 deleted file mode 100644 index c2de2a6224f1a887deda36f6630a396d8e1f9381..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1286 zcmV+h1^N0gf&~Hs0Ru3C1gr)LDuzgg_YDCD0ic2eodkjenJ|I`l`w(?kp>AWhDe6@ z4FLxRpn?Q~FoFbr0s#Opf&_O42`Yw2hW8Bt2LUi<1_>&LNQU+thDZTr0|Wso1Q2L2jjT1d9Q8F0VH`KGvbKPN1ON%}QT|JGj40NE)Mw1g*PTg? zAHYfRDMkg!2QHesqk*bRCfLIj{?br>w&dr`#2lF3>_eRD8x+9+w ziUsc*vL7S>lYoRqE^O z6v~ak>AyZCvp*GNQ5U(h*MAh9^Sd)G0H?N-EA@B9NJQ*W;5=6Uncv1>kh}5Cb9ZE zQ-fnG?P_P>2suqLohjOTl-yz9`(|%8r8f0oviU>ab6-so8$*pn?q*y5aLw01!GPDE zI`efsVlo4U$mq^h+{bVlZwiyN$(|9~?Z(SIOT-#JO*Ze}@OwZoH4#ZQRAP6+X3S-A zXH1rW0r0AQR5gwUzIN=HS~>VfqF>e;tMQr1b&^+&cuVgP8%!12J~*GKsL1Ps9X$H!_763$l+KD{m7PamPlTE! z=eLbbfmlr^Kd=k7{KLl=a#+lvt_(ahLX6i}h4#dNq412|(zOqQw$zXGs9~NEwR!8? zA6E}HW3y>7SJAV)|F6n2ml&r6{ScB>FjpXqz>)AR(h3TFy6>_!i&QKsD5ar>Z=~@l zX@E*IRfh!$lYq&%!!9Tf&pl#JWmpsQ3JI+i#WW0}5W(j@?iON8v^hfq&WD)d8~0$n zV6r_^nLH<>;dzz(oLApDe11{e#QQ&9;{pXS|itb0bcV7KeO^!b1hI1fjU9&k#K(Xh4_{fEAj z7Ov=tV*heyT#zm&vP*s`m|!qXFflL<1_@w>NC9O71OfpC00baSbk`p8OSeK7Mc8K1 w@n?V{T`}?g=!U|i73`Po@ttb~6bQiZ=J-=O0jrG0-m7KZYa*S&#sUH-5c?xgc>n+a diff --git a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java index 084d96c..19a6409 100644 --- a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java +++ b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java @@ -10,6 +10,7 @@ import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -22,10 +23,8 @@ import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; -import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.session.HttpSessionEventPublisher; @@ -37,7 +36,6 @@ import java.security.KeyPairGenerator; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; -import java.time.Duration; import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -46,6 +44,9 @@ @EnableWebSecurity public class SecurityConfig { + @Value("${server.servlet.context-path:/}") + private String contextPath; + @Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { @@ -65,7 +66,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h // from the authorization endpoint http.exceptionHandling(exceptions -> exceptions .defaultAuthenticationEntryPointFor( - new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/github"), + new LoginUrlAuthenticationEntryPoint(contextPath + "/oauth2/authorization/github"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML) ) ); diff --git a/hello-sample-sas/src/main/resources/application.yml b/hello-sample-sas/src/main/resources/application.yml index fc37ee2..fbbc0b7 100644 --- a/hello-sample-sas/src/main/resources/application.yml +++ b/hello-sample-sas/src/main/resources/application.yml @@ -10,11 +10,11 @@ logging: management: endpoints: web: - base-path: /management +# base-path: /management exposure: include: health, info cors: - allowed-origins: "${APP_SERVER},${SAS_SERVER}" + allowed-origins: "${APP_SERVER:https://localhost/api},${SAS_SERVER:https://localhost/auth}" allowed-methods: "OPTIONS,GET,POST,PUT,DELETE,HEAD" allowed-headers: "Accept,Authorization,Content-Type,X-Requested-With,Origin" allow-credentials: true @@ -22,23 +22,12 @@ management: server: port: ${SAS_SERVER_PORT:9000} - ssl: - bundle: sas-server + servlet: + context-path: /auth spring: application: name: hello-sample-sas - ssl: - bundle: - jks: - sas-server: - key: - alias: hello-sample-server - password: "${KEYSTORE_PASSWORD}" - keystore: - location: classpath:keystore.p12 - password: "${KEYSTORE_PASSWORD}" - type: PKCS12 security: oauth2: authorizationserver: @@ -51,9 +40,9 @@ spring: authorization-grant-types: - "authorization_code" redirect-uris: - - "${APP_SERVER}/swagger-ui/oauth2-redirect.html" + - "${APP_SERVER:https://localhost/api}/swagger-ui/oauth2-redirect.html" post-logout-redirect-uris: - - "${APP_SERVER}/" + - "${APP_SERVER:https://localhost/api}/" scopes: - "openid" - "api.read" diff --git a/hello-sample-sas/src/main/resources/keystore.p12 b/hello-sample-sas/src/main/resources/keystore.p12 deleted file mode 100644 index fa0e0568b5b13281b1a3ce4a567850f7517fa2fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2786 zcma)8X*3iJ7oM4BMq^7T%D!i9#?DJ3vh>anL%c|hM)qYa!%LQlp+eTOCs84qFm@S? zT{GDugLkrJDN(i|pKm$6=lk`2=bn4+eV%)t`|~*$PGFw~0gu56>`rw z782OQzy$W-BRT+1fb{&=2+{y1K*&e*o1?6Na{Ncd$qob+5+E0kNIkgQF9n1P?f}>R z8*;)G!Te%I8Z^CF$fwtVixCrpjh7NIU@{0K#{&Y2!#SbH|9uezg8<+nP}mKk84!C6 z3{(K~h&{mL$PzQ>z)qS*!&#^#v_iWo7Y1cq`H%XDoy5aY( zV4vNyd))%Ms@Yr$7wGJ zGc&}%S%@xCKZQ%-aWNx(69{hsZJsunpH;%;WL#X(5{|<&((Nv*;KY~5%~=GaO&=a> z$FRp@NGR}7u=g*So$=Grn`d50hg5m;nj7aTHNInI@VVPm=;J-G$$r6oMzWn78S`1# z5LCMA`G!w1uE6wmRo%haW-C*j9yHL}y)kdB_FABG(b!EDUx&3x_b}^C33rW_H_@NQt8k77*&m1H5y@FwP_O;7*8}SPzhyL-+cMOj6VVgY8a&r(@ zQ?@_}TP5E0kYYbM2pgqGL`-uj7vtIZbl{JxnR0BAt3tV&Ys%f$Mss@b># z<(lF0qA%I(U8|XFz|Q^q^1N5g{eH6p>?JezsH6Udtfc$m@7<~aq01sRg@fhmF+jp| zd?K|fb0<&|ZRMmmk(W}_6r%niTB+$>5c~N0bj&W}tk9eahK5vEz7f9@hvemc%tPil z-o@g+yC$pD*UsnZH0Y^4P@wlSH5WFsz${ADCnKMiPolZY?_QQ-eFCJ#{#fq6(7PYt z>XB3ccJ_AjjXU@p4sN4LDvmL_OuqoppR&Pn5rS#Kj?Q_Rwm*(9KZ z;UNRaEvrFA*)j&JX|mFD$yH(JgaFs^uoay+*~eL3l-^Ia0}=9Zu_;N~C&rZCKYf5o zS)*Mu3K_}lRz5A$Fm!xRy9|F(LM99cGsV0Kx*9oBo9z5#%Yw6%DQ0tK^~RuGOi!}7 z8EDknN;wpp;-g0S|52W6az4==F8NbQ>S4Qz|Hn5%!_-l(OXi&Q-IoNa z*EX@rss#&Em^pekeJj-DR*WMmor$6V5ab+uP8^MmEp;?!g zqG%*RkjC5`>G`AZ=~9_(-(k+iE%7as!A4^L5Z^|=Vm7Aon@eQNR9(VFC)F7*50KX3CMb_Dz05Y)R**Nmi$I>PeQxRVL>hAwz;GgzLNTM6zRhFTp3$`{+-yD08r zX-v(Nt`Wn9*?sUT(L{s;y9KWzHZsE?JzCwE4!6SBJImZZ5C?3fg)N)9t&aYdGq)$j ztm4a;t^_+VYej1L9iM&<3y0}!z_Wh5rY(BSSzBSxXC^OsQwA10ZDJ(B!qZ$Q1ur+( z^!4QI>Ke~-EA}JI;D-tK(lo!-W`&6D$gOuQnNxq0p5AE-(|$H8X=>IU65EeaaMu^9 zOhD4y>DrtD>|5o=1#~UBAW>sT&a>aMPM5j1_PlDAoF^URki#&#=zMGRetl(??EAe4 zIn17XwM%zHQ+~C1?yNR0(9N(Ts;KA1*YRTm{Gp}9EK-zyyM%}5?c}e1(gCzEt-5febJML_kcw*EtjrribKJ0eu07E2DjPDEEEGIKk;o1(^w1~9W`+|hm9o1J2^povJ1>F4(Lj>29IE2hX;$bvR@eYi2dj_`}Tt-;-Zok>>=y!;x?Voa@)$2LeI>U~zi| zIIm2kN~hO#;J&8q7798aT%Cl2`glFkvGRR+;^?Q0?^$%{)XP=Afu9hNzNdxH=wf32 HX-WS8*{2>w diff --git a/nginx/README-ru.txt b/nginx/README-ru.txt new file mode 100644 index 0000000..84c0d11 --- /dev/null +++ b/nginx/README-ru.txt @@ -0,0 +1,8 @@ +# Для работы схемы: +# 1. Все внешние запросы идут через https://localhost/ (порт 443) +# 2. Для доступа к сервисам используйте: +# https://localhost/sas/ → hello-sample-sas (http внутри docker) +# https://localhost/app/ → hello-sample-app (http внутри docker) +# 3. Сертификат самоподписанный, для теста в браузере потребуется принять исключение. +# 4. Внутри docker-сети сервисы общаются по http, SSL только на nginx. + diff --git a/nginx/README.txt b/nginx/README.txt new file mode 100644 index 0000000..af0f4df --- /dev/null +++ b/nginx/README.txt @@ -0,0 +1,4 @@ +# Self-signed certificate for local development +# Run the following command to generate: +# openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt -subj "/CN=localhost" + diff --git a/nginx/SETUP.md b/nginx/SETUP.md new file mode 100644 index 0000000..6d925c0 --- /dev/null +++ b/nginx/SETUP.md @@ -0,0 +1,51 @@ +# SSL через Nginx - Конфигурация + +## Архитектура + +``` +Browser (HTTPS) → Nginx (SSL termination) → Services (HTTP inside Docker) + ↓ + ┌──────┴──────┐ + ↓ ↓ + https://localhost/auth https://localhost/api + ↓ ↓ + hello-sample-sas:9000 hello-sample-app:8080 + (HTTP + context-path=/auth) (HTTP + context-path=/api) +``` + +## Ключевые моменты + +1. **SSL на уровне Nginx**: + - Сертификаты в `nginx/certs/` (server.crt, server.key) + - Порт 443 открыт наружу только у nginx + +2. **Servlet Context Path**: + - hello-sample-sas: `context-path=/auth` + - hello-sample-app: `context-path=/api` + - Это исключает конфликты путей + +3. **URL маппинг**: + - Внешний: `https://localhost/auth/*` → Внутренний: `http://hello-sample-sas:9000/auth/*` + - Внешний: `https://localhost/api/*` → Внутренний: `http://hello-sample-app:8080/api/*` + +4. **OAuth2 Redirect URLs**: + - Используют публичные HTTPS URL через nginx + - Внутренние вызовы (jwk-set-uri, issuer-uri) используют HTTP внутри docker + +## Доступ к сервисам + +- Auth Server: https://localhost/auth/ +- Sample App: https://localhost/api/ +- Swagger UI: https://localhost/api/swagger-ui/index.html +- Health Checks: + - https://localhost/auth/management/health + - https://localhost/api/management/health + +## Запуск + +```bash +docker compose up --build +``` + +При первом подключении браузер попросит принять самоподписанный сертификат. + diff --git a/nginx/certs/server.crt b/nginx/certs/server.crt new file mode 100644 index 0000000..f84d464 --- /dev/null +++ b/nginx/certs/server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUSXNDtwGh8VYBsNA2q+nE7z9bTq4wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTIyOTA3NTUwOVoXDTI2MTIy +OTA3NTUwOVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAqcHE4Mk44Dg4vu3+ht89mMszh4yl45KFdabDUQMrEYM2 +IYmhIgp1fcCDanGdtIM3kBLLm/hRTaw6bI4hBd7XZYcaYDMVkQJbcd4KURcI8cR5 +2oHmpw59AFLV8F2YGDka234ULwFLDRP+MRCoPgB98G7V84cLANTcVY7Zddv901nu +5Scelkb1eG/2avM/wU+0J5QwKeyImUIT1+eenk2ZblXxQryo4e5aMgjmhVP+HFk7 +bvtcrnEcsWqlfDPPBdakvxtI5TE5G+yVZAB3CwoHiOmFdTk2xgLSzl2CgGfQRm9U +c0+ixJstlWsdV/Ri6c/2HTfAfuIR4rl8ax1Dwd8FXQIDAQABo1MwUTAdBgNVHQ4E +FgQUtGuAJC/ZBdj5Gn/S3+TByvxH8QgwHwYDVR0jBBgwFoAUtGuAJC/ZBdj5Gn/S +3+TByvxH8QgwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAFLIC +6sWK/xmYmVYz7du7HDpteD0lFEKqt7Nz50IjoTBEz3n0LEfE4JinG8QvkLYDZrjn +nVqFP5PiUlECqlVYrlH/pGSAJFkyTXpUHijtjAcO+0HlBVxjPFIZ3RR4TUAVp64D +SiTJgQ25oKW78dAZXiqz8uG9UwnzpD17ar/otn2UJX8k8rpx8RLEIVI0zp6hCWtp +3IhrqCBaTtJvxSFB4rRd9OmhKAzzbiL+PHYzY8vI62V1Xew1IsPleYJptQhqAgvI +hZWo6tdIAfS7/SDLXxW7tWc/RqjBgfbZX4x1ZlLBl/qHyvuNfwzJMG8MiqYmFkGz +oq7fkd6mHdmVagu/KQ== +-----END CERTIFICATE----- diff --git a/nginx/certs/server.key b/nginx/certs/server.key new file mode 100644 index 0000000..452d382 --- /dev/null +++ b/nginx/certs/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpwcTgyTjgODi+ +7f6G3z2YyzOHjKXjkoV1psNRAysRgzYhiaEiCnV9wINqcZ20gzeQEsub+FFNrDps +jiEF3tdlhxpgMxWRAltx3gpRFwjxxHnageanDn0AUtXwXZgYORrbfhQvAUsNE/4x +EKg+AH3wbtXzhwsA1NxVjtl12/3TWe7lJx6WRvV4b/Zq8z/BT7QnlDAp7IiZQhPX +556eTZluVfFCvKjh7loyCOaFU/4cWTtu+1yucRyxaqV8M88F1qS/G0jlMTkb7JVk +AHcLCgeI6YV1OTbGAtLOXYKAZ9BGb1RzT6LEmy2Vax1X9GLpz/YdN8B+4hHiuXxr +HUPB3wVdAgMBAAECggEADsSuQZYP7iXF/gpLceVbAP9wmLLKPc2h8bXT2Sjq5seg +/nLwQztgtFN6u1huDWW7ADw6XXPRcu3wWUBWLCISYCFMUKExF5/6X6IfCKX1376l +kTZq4A65Hj2WoiYqVLUnGoBR9jLpGhaqrw8Ra+90BWZHE7wkX2qlToYycff1EZ1l +ip4RgpXCsQu8Q6rIuK9hAexpktLVd+Ju38SFQ4OnLExjW4p/FSl1EdJhWunk7C88 +K7l/HtADJ0zKlDE05HhUoorYwcOrJzEMAZFP8cer0yov+uwss0nmb/WbSaC8ulDa +nY4zERyLcSsXFrCueKYIlEdxQsz0jezVFcK8e6ZbawKBgQDW9kXJj8J4jGwZExAC +uMLSh9A/efwTDQys/z2Cz33locPkzvjhKKpCXwU0nb7x5sHUueCpNXPybgI8ec/2 +Zrlr1UQBkueGtI6dfiyVhhyH/XoIc5G8hTkcWmUZo8rSUeTM72/Yw4gqa5HAZdRB +lHK91irnlmNwgbu+VzO9EjboswKBgQDKKjYTdsGsxaajKByDkstaCJbQuuBwZZOA +Yrz3JSLfPhN9W4wqVygcE8kdRVh7/EqVP6MdmnqNGQB4ly1mi0IUtPixjQshvaJj +RadFk2cmvSmG4S8pRthRUi5Hcf74p5P75rVaRyLtWs/D6ZUQ89FSfEGaghElqTf6 +Wjv/UP3BrwKBgF93zJKyCBplsvSH5MpwqAW8T56BXJRRbVm/md/oqu87IrcRvLKy +zrrfXH57uHvSki8Zxk8f8Diw5slZCCVUhfEALE3OoojO06/ag458m1tCFdp/CTCC +slSHSPNULRWvTUA+7puEa4r7byXVk6j0dukcnr1vqwYid/EW5WGJH13FAoGAIJl9 +7tWPlZSpslWdg3oAYJxR9Yas+nLmviUt44yRev4/lk9U4t77EMv/+kBcbGHahQal +/vgSGv6VHN0D7S03kq88CyV7Tg2OSgPJXWbPk2edcqqNOFK8PyDJZav0OZSMQGqL +g+tErpGePzFDYGBwuKRgz9F5gmEvLaevVRRyVvECgYEAxpHV0YCouG7aOfrv8XRQ +7AEH79du84jYrltnSZBPwrMiwvKRzu21H4Cd9Is7ac0+g/CjvJq/31KBqJkZxfvh +vN/R5GT2hQudpwyYavvoUKkPPfQyFD6BBwFe7WboWvUDUhFIaRspLTzQ0U5p2n7X +7dG+5WHwnxDa1aRxDrz5u9U= +-----END PRIVATE KEY----- diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..80bdcb3 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,35 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 80; + server_name localhost; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/certs/server.crt; + ssl_certificate_key /etc/nginx/certs/server.key; + + location /auth/ { + proxy_pass http://sas:9000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/ { + proxy_pass http://app:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} From 8c821f526104ce986776497591952b6aebab0677 Mon Sep 17 00:00:00 2001 From: Mikhail Niedre Date: Mon, 29 Dec 2025 14:26:24 +0500 Subject: [PATCH 2/9] internal/external sas host --- CHANGES.md | 75 ++++++++ QUICK-REFERENCE.md | 92 ++++++++++ SWAGGER-OAUTH2-SETUP.md | 166 ++++++++++++++++++ .../src/main/resources/application.yml | 9 +- .../src/main/resources/application.yml | 3 +- nginx/nginx.conf | 4 + 6 files changed, 344 insertions(+), 5 deletions(-) create mode 100644 CHANGES.md create mode 100644 QUICK-REFERENCE.md create mode 100644 SWAGGER-OAUTH2-SETUP.md diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..41f05ad --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,75 @@ +# Summary of Changes - Swagger UI OAuth2 Redirect URI Configuration + +## Changes Made + +### 1. Environment Variables (.env) +- Added `SAS_SERVER_EXTERNAL=https://localhost/auth` for browser-facing OAuth2 redirects +- Updated `APP_SERVER=https://localhost/api` to use external URL +- Kept `SAS_SERVER=http://sas:9000/auth` for internal service-to-service JWT validation + +### 2. Nginx Configuration (nginx/nginx.conf) +- Added `X-Forwarded-Host` header to both `/auth/` and `/api/` locations +- Added `X-Forwarded-Port 443` header to both locations +- These headers help Spring construct correct redirect URLs when behind a reverse proxy + +### 3. Application Configuration (hello-sample-app/src/main/resources/application.yml) +- Confirmed `forward-headers-strategy: framework` is enabled (allows Spring to use X-Forwarded headers) +- Updated `oauth.issuer-url` to use `${SAS_SERVER_EXTERNAL}` for browser redirects +- Updated `oAuthFlow.authorizationUrl` to use `${SAS_SERVER_EXTERNAL}/oauth2/authorize` +- Updated `oAuthFlow.tokenUrl` to use `${SAS_SERVER_EXTERNAL}/oauth2/token` +- Updated CORS `allowed-origins` to use `${SAS_SERVER_EXTERNAL}` instead of `${SAS_SERVER}` +- JWT validation still uses `${SAS_SERVER}` (internal URL) for jwk-set-uri and issuer-uri + +### 4. Authorization Server Configuration (hello-sample-sas/src/main/resources/application.yml) +- Confirmed `forward-headers-strategy: framework` is enabled +- Confirmed redirect-uris uses `${APP_SERVER}` which now points to external URL +- Updated CORS `allowed-origins` to use `${SAS_SERVER_EXTERNAL}` instead of `${SAS_SERVER}` + +## Why These Changes Were Necessary + +### The Problem +In Docker Compose with nginx reverse proxy: +- Services communicate internally using HTTP (e.g., `http://sas:9000/auth`) +- Browser accesses services through HTTPS nginx (e.g., `https://localhost/auth`) +- Swagger UI OAuth2 redirects must use the external URL that the browser can access +- JWT validation should use internal URL for better performance and reliability + +### The Solution +**Separate Internal and External URLs:** +- `SAS_SERVER` (internal): For service-to-service communication (JWT validation) +- `SAS_SERVER_EXTERNAL` (external): For browser-facing OAuth2 flows +- `APP_SERVER` (external): For browser redirects back to Swagger UI + +**Enable Proxy Header Support:** +- Spring Boot's `forward-headers-strategy: framework` makes it aware of proxy +- Nginx's `X-Forwarded-*` headers tell Spring the original request details +- This ensures Spring generates correct redirect URLs even when behind a proxy + +## How OAuth2 Flow Works Now + +1. **User opens Swagger UI**: `https://localhost/api/swagger-ui` +2. **Click Authorize**: Browser redirects to `https://localhost/auth/oauth2/authorize` +3. **User authenticates**: Via GitHub IDP +4. **Authorization server redirects back**: `https://localhost/api/swagger-ui/oauth2-redirect.html` +5. **Swagger UI exchanges code for token**: POST to `https://localhost/auth/oauth2/token` +6. **Token validation (backend)**: App validates token by calling `http://sas:9000/auth/oauth2/jwks` + +## Testing + +To test the configuration: + +```bash +# Rebuild and start services +docker-compose down +docker-compose up --build + +# Access Swagger UI +open https://localhost/api/swagger-ui + +# Click "Authorize" button and complete OAuth2 flow +``` + +## Documentation + +See [SWAGGER-OAUTH2-SETUP.md](./SWAGGER-OAUTH2-SETUP.md) for detailed explanation of the configuration. + diff --git a/QUICK-REFERENCE.md b/QUICK-REFERENCE.md new file mode 100644 index 0000000..7f9ac63 --- /dev/null +++ b/QUICK-REFERENCE.md @@ -0,0 +1,92 @@ +# Swagger UI OAuth2 - Quick Reference + +## URLs + +### Browser Access (External) +- **Swagger UI**: https://localhost/api/swagger-ui +- **Authorization Server**: https://localhost/auth +- **API**: https://localhost/api + +### Internal Service Communication +- **JWT Validation**: http://sas:9000/auth + +## Key Configuration Points + +### ✅ What's Configured + +1. **Redirect URI**: `https://localhost/api/swagger-ui/oauth2-redirect.html` + - Configured in Swagger UI (app) + - Registered in OAuth2 client (sas) + +2. **OAuth2 Endpoints**: + - Authorization: `https://localhost/auth/oauth2/authorize` + - Token: `https://localhost/auth/oauth2/token` + - JWKS: `http://sas:9000/auth/oauth2/jwks` (internal) + +3. **PKCE**: Enabled (`use-pkce-with-authorization-code-grant: true`) + +4. **Scopes**: `openid`, `api.read` + +5. **Proxy Headers**: Enabled via `forward-headers-strategy: framework` + +## Environment Variables + +```bash +# Internal (service-to-service) +SAS_SERVER=http://sas:9000/auth + +# External (browser-facing) +SAS_SERVER_EXTERNAL=https://localhost/auth +APP_SERVER=https://localhost/api +``` + +## Quick Start + +```bash +# Start services +docker-compose up --build + +# Access Swagger UI +open https://localhost/api/swagger-ui + +# Test OAuth2 flow +1. Click "Authorize" button +2. Login with GitHub +3. Allow access +4. You should be redirected back to Swagger UI with access token +``` + +## Troubleshooting + +| Issue | Check | +|-------|-------| +| Redirect URI mismatch | Verify `APP_SERVER` in `.env` | +| CORS errors | Check `allowed-origins` uses external URLs | +| JWT validation fails | Verify `SAS_SERVER` uses internal URL | +| Wrong redirect URL | Check `forward-headers-strategy: framework` is set | +| SSL warnings | Accept self-signed certificate in browser | + +## Architecture + +``` +┌─────────┐ HTTPS ┌───────┐ +│ Browser │ ──────────────────────▶│ Nginx │ +└─────────┘ https://localhost └───────┘ + │ + ┌──────────────────┴──────────────────┐ + │ │ + HTTP HTTP + │ │ + ┌─────▼─────┐ ┌─────▼─────┐ + │ SAS │◀───── JWT Validate ────│ App │ + │ :9000 │ (internal) │ :8080 │ + └───────────┘ └───────────┘ +``` + +## Files Modified + +- ✅ `.env` - Added `SAS_SERVER_EXTERNAL` +- ✅ `nginx/nginx.conf` - Added X-Forwarded headers +- ✅ `hello-sample-app/src/main/resources/application.yml` - Updated OAuth URLs +- ✅ `hello-sample-sas/src/main/resources/application.yml` - Updated CORS + diff --git a/SWAGGER-OAUTH2-SETUP.md b/SWAGGER-OAUTH2-SETUP.md new file mode 100644 index 0000000..13159ff --- /dev/null +++ b/SWAGGER-OAUTH2-SETUP.md @@ -0,0 +1,166 @@ +# Swagger UI OAuth2 Configuration for Docker Compose + +## Overview + +This document explains how Swagger UI OAuth2 redirect URIs are configured to work with Docker Compose deployment behind an nginx reverse proxy. + +## Architecture + +``` +Browser → HTTPS (nginx:443) → HTTP (app:8080 or sas:9000) +``` + +## Key Concepts + +### 1. Two Types of URLs + +**Internal URLs** (service-to-service communication): +- Used for backend services to communicate with each other +- Example: `http://sas:9000/auth` (app validates JWT tokens) + +**External URLs** (browser-facing): +- Used by the browser to access services through nginx +- Example: `https://localhost/auth`, `https://localhost/api` +- Used for OAuth2 redirects and CORS + +### 2. Environment Variables + +In `.env` file: +```bash +# Internal - for JWT validation (app → sas) +SAS_SERVER=http://sas:9000/auth + +# External - for browser redirects +SAS_SERVER_EXTERNAL=https://localhost/auth +APP_SERVER=https://localhost/api +``` + +### 3. Configuration Files + +#### Application Configuration (`hello-sample-app/src/main/resources/application.yml`) + +```yaml +server: + forward-headers-strategy: framework # Essential for proxied environment + +spring: + security: + oauth2: + resourceserver: + jwt: + # Internal URL - app validates tokens against auth server + jwk-set-uri: ${SAS_SERVER}/oauth2/jwks + issuer-uri: ${SAS_SERVER} + +springdoc: + swagger-ui: + oauth: + # External URL - browser redirects + issuer-url: ${SAS_SERVER_EXTERNAL} + # External URL - where browser returns after OAuth2 flow + oauth2-redirect-url: ${APP_SERVER}/swagger-ui/oauth2-redirect.html + oAuthFlow: + # External URLs - browser initiates OAuth2 flow + authorizationUrl: ${SAS_SERVER_EXTERNAL}/oauth2/authorize + tokenUrl: ${SAS_SERVER_EXTERNAL}/oauth2/token +``` + +#### Authorization Server Configuration (`hello-sample-sas/src/main/resources/application.yml`) + +```yaml +server: + forward-headers-strategy: framework # Essential for proxied environment + +spring: + security: + oauth2: + authorizationserver: + client: + swagger-ui: + registration: + redirect-uris: + # External URL - must match Swagger UI redirect + - "${APP_SERVER}/swagger-ui/oauth2-redirect.html" +``` + +#### Nginx Configuration (`nginx/nginx.conf`) + +```nginx +location /api/ { + proxy_pass http://app:8080; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; +} +``` + +The `X-Forwarded-*` headers tell Spring the external URL scheme and port. + +## OAuth2 Authorization Code Flow with PKCE + +1. User clicks "Authorize" in Swagger UI at `https://localhost/api/swagger-ui` +2. Browser redirects to Authorization Server: `https://localhost/auth/oauth2/authorize` +3. User authenticates (via GitHub) +4. Authorization Server redirects back to: `https://localhost/api/swagger-ui/oauth2-redirect.html` +5. Swagger UI exchanges authorization code for access token at: `https://localhost/auth/oauth2/token` +6. Swagger UI uses access token for API calls + +## Troubleshooting + +### Redirect URI Mismatch + +**Symptom**: `redirect_uri_mismatch` error + +**Solution**: Ensure that: +1. `APP_SERVER` in `.env` is set to the external URL: `https://localhost/api` +2. Authorization server's registered redirect URI matches: `${APP_SERVER}/swagger-ui/oauth2-redirect.html` +3. Swagger UI's `oauth2-redirect-url` matches the same + +### CORS Errors + +**Symptom**: Browser blocks requests due to CORS + +**Solution**: +1. Set `allowed-origins` to include external URLs +2. Use `SAS_SERVER_EXTERNAL` and `APP_SERVER` (not internal URLs) + +### Wrong Issuer URI + +**Symptom**: JWT validation fails with issuer mismatch + +**Solution**: +- For JWT validation: use `SAS_SERVER` (internal URL) +- For browser OAuth: use `SAS_SERVER_EXTERNAL` (external URL) + +### SSL Certificate Issues + +**Symptom**: Browser shows SSL warnings + +**Solution**: Accept the self-signed certificate or add it to your system trust store + +## Testing + +1. Start the services: + ```bash + docker-compose up --build + ``` + +2. Open Swagger UI: + ``` + https://localhost/api/swagger-ui + ``` + +3. Click "Authorize" button +4. Complete OAuth2 flow (login with GitHub) +5. Test protected endpoints + +## Key Takeaways + +✅ Use **internal URLs** for service-to-service communication (JWT validation) +✅ Use **external URLs** for browser-facing OAuth2 flows +✅ Enable `forward-headers-strategy: framework` for Spring to respect proxy headers +✅ Configure nginx to pass `X-Forwarded-*` headers +✅ Match redirect URIs exactly between Swagger UI config and OAuth2 client registration + diff --git a/hello-sample-app/src/main/resources/application.yml b/hello-sample-app/src/main/resources/application.yml index 5db3279..9cc3a71 100644 --- a/hello-sample-app/src/main/resources/application.yml +++ b/hello-sample-app/src/main/resources/application.yml @@ -20,7 +20,7 @@ management: exposure: include: health, info cors: - allowed-origins: "${APP_SERVER:https://localhost/api},${SAS_SERVER:https://localhost/auth}" + allowed-origins: "${APP_SERVER:https://localhost/api},${SAS_SERVER_EXTERNAL:https://localhost/auth}" allowed-methods: "OPTIONS,GET,POST,PUT,DELETE,HEAD" allowed-headers: "Accept,Authorization,Content-Type,X-Requested-With,Origin" allow-credentials: true @@ -30,6 +30,7 @@ server: port: ${APP_SERVER_PORT:8080} servlet: context-path: /api + forward-headers-strategy: framework spring: application: @@ -57,7 +58,7 @@ springdoc: path: /swagger-ui docExpansion: none oauth: - issuer-url: ${SAS_SERVER:https://localhost/auth} + issuer-url: ${SAS_SERVER_EXTERNAL:https://localhost/auth} appName: hello-spring-oauth2 client-id: "swagger-ui" use-pkce-with-authorization-code-grant: true @@ -65,5 +66,5 @@ springdoc: oauth2-redirect-url: ${APP_SERVER:https://localhost/api}/swagger-ui/oauth2-redirect.html show-actuator: false oAuthFlow: - authorizationUrl: ${SAS_SERVER:https://localhost/auth}/oauth2/authorize - tokenUrl: ${SAS_SERVER:https://localhost/auth}/oauth2/token + authorizationUrl: ${SAS_SERVER_EXTERNAL:https://localhost/auth}/oauth2/authorize + tokenUrl: ${SAS_SERVER_EXTERNAL:https://localhost/auth}/oauth2/token diff --git a/hello-sample-sas/src/main/resources/application.yml b/hello-sample-sas/src/main/resources/application.yml index fbbc0b7..18896ed 100644 --- a/hello-sample-sas/src/main/resources/application.yml +++ b/hello-sample-sas/src/main/resources/application.yml @@ -14,7 +14,7 @@ management: exposure: include: health, info cors: - allowed-origins: "${APP_SERVER:https://localhost/api},${SAS_SERVER:https://localhost/auth}" + allowed-origins: "${APP_SERVER:https://localhost/api},${SAS_SERVER_EXTERNAL:https://localhost/auth}" allowed-methods: "OPTIONS,GET,POST,PUT,DELETE,HEAD" allowed-headers: "Accept,Authorization,Content-Type,X-Requested-With,Origin" allow-credentials: true @@ -24,6 +24,7 @@ server: port: ${SAS_SERVER_PORT:9000} servlet: context-path: /auth + forward-headers-strategy: framework spring: application: diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 80bdcb3..5de6451 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -22,6 +22,8 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; } location /api/ { @@ -30,6 +32,8 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; } } } From 578c4a93f6ef168f993666d96a45d78d3ec4d942 Mon Sep 17 00:00:00 2001 From: Mikhail Niedre Date: Mon, 29 Dec 2025 14:34:46 +0500 Subject: [PATCH 3/9] fix contextPath --- .../java/cane/brothers/spring/security/SecurityConfig.java | 7 ++----- .../spring/authserver/security/SecurityConfig.java | 5 +---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java b/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java index ac5caa8..3198084 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java @@ -2,7 +2,6 @@ import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -26,8 +25,6 @@ class SecurityConfig { private final JwtDecoder jwtDecoder; - @Value("${server.servlet.context-path:/}") - private String contextPath; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -42,8 +39,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http.authorizeHttpRequests(auth -> auth // Разрешить доступ к эндпоинтам Swagger UI без авторизации - .requestMatchers(contextPath + "/v3/api-docs/**", contextPath + "/swagger-ui/**", contextPath + "/swagger-ui.html").permitAll() - .requestMatchers(contextPath + "/sample/**").authenticated() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/sample/**").authenticated() .anyRequest().permitAll() ); // throw Access Denied exception only once diff --git a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java index 19a6409..2ad7db0 100644 --- a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java +++ b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java @@ -10,7 +10,6 @@ import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -44,8 +43,6 @@ @EnableWebSecurity public class SecurityConfig { - @Value("${server.servlet.context-path:/}") - private String contextPath; @Bean @Order(1) @@ -66,7 +63,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h // from the authorization endpoint http.exceptionHandling(exceptions -> exceptions .defaultAuthenticationEntryPointFor( - new LoginUrlAuthenticationEntryPoint(contextPath + "/oauth2/authorization/github"), + new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/github"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML) ) ); From fdbf40af3acf76c84ef5880065767e3398d1db0a Mon Sep 17 00:00:00 2001 From: Mikhail Niedre Date: Mon, 29 Dec 2025 15:01:30 +0500 Subject: [PATCH 4/9] add docu --- README.md | 604 ++++++++++++++++++ .../authserver/security/SecurityConfig.java | 25 +- 2 files changed, 624 insertions(+), 5 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff36f2b --- /dev/null +++ b/README.md @@ -0,0 +1,604 @@ +# Spring Authorization Server with Federated Identity + +## Overview + +This project demonstrates a **proper separation of concerns** in OAuth2/OpenID Connect architecture by implementing a **Spring Authorization Server** that uses GitHub as an external Identity Provider (IdP) for user authentication. + +### The Problem It Solves + +Instead of each application managing OAuth2 integrations with external IdPs (GitHub, Google, Azure AD, etc.), this architecture centralizes authentication: + +``` +┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ +│ │ │ │ │ │ +│ Client App │────────▶│ Authorization │────────▶│ GitHub IdP │ +│ (Resource │ OAuth2 │ Server │ OAuth2 │ (External) │ +│ Server) │◀────────│ (This project) │◀────────│ │ +│ │ JWT │ │ └──────────────┘ +└──────────────┘ └─────────────────┘ +``` + +### Key Benefits + +1. **Centralized Authentication**: One place to manage IdP integration +2. **Separation of Concerns**: Client applications only need to trust your Authorization Server +3. **Token Translation**: Convert GitHub OAuth2 tokens into your own JWT tokens +4. **Consistent API**: Same authentication flow for all client applications +5. **Easy to Scale**: Architecture ready to add more IdPs in the future +6. **Enhanced Security**: Control token format, lifetime, and claims centrally + +### Current Implementation + +- ✅ **GitHub OAuth2 Login** as the authentication provider +- ✅ OAuth2 Authorization Server with OIDC support +- ✅ PKCE (Proof Key for Code Exchange) support +- ✅ JWT token generation with RSA signing +- ✅ Standard Spring Security OAuth2 Login flow + +### Future Extensions (Planned) + +This architecture can be extended to support: + +1. **Federated Identity**: + - Custom `OAuth2UserService` to map external users to internal user model + - `FederatedUser` entity to link multiple IdPs to one account + - User profile management and account linking + +2. **Additional Identity Providers**: + - Google OAuth2 + - Microsoft Azure AD / Entra ID + - Okta / Auth0 + - Custom LDAP/Database authentication + - SAML 2.0 providers + +All extensions can be added without changing client applications! + +## How Authentication Works + +### Current Authentication Flow (OAuth2 Login with GitHub) + +1. **User** accesses a protected resource in the Client App +2. **Client App** redirects to the Authorization Server (`/oauth2/authorize`) +3. **Authorization Server** checks if user is authenticated: + - If not, redirects to GitHub OAuth2 login (`/oauth2/authorization/github`) +4. **User** logs in with GitHub credentials +5. **GitHub** redirects back to Authorization Server with authorization code +6. **Authorization Server**: + - Exchanges code for GitHub access token + - Retrieves user info from GitHub API + - Creates an authenticated session using Spring Security's `OAuth2User` + - Generates its own JWT access token for the client +7. **Client App** receives JWT token from Authorization Server +8. **Client App** validates JWT and grants access to resources + +**Key Point**: The Authorization Server acts as an intermediary, translating GitHub authentication into JWT tokens that client applications can trust and validate. + +### Planned: Federated Identity (Future Enhancement) + +A **federated user** concept will allow unified identity across multiple external IdPs: + +```java +// Planned implementation +FederatedUser { + id: "uuid-1234-5678" + username: "john.doe" + email: "john@example.com" + linkedAccounts: [ + { provider: "github", externalId: "github-123", linkedAt: "2024-01-15" }, + { provider: "google", externalId: "google-456", linkedAt: "2024-02-20" } + ] +} +``` + +**This will require**: +- Custom `OAuth2UserService` implementation +- Database entity for `FederatedUser` +- Account linking logic +- User profile management UI + +**Benefits**: +- Log in with different providers but maintain the same identity +- Link multiple external accounts to one internal account +- Switch between providers without losing access + +## Project Structure + +``` +hello-spring-oauth2/ +├── hello-sample-sas/ # Spring Authorization Server +│ ├── src/main/java/cane/brothers/spring/authserver/ +│ │ ├── App.java # Main application entry point +│ │ ├── security/ +│ │ │ └── SecurityConfig.java # OAuth2 & Security configuration +│ │ └── web/ +│ │ └── DevToolsController.java # Development utilities +│ ├── src/main/resources/ +│ │ └── application.yml # Server configuration with GitHub IdP +│ ├── build.gradle # Dependencies & build configuration +│ └── Dockerfile # Container image +│ +├── hello-sample-app/ # Sample Resource Server (Client App) +│ ├── src/main/java/cane/brothers/spring/ +│ │ ├── App.java # Main application +│ │ ├── sample/ # Business logic & REST API +│ │ ├── security/ # JWT validation & authorities +│ │ └── swagger/ # API documentation with OAuth2 +│ ├── src/main/resources/ +│ │ └── application.yml # JWT validation configuration +│ ├── build.gradle +│ └── Dockerfile +│ +├── nginx/ # Reverse proxy +│ ├── nginx.conf # HTTPS termination & routing +│ ├── certs/ # SSL certificates +│ │ ├── server.crt +│ │ └── server.key +│ └── SETUP.md # Nginx configuration guide +│ +├── compose.yaml # Docker Compose orchestration +├── Makefile # Convenient commands +├── .env # Environment variables (not in repo) +└── QUICK-REFERENCE.md # Configuration reference + +``` + +### Key Components + +#### Authorization Server (`hello-sample-sas`) + +- **Purpose**: Central authentication & token issuer +- **Technology**: Spring Authorization Server 1.3+ +- **Features**: + - OAuth2 Authorization Code flow with PKCE + - OpenID Connect 1.0 (OIDC) + - OAuth2 Client (for GitHub IdP integration) + - JWT token generation with RSA keys + - Session management + - Health checks & actuator endpoints + +#### Resource Server (`hello-sample-app`) + +- **Purpose**: Example application with protected API +- **Technology**: Spring Boot 3.5+ with Spring Security +- **Features**: + - JWT validation from Authorization Server + - Swagger UI with OAuth2 integration + - Custom authority mapping from JWT claims + - CORS configuration + - Protected REST endpoints + +#### Nginx Proxy + +- **Purpose**: HTTPS termination & request routing +- **Features**: + - SSL/TLS support + - Path-based routing: + - `/auth` → Authorization Server + - `/api` → Resource Server + - Header forwarding for proper OAuth2 redirects + +## Configuration + +### GitHub IdP Setup + +1. **Register OAuth App** in GitHub: + - Go to: Settings → Developer settings → OAuth Apps → New OAuth App + - **Application name**: `Hello Spring Auth` + - **Homepage URL**: `https://localhost/auth` + - **Authorization callback URL**: `https://localhost/auth/login/oauth2/code/github` + +2. **Get Credentials**: + - Copy `Client ID` and `Client Secret` + +3. **Configure Environment**: + +Create `.env` file in project root: + +```bash +# Server ports (internal) +SAS_SERVER_PORT=9000 +APP_SERVER_PORT=8080 + +# External URLs (browser-facing) +SAS_SERVER_EXTERNAL=https://localhost/auth +APP_SERVER=https://localhost/api + +# GitHub OAuth2 credentials +SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_ID=your_github_client_id +SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_SECRET=your_github_client_secret + +# Management endpoints +MANAGEMENT_ENDPOINTS_WEB_BASE_PATH=/management +``` + +### SSL Certificates + +For local development, you need SSL certificates for HTTPS: + +```bash +# See nginx/SETUP.md for detailed instructions +cd nginx/certs +# Generate self-signed certificate (if not exists) +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout server.key -out server.crt \ + -subj "/CN=localhost" +``` + +## Running the Application + +### Prerequisites + +- Docker & Docker Compose +- Make (optional, for convenience commands) +- GitHub OAuth App credentials + +### Using Make Commands + +```bash +# Start all services (build & run in detached mode) +make up + +# View logs (follow mode) +make logs + +# Stop services +make down + +# Stop services and remove volumes +make downv + +# Access Authorization Server container +make bash-sas + +# Access Sample App container +make bash-app +``` + +### Using Docker Compose Directly + +```bash +# Start services +docker compose up --build --detach + +# View logs +docker compose logs -f + +# Stop services +docker compose down + +# Stop and remove volumes +docker compose down --remove-orphans -v +``` + +### Manual Local Development + +#### Terminal 1 - Authorization Server + +```bash +cd hello-sample-sas +./gradlew bootRun +``` + +#### Terminal 2 - Resource Server + +```bash +cd hello-sample-app +./gradlew bootRun +``` + +#### Terminal 3 - Nginx Proxy + +```bash +# Make sure nginx is installed +# On macOS: brew install nginx +cd nginx +nginx -c $(pwd)/nginx.conf -p $(pwd) +``` + +## Testing the Setup + +### 1. Health Checks + +```bash +# Authorization Server +curl -k https://localhost/auth/management/health + +# Resource Server +curl -k https://localhost/api/management/health +``` + +### 2. OpenID Configuration + +```bash +# View Authorization Server metadata +curl -k https://localhost/auth/.well-known/openid-configuration | jq +``` + +### 3. Swagger UI with OAuth2 + +1. Open browser: https://localhost/api/swagger-ui +2. Click **"Authorize"** button +3. Select scopes: `openid`, `api.read` +4. Click **"Authorize"** +5. Redirect to GitHub login +6. Authorize the application +7. You're authenticated! Try protected endpoints + +### 4. Direct OAuth2 Flow + +```bash +# Step 1: Get authorization code (open in browser) +https://localhost/auth/oauth2/authorize?response_type=code&client_id=swagger-ui&redirect_uri=https://localhost/api/swagger-ui/oauth2-redirect.html&scope=openid%20api.read&code_challenge=CHALLENGE&code_challenge_method=S256 + +# Step 2: Exchange code for token (use Postman/curl) +curl -X POST https://localhost/auth/oauth2/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=authorization_code" \ + -d "code=YOUR_CODE" \ + -d "redirect_uri=https://localhost/api/swagger-ui/oauth2-redirect.html" \ + -d "client_id=swagger-ui" \ + -d "code_verifier=VERIFIER" +``` + +## Key Endpoints + +### Authorization Server (Port 9000, Path /auth) + +| Endpoint | Description | +|----------|-------------| +| `GET /auth/oauth2/authorize` | OAuth2 authorization endpoint | +| `POST /auth/oauth2/token` | Token endpoint | +| `GET /auth/oauth2/jwks` | JSON Web Key Set (public keys) | +| `GET /auth/.well-known/openid-configuration` | OIDC discovery | +| `GET /auth/userinfo` | OIDC user info endpoint | +| `GET /auth/oauth2/authorization/github` | Redirect to GitHub login | +| `GET /auth/login/oauth2/code/github` | GitHub callback URL | + +### Resource Server (Port 8080, Path /api) + +| Endpoint | Description | +|----------|-------------| +| `GET /api/swagger-ui` | Swagger UI with OAuth2 | +| `GET /api/v3/api-docs` | OpenAPI specification | +| `GET /api/samples` | Protected API endpoint (requires JWT) | + +## Architecture Decisions + +### Why Spring Authorization Server? + +1. **Official Implementation**: Spring Security's official OAuth2 server +2. **Production-Ready**: Battle-tested, secure, maintained +3. **Flexible**: Highly customizable for federated identity +4. **Standards-Compliant**: OAuth2.1, OIDC 1.0, PKCE +5. **Integration**: Seamless with Spring ecosystem + +### Why OAuth2 Login as Identity Provider? + +**Current approach**: Using Spring Security OAuth2 Login to integrate with GitHub: + +1. **Quick Setup**: Minimal configuration required +2. **Standard Flow**: Industry-standard OAuth2 authorization code flow +3. **Built-in Support**: Spring Security handles token exchange, user info retrieval +4. **Extensible**: Easy to add more providers (Google, Azure AD, etc.) + +**Limitation**: Each IdP creates separate user identities without federated linking. + +### Future: Federated Identity + +**Planned enhancement** to link multiple IdP accounts to a single user: + +1. **User Convenience**: Let users choose their preferred login method +2. **No Password Management**: Delegate to trusted IdPs +3. **Single Identity**: One user account, multiple login options +4. **Compliance**: Meet enterprise SSO requirements +5. **Future-Proof**: Easy to add new authentication methods + +**Implementation requires**: +- Custom `OAuth2UserService` for user mapping +- `FederatedUser` entity and repository +- Account linking logic +- User profile management UI + +### Security Considerations + +- ✅ HTTPS required for all endpoints +- ✅ PKCE enabled for public clients +- ✅ JWT signing with RSA-2048 keys +- ✅ Token expiration (1 hour default) +- ✅ CORS properly configured +- ✅ Forward headers strategy for proxy +- ⚠️ Self-signed certificates (use real CA in production) +- ⚠️ In-memory key storage (use persistent in production) +- ⚠️ No user persistence yet (sessions only) + +## Adding More Identity Providers + +### Example: Adding Google IdP (Configuration Only) + +You can add more OAuth2 providers using Spring Security's standard OAuth2 Login: + +1. **Register app** in Google Cloud Console +2. **Update** `application.yml` in `hello-sample-sas`: + +```yaml +spring: + security: + oauth2: + client: + registration: + github: + # ... existing GitHub config + google: + provider: google + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + scope: openid,profile,email + provider: + google: + issuer-uri: https://accounts.google.com +``` + +3. **Add environment variables** to `.env` +4. **Update entry point** in `SecurityConfig.java` (optional - to offer IdP selection) + +**Current Limitation**: Without federated identity implementation, each provider creates a separate user session. Users logging in via GitHub and Google would be treated as different users, even with the same email. + +### Implementing Federated Identity (Roadmap) + +To properly link multiple IdPs to the same user account, you need to implement: + +**1. Create `FederatedUser` entity**: +```java +@Entity +public class FederatedUser { + @Id + private UUID id; + private String email; + private String username; + + @OneToMany(mappedBy = "user") + private Set linkedAccounts; +} + +@Entity +public class LinkedAccount { + @Id + private UUID id; + private String provider; // "github", "google" + private String externalId; + private Instant linkedAt; + + @ManyToOne + private FederatedUser user; +} +``` + +**2. Implement custom `OAuth2UserService`**: +```java +@Service +public class FederatedUserService implements OAuth2UserService { + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) { + // Delegate to default implementation + OAuth2UserService delegate = + new DefaultOAuth2UserService(); + OAuth2User oauth2User = delegate.loadUser(userRequest); + + // Extract provider and user info + String provider = userRequest.getClientRegistration().getRegistrationId(); + String email = oauth2User.getAttribute("email"); + String externalId = oauth2User.getName(); + + // Find or create federated user + FederatedUser user = findOrCreateFederatedUser(email, provider, externalId); + + // Return custom user with federated identity + return new FederatedOAuth2User(user, oauth2User); + } + + private FederatedUser findOrCreateFederatedUser(String email, String provider, String externalId) { + // Find existing user by email or linked account + FederatedUser user = userRepository.findByEmail(email) + .orElseGet(() -> createNewUser(email)); + + // Link account if not already linked + if (!user.hasLinkedAccount(provider, externalId)) { + user.linkAccount(provider, externalId); + userRepository.save(user); + } + + return user; + } +} +``` + +**3. Register custom service in `SecurityConfig`**: +```java +@Bean +@Order(2) +public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + // ...existing config... + + http.oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(userInfo -> userInfo + .userService(federatedUserService) + ) + ); + + return http.build(); +} +``` + +**4. Update token generation** to include federated user ID in JWT claims + +## Troubleshooting + +### Common Issues + +**1. SSL Certificate Errors** + +```bash +# Trust self-signed certificate in browser +# Or disable SSL verification (dev only): +curl -k https://localhost/... +``` + +**2. GitHub OAuth Callback Mismatch** + +- Verify GitHub OAuth App callback URL: `https://localhost/auth/login/oauth2/code/github` +- Check `.env` variables match + +**3. Token Validation Fails** + +- Ensure `SAS_SERVER` points to internal service: `http://sas:9000/auth` +- Check JWKS endpoint is accessible: `curl http://sas:9000/auth/oauth2/jwks` + +**4. CORS Errors** + +- Check `management.endpoints.web.cors` configuration +- Verify nginx proxy headers + +### Debug Mode + +Enable detailed logging in `application.yml`: + +```yaml +logging: + level: + org.springframework.security: TRACE + org.springframework.security.oauth2: TRACE +``` + +## Production Considerations + +Before deploying to production: + +- [ ] Use real SSL certificates from trusted CA +- [ ] Store client secrets in secure vault (not `.env`) +- [ ] Implement persistent key storage (database or HSM) +- [ ] Configure token refresh flow +- [ ] Implement user consent screens +- [ ] Add user profile management +- [ ] Set up monitoring & alerting +- [ ] Configure session clustering +- [ ] Implement rate limiting +- [ ] Add audit logging +- [ ] Use production-ready database for authorization data +- [ ] Implement federated identity with `OAuth2UserService` and `FederatedUser` entity + +## References + +- [Spring Authorization Server Documentation](https://docs.spring.io/spring-authorization-server/reference/) +- [OAuth 2.1 Specification](https://oauth.net/2.1/) +- [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html) +- [RFC 7636 - PKCE](https://tools.ietf.org/html/rfc7636) + +## License + +This is a sample project for educational purposes. + +## Contributing + +Feel free to submit issues and enhancement requests! + diff --git a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java index 2ad7db0..66e6ff3 100644 --- a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java +++ b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java @@ -46,23 +46,37 @@ public class SecurityConfig { @Bean @Order(1) + // Security filter chain for the authorization server endpoints (OAuth2 and OIDC) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer.authorizationServer(); + + // Apply the authorization server configuration to the HttpSecurity + // Handles: + // - /oauth2/authorize, + // - /oauth2/token, + // - /oauth2/jwks, + // - /.well-known/openid-configuration - OIDC discovery + // - /oauth2/introspect, + // - /oauth2/revoke endpoints http.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()); http.with(authorizationServerConfigurer, sas -> sas.oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0 ); + + // Require authentication for all requests to the authorization server endpoints http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated() ); + // Enable CORS with default settings (defined over application properties) http.cors(Customizer.withDefaults()); // Redirect to the OAuth 2.0 Login endpoint when not authenticated // from the authorization endpoint http.exceptionHandling(exceptions -> exceptions .defaultAuthenticationEntryPointFor( + // redirect to GitHub OAuth2 login page. (OAuth2 Login as Federated Identity Provider) new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/github"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML) ) @@ -72,6 +86,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h @Bean @Order(2) + // default security filter chain for other endpoints public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated()); @@ -90,19 +105,19 @@ public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } + // to track sessions for logged in users @Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } + // to publish session events (session created, destroyed) @Bean public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } - // for signing access tokens - // configure JWK Set endpoint - // required for OpenID Connect 1.0 endpoints + // for signing access tokens. (configure JWK Set endpoint. required for OpenID Connect 1.0 endpoints) @Bean public JWKSource jwkSource() { RSAKey rsaKey = generateRsaKey(); @@ -110,6 +125,7 @@ public JWKSource jwkSource() { return new ImmutableJWKSet<>(jwkSet); } + // RSA key pair setting for signing access tokens private static RSAKey generateRsaKey() { RSAKey rsaKey; try { @@ -136,8 +152,7 @@ public JwtDecoder jwtDecoder(JWKSource jwkSource) { ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>(jwsAlgs, jwkSource); jwtProcessor.setJWSKeySelector(jwsKeySelector); - // Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it - // instead + // Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it instead jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); return new NimbusJwtDecoder(jwtProcessor); From d61666f280c96b592104d86fafdff9ff54cac174 Mon Sep 17 00:00:00 2001 From: Mikhail Niedre Date: Mon, 29 Dec 2025 18:38:52 +0500 Subject: [PATCH 5/9] nginx dynamically resolve app's --- Makefile | 3 ++ README.md | 36 +++++++++++++++++++ compose.yaml | 11 +++--- .../cane/brothers/spring/sample/Sample.java | 1 + .../brothers/spring/sample/SampleApi.java | 20 ++++++----- .../spring/sample/SampleController.java | 2 +- .../spring/security/JwtDecoderConfig.java | 21 ----------- .../spring/security/SecurityConfig.java | 16 ++++----- .../src/main/resources/application.yml | 2 +- .../authserver/security/SecurityConfig.java | 31 ++++++++++++---- nginx/SETUP.md | 30 ++++++++-------- nginx/nginx.conf | 27 ++++++++++++-- 12 files changed, 130 insertions(+), 70 deletions(-) delete mode 100644 hello-sample-app/src/main/java/cane/brothers/spring/security/JwtDecoderConfig.java diff --git a/Makefile b/Makefile index 27e3bb9..bf8c041 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,9 @@ downv: logs: @docker compose logs -f +restart-nginx: + @docker compose restart nginx + # BASH ACCESS diff --git a/README.md b/README.md index ff36f2b..90718d0 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,42 @@ All extensions can be added without changing client applications! **Key Point**: The Authorization Server acts as an intermediary, translating GitHub authentication into JWT tokens that client applications can trust and validate. +### JWT Access Token Structure + +The Authorization Server issues JWT access tokens with the following claims: + +```json +{ + "sub": "github_username", + "aud": "swagger-ui", + "nbf": 1767003392, + "scope": [ + "openid", + "api.read" + ], + "iss": "https://localhost/auth", + "exp": 1767006992, + "iat": 1767003392, + "jti": "27231829-a905-4c82-a043-c9ad44cdc6bz" +} +``` + +**Claim Descriptions**: +- `sub` (Subject): Username from the external IdP (GitHub username) +- `aud` (Audience): Client ID that the token was issued for (e.g., `swagger-ui`) +- `nbf` (Not Before): Timestamp when the token becomes valid +- `scope`: Granted OAuth2 scopes (e.g., `openid`, `api.read`) +- `iss` (Issuer): Authorization Server URL (`https://localhost/auth`) +- `exp` (Expiration): Timestamp when the token expires (default: 1 hour) +- `iat` (Issued At): Timestamp when the token was issued +- `jti` (JWT ID): Unique identifier for this token + +The Resource Server validates these tokens by: +1. Fetching public keys from the Authorization Server's JWKS endpoint (`/oauth2/jwks`) +2. Verifying the JWT signature using RSA-2048 +3. Checking token expiration and audience claims +4. Extracting scopes to determine granted authorities + ### Planned: Federated Identity (Future Enhancement) A **federated user** concept will allow unified identity across multiple external IdPs: diff --git a/compose.yaml b/compose.yaml index d41d299..34b9731 100644 --- a/compose.yaml +++ b/compose.yaml @@ -10,11 +10,6 @@ services: volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/certs:/etc/nginx/certs:ro - depends_on: - sas: - condition: service_healthy - app: - condition: service_healthy networks: network: { } @@ -28,6 +23,8 @@ services: env_file: - .env environment: + - SAS_SERVER_EXTERNAL=https://localhost/auth + - APP_SERVER=https://localhost/api - GITHUB_CLIENT_ID=$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_ID - GITHUB_CLIENT_SECRET=$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_SECRET healthcheck: @@ -48,6 +45,10 @@ services: - "$APP_SERVER_PORT" env_file: - .env + environment: + - SAS_SERVER=http://sas:9000/auth + - SAS_SERVER_EXTERNAL=https://localhost/auth + - APP_SERVER=https://localhost/api depends_on: sas: condition: service_healthy diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/sample/Sample.java b/hello-sample-app/src/main/java/cane/brothers/spring/sample/Sample.java index ec87f73..f032d8c 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/sample/Sample.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/sample/Sample.java @@ -2,4 +2,5 @@ import java.util.UUID; +// Simple record representing a Sample entity record Sample(UUID sampleId, Long sampleKey, String status) {} \ No newline at end of file diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleApi.java b/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleApi.java index 18d799a..65c8bfb 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleApi.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleApi.java @@ -9,20 +9,22 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -@Tag(name = "Sample API", description = "API для работы с Sample") +@Tag(name = "Sample API", description = "API for working with Samples") interface SampleApi { - @Operation(summary = "Получить Sample по sampleKey") + @Operation(summary = "Get Sample by sampleKey") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Sample найден", content = @Content(schema = @Schema(implementation = Sample.class))), - @ApiResponse(responseCode = "404", description = "Sample не найден") + @ApiResponse(responseCode = "200", description = "Sample found", + content = @Content(schema = @Schema(implementation = Sample.class))), + @ApiResponse(responseCode = "404", description = "Sample not found") }) ResponseEntity getSample( - @Parameter(description = "Ключ sample", required = true) Long sampleKey); + @Parameter(description = "Sample key", required = true) Long sampleKey); - @Operation(summary = "Создать новый Sample") + @Operation(summary = "Create new Sample") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Sample создан", content = @Content(schema = @Schema(implementation = Sample.class))) + @ApiResponse(responseCode = "200", description = "Sample created", + content = @Content(schema = @Schema(implementation = Sample.class))) }) ResponseEntity createSample( - @Parameter(description = "Sample для создания", required = true) Sample sample); -} \ No newline at end of file + @Parameter(description = "Sample to create", required = true) Sample sample); +} \ No newline at end of file diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleController.java b/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleController.java index 34fae4c..312abd0 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleController.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/sample/SampleController.java @@ -7,7 +7,7 @@ import java.util.UUID; @RestController -@RequestMapping("/api/sample") +@RequestMapping("/sample") class SampleController implements SampleApi { private final SampleRepo repo; diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/security/JwtDecoderConfig.java b/hello-sample-app/src/main/java/cane/brothers/spring/security/JwtDecoderConfig.java deleted file mode 100644 index a35628b..0000000 --- a/hello-sample-app/src/main/java/cane/brothers/spring/security/JwtDecoderConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package cane.brothers.spring.security; - -import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; - -import java.net.MalformedURLException; - -@Configuration -public class JwtDecoderConfig { - - - @Bean - public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) throws MalformedURLException { - // jwt decoder required for validating tokens - return NimbusJwtDecoder.withIssuerLocation(properties.getJwt().getIssuerUri()) - .build(); - } -} diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java b/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java index 3198084..cac1339 100644 --- a/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java +++ b/hello-sample-app/src/main/java/cane/brothers/spring/security/SecurityConfig.java @@ -12,7 +12,6 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.web.cors.CorsConfigurationSource; @@ -23,8 +22,6 @@ @RequiredArgsConstructor class SecurityConfig { - private final JwtDecoder jwtDecoder; - @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -38,21 +35,22 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http.authorizeHttpRequests(auth -> auth - // Разрешить доступ к эндпоинтам Swagger UI без авторизации + // Allow access to Swagger UI endpoints without authorization .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .requestMatchers("/sample/**").authenticated() .anyRequest().permitAll() ); // throw Access Denied exception only once - http.exceptionHandling(e -> e.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.FORBIDDEN))); + http.exceptionHandling(e -> + e.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.FORBIDDEN))); // disables session creation on Spring Security - http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + http.sessionManagement(s -> + s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - // authentication + // authentication. validate access tokens http.oauth2ResourceServer(c -> - c.jwt(jwt -> jwt.decoder(jwtDecoder)) -// c.jwt(Customizer.withDefaults()) + c.jwt(Customizer.withDefaults()) ); return http.build(); diff --git a/hello-sample-app/src/main/resources/application.yml b/hello-sample-app/src/main/resources/application.yml index 9cc3a71..d53def5 100644 --- a/hello-sample-app/src/main/resources/application.yml +++ b/hello-sample-app/src/main/resources/application.yml @@ -41,7 +41,7 @@ spring: jwt: jws-algorithms: RS256 jwk-set-uri: ${SAS_SERVER:http://hello-sample-sas:9000/auth}/oauth2/jwks - issuer-uri: ${SAS_SERVER:http://hello-sample-sas:9000/auth} + issuer-uri: ${SAS_SERVER_EXTERNAL:https://localhost/auth} # Увеличиваем таймаут на установление соединения (в миллисекундах) jwk-set-uri-connect-timeout: 3000 # Увеличиваем таймаут на чтение ответа diff --git a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java index 66e6ff3..a993662 100644 --- a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java +++ b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/security/SecurityConfig.java @@ -10,7 +10,9 @@ import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -21,6 +23,7 @@ import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; @@ -43,7 +46,6 @@ @EnableWebSecurity public class SecurityConfig { - @Bean @Order(1) // Security filter chain for the authorization server endpoints (OAuth2 and OIDC) @@ -105,7 +107,7 @@ public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } - // to track sessions for logged in users + // to track logged in users sessions @Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); @@ -138,24 +140,39 @@ private static RSAKey generateRsaKey() { .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); - } - catch (Exception ex) { + } catch (Exception ex) { throw new IllegalStateException(ex); } return rsaKey; } - // for decoding signed access tokens + // JWT processor for processing and verifying JWT tokens @Bean - public JwtDecoder jwtDecoder(JWKSource jwkSource) { + public ConfigurableJWTProcessor jwtProcessor(JWKSource jwkSource) { Set jwsAlgs = new HashSet<>(JWSAlgorithm.Family.RSA); ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>(jwsAlgs, jwkSource); jwtProcessor.setJWSKeySelector(jwsKeySelector); + // Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it instead jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); - return new NimbusJwtDecoder(jwtProcessor); + return jwtProcessor; + } + + // for decoding signed access tokens + @Bean + public JwtDecoder jwtDecoder(ConfigurableJWTProcessor jwtProcessor, + @Value("${SAS_SERVER}") String issuerUri) { + NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor); +// NimbusJwtDecoder decoder = NimbusJwtDecoder +// .withIssuerLocation(issuerUri) +// .jwtProcessorCustomizer(proc -> proc = jwtProcessor) +// .build(); + // validate issuer, signature, expiration time +// decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri)); + + return decoder; } @Bean diff --git a/nginx/SETUP.md b/nginx/SETUP.md index 6d925c0..2a3cc48 100644 --- a/nginx/SETUP.md +++ b/nginx/SETUP.md @@ -1,6 +1,6 @@ -# SSL через Nginx - Конфигурация +# SSL via Nginx - Configuration -## Архитектура +## Architecture ``` Browser (HTTPS) → Nginx (SSL termination) → Services (HTTP inside Docker) @@ -13,26 +13,26 @@ Browser (HTTPS) → Nginx (SSL termination) → Services (HTTP inside Docker) (HTTP + context-path=/auth) (HTTP + context-path=/api) ``` -## Ключевые моменты +## Key Points -1. **SSL на уровне Nginx**: - - Сертификаты в `nginx/certs/` (server.crt, server.key) - - Порт 443 открыт наружу только у nginx +1. **SSL at Nginx level**: + - Certificates in `nginx/certs/` (server.crt, server.key) + - Port 443 is only exposed externally on nginx 2. **Servlet Context Path**: - hello-sample-sas: `context-path=/auth` - hello-sample-app: `context-path=/api` - - Это исключает конфликты путей + - This prevents path conflicts -3. **URL маппинг**: - - Внешний: `https://localhost/auth/*` → Внутренний: `http://hello-sample-sas:9000/auth/*` - - Внешний: `https://localhost/api/*` → Внутренний: `http://hello-sample-app:8080/api/*` +3. **URL mapping**: + - External: `https://localhost/auth/*` → Internal: `http://hello-sample-sas:9000/auth/*` + - External: `https://localhost/api/*` → Internal: `http://hello-sample-app:8080/api/*` 4. **OAuth2 Redirect URLs**: - - Используют публичные HTTPS URL через nginx - - Внутренние вызовы (jwk-set-uri, issuer-uri) используют HTTP внутри docker + - Use public HTTPS URLs via nginx + - Internal calls (jwk-set-uri, issuer-uri) use HTTP inside docker -## Доступ к сервисам +## Service Access - Auth Server: https://localhost/auth/ - Sample App: https://localhost/api/ @@ -41,11 +41,11 @@ Browser (HTTPS) → Nginx (SSL termination) → Services (HTTP inside Docker) - https://localhost/auth/management/health - https://localhost/api/management/health -## Запуск +## Startup ```bash docker compose up --build ``` -При первом подключении браузер попросит принять самоподписанный сертификат. +On first connection the browser will ask you to accept the self-signed certificate. diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 5de6451..a5524bf 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -16,8 +16,27 @@ http { ssl_certificate /etc/nginx/certs/server.crt; ssl_certificate_key /etc/nginx/certs/server.key; + # use Docker's internal DNS resolver for hostname lookups. + resolver 127.0.0.11 valid=30s; + + location /.well-known/ { + # dynamically resolve the "sas" + # resolve the hostnames at request time (runtime) rather than at configuration parsing time (startup) + set $upstream_sas sas; + proxy_pass http://$upstream_sas:9000; + # proxy_pass http://host.docker.internal:8000; # local + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /auth/ { - proxy_pass http://sas:9000; + # dynamically resolve the "sas" + # resolve the hostnames at request time (runtime) rather than at configuration parsing time (startup) + set $upstream_sas sas; + proxy_pass http://$upstream_sas:9000; + # proxy_pass http://host.docker.internal:8000; # local proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -27,7 +46,11 @@ http { } location /api/ { - proxy_pass http://app:8080; + # dynamically resolve the "app" + # resolve the hostnames at request time (runtime) rather than at configuration parsing time (startup) + set $upstream_app app; + proxy_pass http://$upstream_app:8080; +# proxy_pass http://host.docker.internal:8080; # local proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From eea286dba222fe3429efac4ea5929fb224550367 Mon Sep 17 00:00:00 2001 From: Mikhail Niedre Date: Mon, 29 Dec 2025 18:45:11 +0500 Subject: [PATCH 6/9] nginx pass upstream errors without transformation --- nginx/nginx.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index a5524bf..d25255c 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -29,6 +29,7 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_intercept_errors off; } location /auth/ { @@ -43,6 +44,7 @@ http { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port 443; + proxy_intercept_errors off; } location /api/ { @@ -57,6 +59,7 @@ http { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port 443; + proxy_intercept_errors off; } } } From 8fc5628e25af0ce26f10278371e5729b50e8792c Mon Sep 17 00:00:00 2001 From: Mikhail Niedre Date: Mon, 29 Dec 2025 18:52:50 +0500 Subject: [PATCH 7/9] fix GET /.well-known/appspecific/com.chrome.devtools.json on nginx --- .../spring/authserver/web/DevToolsController.java | 14 -------------- nginx/nginx.conf | 12 ++---------- 2 files changed, 2 insertions(+), 24 deletions(-) delete mode 100644 hello-sample-sas/src/main/java/cane/brothers/spring/authserver/web/DevToolsController.java diff --git a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/web/DevToolsController.java b/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/web/DevToolsController.java deleted file mode 100644 index 316e8bf..0000000 --- a/hello-sample-sas/src/main/java/cane/brothers/spring/authserver/web/DevToolsController.java +++ /dev/null @@ -1,14 +0,0 @@ -package cane.brothers.spring.authserver.web; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -class DevToolsController { - - @GetMapping("/.well-known/appspecific/com.chrome.devtools.json") - public ResponseEntity handleDevToolsRequest() { - return ResponseEntity.ok("{}"); - } -} diff --git a/nginx/nginx.conf b/nginx/nginx.conf index d25255c..00edab5 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -20,16 +20,8 @@ http { resolver 127.0.0.11 valid=30s; location /.well-known/ { - # dynamically resolve the "sas" - # resolve the hostnames at request time (runtime) rather than at configuration parsing time (startup) - set $upstream_sas sas; - proxy_pass http://$upstream_sas:9000; - # proxy_pass http://host.docker.internal:8000; # local - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_intercept_errors off; + default_type application/json; + return 200 '{}'; } location /auth/ { From 6f8a5fa9a188cde45c24ce492fb99f47499a88d3 Mon Sep 17 00:00:00 2001 From: Mikhail Niedre Date: Mon, 29 Dec 2025 18:53:17 +0500 Subject: [PATCH 8/9] remove extra http client settings --- .../brothers/spring/security/RestConfig.java | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 hello-sample-app/src/main/java/cane/brothers/spring/security/RestConfig.java diff --git a/hello-sample-app/src/main/java/cane/brothers/spring/security/RestConfig.java b/hello-sample-app/src/main/java/cane/brothers/spring/security/RestConfig.java deleted file mode 100644 index ef65ff0..0000000 --- a/hello-sample-app/src/main/java/cane/brothers/spring/security/RestConfig.java +++ /dev/null @@ -1,47 +0,0 @@ -package cane.brothers.spring.security; - -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.web.client.RestOperations; - -import java.time.Duration; - -@Configuration -public class RestConfig { - - @Bean - public RestOperations restOperations() { - return new RestTemplateBuilder() - .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) - .build(); - } - - @Bean - public CloseableHttpClient httpClient() { - // Configure connection manager with TTL - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); - connectionManager.setMaxTotal(100); // Maximum total connections - connectionManager.setDefaultMaxPerRoute(20); // Maximum connections per route - - // Build the HttpClient - return HttpClients.custom() - .setConnectionManager(connectionManager) - .build(); - } - - @Bean - public ClientHttpRequestFactory clientHttpRequestFactory() { - var connectionTimeout = 5000; - var readTimeout = 5000; - HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); - factory.setConnectTimeout(Duration.ofMillis(connectionTimeout)); - factory.setReadTimeout(Duration.ofMillis(readTimeout)); - return factory; - } -} From f66b4ad86959368528256e55a35ac69c8ae17acf Mon Sep 17 00:00:00 2001 From: Mikhail Niedre Date: Mon, 29 Dec 2025 19:00:12 +0500 Subject: [PATCH 9/9] add build pipeline --- .github/workflows/build.yml | 45 +++++++++ QUICK-REFERENCE.md | 8 -- README.md | 185 ++++++++++++++++++++---------------- 3 files changed, 150 insertions(+), 88 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b6f0b5f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,45 @@ +name: Build + +on: + push: + branches: [ main, master, develop, nginx-ssl ] + pull_request: + branches: [ main, master, develop ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'gradle' + + - name: Build SAS + working-directory: ./hello-sample-sas + run: | + chmod +x ./gradlew + ./gradlew build --no-daemon + + - name: Build APP (API) + working-directory: ./hello-sample-app + run: | + chmod +x ./gradlew + ./gradlew build --no-daemon + + - name: Upload build artifacts + if: success() + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + hello-sample-sas/build/libs/*.jar + hello-sample-app/build/libs/*.jar + retention-days: 7 + diff --git a/QUICK-REFERENCE.md b/QUICK-REFERENCE.md index 7f9ac63..88ab7b6 100644 --- a/QUICK-REFERENCE.md +++ b/QUICK-REFERENCE.md @@ -82,11 +82,3 @@ open https://localhost/api/swagger-ui │ :9000 │ (internal) │ :8080 │ └───────────┘ └───────────┘ ``` - -## Files Modified - -- ✅ `.env` - Added `SAS_SERVER_EXTERNAL` -- ✅ `nginx/nginx.conf` - Added X-Forwarded headers -- ✅ `hello-sample-app/src/main/resources/application.yml` - Updated OAuth URLs -- ✅ `hello-sample-sas/src/main/resources/application.yml` - Updated CORS - diff --git a/README.md b/README.md index 90718d0..9299d2e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # Spring Authorization Server with Federated Identity +[![Build](https://github.com/webcane/hello-spring-oauth2/actions/workflows/build.yml/badge.svg)](https://github.com/webcane/hello-spring-oauth2/actions/workflows/build.yml) + ## Overview -This project demonstrates a **proper separation of concerns** in OAuth2/OpenID Connect architecture by implementing a **Spring Authorization Server** that uses GitHub as an external Identity Provider (IdP) for user authentication. +This project demonstrates a **proper separation of concerns** in OAuth2/OpenID Connect architecture by implementing a * +*Spring Authorization Server** that uses GitHub as an external Identity Provider (IdP) for user authentication. ### The Problem It Solves -Instead of each application managing OAuth2 integrations with external IdPs (GitHub, Google, Azure AD, etc.), this architecture centralizes authentication: +Instead of each application managing OAuth2 integrations with external IdPs (GitHub, Google, Azure AD, etc.), this +architecture centralizes authentication: ``` ┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ @@ -14,7 +18,7 @@ Instead of each application managing OAuth2 integrations with external IdPs (Git │ Client App │────────▶│ Authorization │────────▶│ GitHub IdP │ │ (Resource │ OAuth2 │ Server │ OAuth2 │ (External) │ │ Server) │◀────────│ (This project) │◀────────│ │ -│ │ JWT │ │ └──────────────┘ +│ │ own JWT │ │ JWT └──────────────┘ └──────────────┘ └─────────────────┘ ``` @@ -40,16 +44,16 @@ Instead of each application managing OAuth2 integrations with external IdPs (Git This architecture can be extended to support: 1. **Federated Identity**: - - Custom `OAuth2UserService` to map external users to internal user model - - `FederatedUser` entity to link multiple IdPs to one account - - User profile management and account linking + - Custom `OAuth2UserService` to map external users to internal user model + - `FederatedUser` entity to link multiple IdPs to one account + - User profile management and account linking 2. **Additional Identity Providers**: - - Google OAuth2 - - Microsoft Azure AD / Entra ID - - Okta / Auth0 - - Custom LDAP/Database authentication - - SAML 2.0 providers + - Google OAuth2 + - Microsoft Azure AD / Entra ID + - Okta / Auth0 + - Custom LDAP/Database authentication + - SAML 2.0 providers All extensions can be added without changing client applications! @@ -60,18 +64,19 @@ All extensions can be added without changing client applications! 1. **User** accesses a protected resource in the Client App 2. **Client App** redirects to the Authorization Server (`/oauth2/authorize`) 3. **Authorization Server** checks if user is authenticated: - - If not, redirects to GitHub OAuth2 login (`/oauth2/authorization/github`) + - If not, redirects to GitHub OAuth2 login (`/oauth2/authorization/github`) 4. **User** logs in with GitHub credentials 5. **GitHub** redirects back to Authorization Server with authorization code 6. **Authorization Server**: - - Exchanges code for GitHub access token - - Retrieves user info from GitHub API - - Creates an authenticated session using Spring Security's `OAuth2User` - - Generates its own JWT access token for the client + - Exchanges code for GitHub access token + - Retrieves user info from GitHub API + - Creates an authenticated session using Spring Security's `OAuth2User` + - Generates its own JWT access token for the client 7. **Client App** receives JWT token from Authorization Server 8. **Client App** validates JWT and grants access to resources -**Key Point**: The Authorization Server acts as an intermediary, translating GitHub authentication into JWT tokens that client applications can trust and validate. +**Key Point**: The Authorization Server acts as an intermediary, translating GitHub authentication into JWT tokens that +client applications can trust and validate. ### JWT Access Token Structure @@ -94,6 +99,7 @@ The Authorization Server issues JWT access tokens with the following claims: ``` **Claim Descriptions**: + - `sub` (Subject): Username from the external IdP (GitHub username) - `aud` (Audience): Client ID that the token was issued for (e.g., `swagger-ui`) - `nbf` (Not Before): Timestamp when the token becomes valid @@ -104,6 +110,7 @@ The Authorization Server issues JWT access tokens with the following claims: - `jti` (JWT ID): Unique identifier for this token The Resource Server validates these tokens by: + 1. Fetching public keys from the Authorization Server's JWKS endpoint (`/oauth2/jwks`) 2. Verifying the JWT signature using RSA-2048 3. Checking token expiration and audience claims @@ -112,27 +119,37 @@ The Resource Server validates these tokens by: ### Planned: Federated Identity (Future Enhancement) A **federated user** concept will allow unified identity across multiple external IdPs: +// Planned implementation FederatedUser -```java -// Planned implementation -FederatedUser { - id: "uuid-1234-5678" - username: "john.doe" - email: "john@example.com" - linkedAccounts: [ - { provider: "github", externalId: "github-123", linkedAt: "2024-01-15" }, - { provider: "google", externalId: "google-456", linkedAt: "2024-02-20" } +```json +{ + "id": "uuid-1234-5678", + "username": "john.doe", + "email": "john@example.com", + "linkedAccounts": [ + { + "provider": "github", + "externalId": "github-123", + "linkedAt": "2024-01-15" + }, + { + "provider": "google", + "externalId": "google-456", + "linkedAt": "2024-02-20" + } ] } ``` **This will require**: + - Custom `OAuth2UserService` implementation - Database entity for `FederatedUser` - Account linking logic - User profile management UI **Benefits**: + - Log in with different providers but maintain the same identity - Link multiple external accounts to one internal account - Switch between providers without losing access @@ -185,46 +202,46 @@ hello-spring-oauth2/ - **Purpose**: Central authentication & token issuer - **Technology**: Spring Authorization Server 1.3+ - **Features**: - - OAuth2 Authorization Code flow with PKCE - - OpenID Connect 1.0 (OIDC) - - OAuth2 Client (for GitHub IdP integration) - - JWT token generation with RSA keys - - Session management - - Health checks & actuator endpoints + - OAuth2 Authorization Code flow with PKCE + - OpenID Connect 1.0 (OIDC) + - OAuth2 Client (for GitHub IdP integration) + - JWT token generation with RSA keys + - Session management + - Health checks & actuator endpoints #### Resource Server (`hello-sample-app`) - **Purpose**: Example application with protected API - **Technology**: Spring Boot 3.5+ with Spring Security - **Features**: - - JWT validation from Authorization Server - - Swagger UI with OAuth2 integration - - Custom authority mapping from JWT claims - - CORS configuration - - Protected REST endpoints + - JWT validation from Authorization Server + - Swagger UI with OAuth2 integration + - Custom authority mapping from JWT claims + - CORS configuration + - Protected REST endpoints #### Nginx Proxy - **Purpose**: HTTPS termination & request routing - **Features**: - - SSL/TLS support - - Path-based routing: - - `/auth` → Authorization Server - - `/api` → Resource Server - - Header forwarding for proper OAuth2 redirects + - SSL/TLS support + - Path-based routing: + - `/auth` → Authorization Server + - `/api` → Resource Server + - Header forwarding for proper OAuth2 redirects ## Configuration ### GitHub IdP Setup 1. **Register OAuth App** in GitHub: - - Go to: Settings → Developer settings → OAuth Apps → New OAuth App - - **Application name**: `Hello Spring Auth` - - **Homepage URL**: `https://localhost/auth` - - **Authorization callback URL**: `https://localhost/auth/login/oauth2/code/github` + - Go to: Settings → Developer settings → OAuth Apps → New OAuth App + - **Application name**: `Hello Spring Auth` + - **Homepage URL**: `https://localhost/auth` + - **Authorization callback URL**: `https://localhost/auth/login/oauth2/code/github` 2. **Get Credentials**: - - Copy `Client ID` and `Client Secret` + - Copy `Client ID` and `Client Secret` 3. **Configure Environment**: @@ -380,23 +397,23 @@ curl -X POST https://localhost/auth/oauth2/token \ ### Authorization Server (Port 9000, Path /auth) -| Endpoint | Description | -|----------|-------------| -| `GET /auth/oauth2/authorize` | OAuth2 authorization endpoint | -| `POST /auth/oauth2/token` | Token endpoint | -| `GET /auth/oauth2/jwks` | JSON Web Key Set (public keys) | -| `GET /auth/.well-known/openid-configuration` | OIDC discovery | -| `GET /auth/userinfo` | OIDC user info endpoint | -| `GET /auth/oauth2/authorization/github` | Redirect to GitHub login | -| `GET /auth/login/oauth2/code/github` | GitHub callback URL | +| Endpoint | Description | +|----------------------------------------------|--------------------------------| +| `GET /auth/oauth2/authorize` | OAuth2 authorization endpoint | +| `POST /auth/oauth2/token` | Token endpoint | +| `GET /auth/oauth2/jwks` | JSON Web Key Set (public keys) | +| `GET /auth/.well-known/openid-configuration` | OIDC discovery | +| `GET /auth/userinfo` | OIDC user info endpoint | +| `GET /auth/oauth2/authorization/github` | Redirect to GitHub login | +| `GET /auth/login/oauth2/code/github` | GitHub callback URL | ### Resource Server (Port 8080, Path /api) -| Endpoint | Description | -|----------|-------------| -| `GET /api/swagger-ui` | Swagger UI with OAuth2 | -| `GET /api/v3/api-docs` | OpenAPI specification | -| `GET /api/samples` | Protected API endpoint (requires JWT) | +| Endpoint | Description | +|------------------------|---------------------------------------| +| `GET /api/swagger-ui` | Swagger UI with OAuth2 | +| `GET /api/v3/api-docs` | OpenAPI specification | +| `GET /api/samples` | Protected API endpoint (requires JWT) | ## Architecture Decisions @@ -430,6 +447,7 @@ curl -X POST https://localhost/auth/oauth2/token \ 5. **Future-Proof**: Easy to add new authentication methods **Implementation requires**: + - Custom `OAuth2UserService` for user mapping - `FederatedUser` entity and repository - Account linking logic @@ -463,7 +481,7 @@ spring: client: registration: github: - # ... existing GitHub config + # ... existing GitHub config google: provider: google client-id: ${GOOGLE_CLIENT_ID} @@ -477,21 +495,24 @@ spring: 3. **Add environment variables** to `.env` 4. **Update entry point** in `SecurityConfig.java` (optional - to offer IdP selection) -**Current Limitation**: Without federated identity implementation, each provider creates a separate user session. Users logging in via GitHub and Google would be treated as different users, even with the same email. +**Current Limitation**: Without federated identity implementation, each provider creates a separate user session. Users +logging in via GitHub and Google would be treated as different users, even with the same email. ### Implementing Federated Identity (Roadmap) To properly link multiple IdPs to the same user account, you need to implement: **1. Create `FederatedUser` entity**: + ```java + @Entity public class FederatedUser { @Id private UUID id; private String email; private String username; - + @OneToMany(mappedBy = "user") private Set linkedAccounts; } @@ -503,65 +524,69 @@ public class LinkedAccount { private String provider; // "github", "google" private String externalId; private Instant linkedAt; - + @ManyToOne private FederatedUser user; } ``` **2. Implement custom `OAuth2UserService`**: + ```java + @Service public class FederatedUserService implements OAuth2UserService { - + @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) { // Delegate to default implementation - OAuth2UserService delegate = - new DefaultOAuth2UserService(); + OAuth2UserService delegate = + new DefaultOAuth2UserService(); OAuth2User oauth2User = delegate.loadUser(userRequest); - + // Extract provider and user info String provider = userRequest.getClientRegistration().getRegistrationId(); String email = oauth2User.getAttribute("email"); String externalId = oauth2User.getName(); - + // Find or create federated user FederatedUser user = findOrCreateFederatedUser(email, provider, externalId); - + // Return custom user with federated identity return new FederatedOAuth2User(user, oauth2User); } - + private FederatedUser findOrCreateFederatedUser(String email, String provider, String externalId) { // Find existing user by email or linked account FederatedUser user = userRepository.findByEmail(email) - .orElseGet(() -> createNewUser(email)); - + .orElseGet(() -> createNewUser(email)); + // Link account if not already linked if (!user.hasLinkedAccount(provider, externalId)) { user.linkAccount(provider, externalId); userRepository.save(user); } - + return user; } } ``` **3. Register custom service in `SecurityConfig`**: + ```java + @Bean @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { // ...existing config... - + http.oauth2Login(oauth2 -> oauth2 - .userInfoEndpoint(userInfo -> userInfo - .userService(federatedUserService) - ) + .userInfoEndpoint(userInfo -> userInfo + .userService(federatedUserService) + ) ); - + return http.build(); } ```