diff --git a/chutney/packaging/local-dev/src/main/resources/application-sso-auth.yml b/chutney/packaging/local-dev/src/main/resources/application-sso-auth.yml index 817ac387c..4ace89b76 100644 --- a/chutney/packaging/local-dev/src/main/resources/application-sso-auth.yml +++ b/chutney/packaging/local-dev/src/main/resources/application-sso-auth.yml @@ -8,38 +8,37 @@ spring: registration: my-provider: provider: my-provider - client-id: my-client - client-secret: my-client-secret + client-id: "${auth.sso.clientId}" + client-secret: "${auth.sso.clientSecret}" authorization-grant-type: authorization_code redirect-uri: "https://${server.http.interface}:${server.port}/login/oauth2/code/{registrationId}" scope: openid, profile, email client-name: My Provider provider: my-provider: - issuer-uri: http://localhost:3000 - authorization-uri: http://localhost:3000/auth - token-uri: http://localhost:3000/token - user-info-uri: http://localhost:3000/me + authorization-uri: ${auth.sso.issuer}/auth + token-uri: ${auth.sso.issuer}/token + user-info-uri: ${auth.sso.issuer}/me user-name-attribute: sub - jwk-set-uri: http://localhost:3000/jwks + jwk-set-uri: ${auth.sso.issuer}/jwks resourceserver: opaque-token: - introspection-uri: http://localhost:3000/token/introspection - client-id: 'my-client' - client-secret: 'my-client-secret' + introspection-uri: "${auth.sso.issuer}/token/introspection" + client-id: "${auth.sso.clientId}" + client-secret: "${auth.sso.clientSecret}" authorizationserver: - issuer: http://localhost:3000 + issuer: "${auth.sso.issuer}" endpoint: oidc: - user-info-uri: http://localhost:3000/userinfo + user-info-uri: "${auth.sso.issuer}/userinfo" auth: sso: - issuer: 'http://localhost:3000' + issuer: "http://localhost:3000" clientId: 'my-client' clientSecret: 'my-client-secret' responseType: 'code' scope: 'openid profile email' - redirectBaseUrl: 'https://localhost:4200' - ssoProviderName: 'SSO OIDC' + redirectBaseUrl: "https://localhost:4200" + ssoProviderName: 'SSO OpenID Connect' oidc: true diff --git a/chutney/packaging/local-dev/src/test/resources/sso/.env b/chutney/packaging/local-dev/src/test/resources/sso/.env new file mode 100644 index 000000000..3e554e32d --- /dev/null +++ b/chutney/packaging/local-dev/src/test/resources/sso/.env @@ -0,0 +1,6 @@ +CLIENT_ID=my-client +CLIENT_SECRET=my-client-secret +REDIRECT_URI='https://localhost:4200' +TOKEN_FORMAT='opaque' +PORT=3000 +GRANT_TYPE=authorization_code diff --git a/chutney/server/src/test/resources/sso/README.md b/chutney/packaging/local-dev/src/test/resources/sso/README.md similarity index 100% rename from chutney/server/src/test/resources/sso/README.md rename to chutney/packaging/local-dev/src/test/resources/sso/README.md diff --git a/chutney/server/src/test/resources/sso/package.json b/chutney/packaging/local-dev/src/test/resources/sso/package.json similarity index 92% rename from chutney/server/src/test/resources/sso/package.json rename to chutney/packaging/local-dev/src/test/resources/sso/package.json index f4d487967..b3883b7a9 100644 --- a/chutney/server/src/test/resources/sso/package.json +++ b/chutney/packaging/local-dev/src/test/resources/sso/package.json @@ -10,6 +10,7 @@ "author": "", "description": "", "dependencies": { + "dotenv": "^16.4.5", "express": "^4.21.0", "oidc-provider": "^8.5.1" } diff --git a/chutney/server/src/test/resources/sso/sso-oidc.mjs b/chutney/packaging/local-dev/src/test/resources/sso/sso-oidc.mjs similarity index 54% rename from chutney/server/src/test/resources/sso/sso-oidc.mjs rename to chutney/packaging/local-dev/src/test/resources/sso/sso-oidc.mjs index 5ca5aaed6..0cbcefd39 100644 --- a/chutney/server/src/test/resources/sso/sso-oidc.mjs +++ b/chutney/packaging/local-dev/src/test/resources/sso/sso-oidc.mjs @@ -7,19 +7,22 @@ import express from 'express'; import { Provider } from 'oidc-provider'; +import * as dotenv from 'dotenv'; + +dotenv.config() const oidc = new Provider('http://localhost:3000', { clients: [{ - client_id: 'my-client', - client_secret: 'my-client-secret', - grant_types: ['authorization_code'], - redirect_uris: ['https://localhost:4200/'], - post_logout_redirect_uris: ['https://localhost:4200/'], + client_id: process.env.CLIENT_ID, + client_secret: process.env.CLIENT_SECRET, + grant_types: [process.env.GRANT_TYPE], + redirect_uris: [process.env.REDIRECT_URI], + post_logout_redirect_uris: [process.env.REDIRECT_URI], }], formats: { - AccessToken: 'opaque', - RefreshToken: 'opaque', - IdToken: 'opaque' + AccessToken: process.env.TOKEN_FORMAT, + RefreshToken: process.env.TOKEN_FORMAT, + IdToken: process.env.TOKEN_FORMAT }, features: { introspection: { @@ -29,7 +32,7 @@ const oidc = new Provider('http://localhost:3000', { userinfo: { enabled: true }, }, clientBasedCORS(ctx, origin, client) { - const allowedOrigins = ['https://localhost:4200']; + const allowedOrigins = [process.env.REDIRECT_URI]; return allowedOrigins.includes(origin); }, async findAccount(ctx, id) { @@ -42,6 +45,7 @@ const oidc = new Provider('http://localhost:3000', { const app = express(); app.use(oidc.callback()); -app.listen(3000, () => { - console.log('OIDC provider listening on port 3000'); +const port = parseInt(process.env.PORT, 10) +app.listen(port, () => { + console.log(`OIDC provider listening on port ${port}`); }); diff --git a/chutney/server/src/main/java/com/chutneytesting/security/AbstractChutneyWebSecurityConfig.java b/chutney/server/src/main/java/com/chutneytesting/security/AbstractChutneyWebSecurityConfig.java deleted file mode 100644 index 03017cdbc..000000000 --- a/chutney/server/src/main/java/com/chutneytesting/security/AbstractChutneyWebSecurityConfig.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2017-2024 Enedis - * - * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.chutneytesting.security; - -import com.chutneytesting.admin.api.InfoController; -import com.chutneytesting.security.api.SsoOpenIdConnectController; -import com.chutneytesting.security.api.UserController; -import com.chutneytesting.security.api.UserDto; -import com.chutneytesting.security.domain.SsoOpenIdConnectConfigService; -import com.chutneytesting.security.infra.handlers.Http401FailureHandler; -import com.chutneytesting.security.infra.handlers.HttpEmptyLogoutSuccessHandler; -import com.chutneytesting.security.infra.handlers.HttpLoginSuccessHandler; -import com.chutneytesting.security.infra.sso.SsoOpenIdConnectConfigProperties; -import com.chutneytesting.server.core.domain.security.Authorization; -import com.chutneytesting.server.core.domain.security.User; -import java.util.ArrayList; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.http.HttpStatus; -import org.springframework.lang.Nullable; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.annotation.web.configurers.ChannelSecurityConfigurer; -import org.springframework.security.web.authentication.HttpStatusEntryPoint; -import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; - -public abstract class AbstractChutneyWebSecurityConfig { - - protected static final String LOGIN_URL = UserController.BASE_URL + "/login"; - protected static final String LOGOUT_URL = UserController.BASE_URL + "/logout"; - protected static final String API_BASE_URL_PATTERN = "/api/**"; - - @Bean - public SsoOpenIdConnectConfigService ssoOpenIdConnectConfigService(@Nullable SsoOpenIdConnectConfigProperties ssoOpenIdConnectConfigProperties) { - return new SsoOpenIdConnectConfigService(ssoOpenIdConnectConfigProperties); - } - - @Value("${management.endpoints.web.base-path:/actuator}") - protected String actuatorBaseUrl; - - @Value("${server.ssl.enabled:true}") - private Boolean sslEnabled; - - protected HttpSecurity configureHttp(final HttpSecurity http) throws Exception { - configureBaseHttpSecurity(http); - UserDto anonymous = anonymous(); - http - .anonymous(anonymousConfigurer -> anonymousConfigurer - .principal(anonymous) - .authorities(new ArrayList<>(anonymous.getAuthorities()))) - .authorizeHttpRequests(httpRequest -> { - HandlerMappingIntrospector introspector = new HandlerMappingIntrospector(); - httpRequest - .requestMatchers(new MvcRequestMatcher(introspector, LOGIN_URL)).permitAll() - .requestMatchers(new MvcRequestMatcher(introspector, LOGOUT_URL)).permitAll() - .requestMatchers(new MvcRequestMatcher(introspector, InfoController.BASE_URL + "/**")).permitAll() - .requestMatchers(new MvcRequestMatcher(introspector, SsoOpenIdConnectController.BASE_URL + "/**")).permitAll() - .requestMatchers(new MvcRequestMatcher(introspector, API_BASE_URL_PATTERN)).authenticated() - .requestMatchers(new MvcRequestMatcher(introspector, actuatorBaseUrl + "/**")).hasAuthority(Authorization.ADMIN_ACCESS.name()) - .anyRequest().permitAll(); - }) - .httpBasic(Customizer.withDefaults()); - return http; - } - - protected void configureBaseHttpSecurity(final HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) - .exceptionHandling(httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) - .requiresChannel(this.requireChannel(sslEnabled)) - .formLogin(httpSecurityFormLoginConfigurer -> httpSecurityFormLoginConfigurer - .loginProcessingUrl(LOGIN_URL) - .successHandler(new HttpLoginSuccessHandler()) - .failureHandler(new Http401FailureHandler())) - .logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer - .logoutUrl(LOGOUT_URL) - .logoutSuccessHandler(new HttpEmptyLogoutSuccessHandler())); - } - - protected UserDto anonymous() { - UserDto anonymous = new UserDto(); - anonymous.setId(User.ANONYMOUS.id); - anonymous.setName(User.ANONYMOUS.id); - anonymous.grantAuthority("ANONYMOUS"); - return anonymous; - } - - private Customizer.ChannelRequestMatcherRegistry> requireChannel(Boolean sslEnabled) { - if (sslEnabled) { - return channelRequestMatcherRegistry -> channelRequestMatcherRegistry.anyRequest().requiresSecure(); - } else { - return channelRequestMatcherRegistry -> channelRequestMatcherRegistry.anyRequest().requiresInsecure(); - } - } -} diff --git a/chutney/server/src/main/java/com/chutneytesting/security/ChutneyWebSecurityConfig.java b/chutney/server/src/main/java/com/chutneytesting/security/ChutneyWebSecurityConfig.java index 89e50c76b..fe601e6e6 100644 --- a/chutney/server/src/main/java/com/chutneytesting/security/ChutneyWebSecurityConfig.java +++ b/chutney/server/src/main/java/com/chutneytesting/security/ChutneyWebSecurityConfig.java @@ -7,23 +7,64 @@ package com.chutneytesting.security; +import com.chutneytesting.admin.api.InfoController; +import com.chutneytesting.security.api.SsoOpenIdConnectController; +import com.chutneytesting.security.api.UserController; +import com.chutneytesting.security.api.UserDto; import com.chutneytesting.security.domain.AuthenticationService; import com.chutneytesting.security.domain.Authorizations; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import com.chutneytesting.security.infra.handlers.Http401FailureHandler; +import com.chutneytesting.security.infra.handlers.HttpEmptyLogoutSuccessHandler; +import com.chutneytesting.security.infra.handlers.HttpLoginSuccessHandler; +import com.chutneytesting.security.infra.sso.OAuth2SsoUserService; +import com.chutneytesting.security.infra.sso.OAuth2TokenAuthenticationFilter; +import com.chutneytesting.security.infra.sso.OAuth2TokenAuthenticationProvider; +import com.chutneytesting.security.infra.sso.SsoOpenIdConnectConfigProperties; +import com.chutneytesting.server.core.domain.security.Authorization; +import com.chutneytesting.server.core.domain.security.User; +import java.util.ArrayList; +import java.util.Collections; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.security.oauth2.server.servlet.OAuth2AuthorizationServerProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.ChannelSecurityConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; @Configuration @EnableWebSecurity @EnableMethodSecurity -@Profile("!sso-auth") -public class ChutneyWebSecurityConfig extends AbstractChutneyWebSecurityConfig { +@EnableConfigurationProperties({OAuth2AuthorizationServerProperties.class, SsoOpenIdConnectConfigProperties.class}) +public class ChutneyWebSecurityConfig { + + protected static final String LOGIN_URL = UserController.BASE_URL + "/login"; + protected static final String LOGOUT_URL = UserController.BASE_URL + "/logout"; + protected static final String API_BASE_URL_PATTERN = "/api/**"; + + @Value("${management.endpoints.web.base-path:/actuator}") + protected String actuatorBaseUrl; + + @Value("${server.ssl.enabled:true}") + private Boolean sslEnabled; @Bean public AuthenticationService authenticationService(Authorizations authorizations) { @@ -31,9 +72,68 @@ public AuthenticationService authenticationService(Authorizations authorizations } @Bean - @Order() - @ConditionalOnMissingBean(value = SecurityFilterChain.class) - public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { - return configureHttp(http).build(); + public SecurityFilterChain securityFilterChain(final HttpSecurity http, AuthenticationService authenticationService, @Nullable ClientRegistrationRepository clientRegistrationRepository) throws Exception { + configureSso(http, authenticationService, clientRegistrationRepository); + configureBaseHttpSecurity(http); + UserDto anonymous = anonymous(); + http.anonymous(anonymousConfigurer -> anonymousConfigurer + .principal(anonymous) + .authorities(new ArrayList<>(anonymous.getAuthorities()))) + .authorizeHttpRequests(httpRequest -> { + HandlerMappingIntrospector introspector = new HandlerMappingIntrospector(); + httpRequest + .requestMatchers(new MvcRequestMatcher(introspector, LOGIN_URL)).permitAll() + .requestMatchers(new MvcRequestMatcher(introspector, LOGOUT_URL)).permitAll() + .requestMatchers(new MvcRequestMatcher(introspector, InfoController.BASE_URL + "/**")).permitAll() + .requestMatchers(new MvcRequestMatcher(introspector, SsoOpenIdConnectController.BASE_URL + "/**")).permitAll() + .requestMatchers(new MvcRequestMatcher(introspector, API_BASE_URL_PATTERN)).authenticated() + .requestMatchers(new MvcRequestMatcher(introspector, actuatorBaseUrl + "/**")).hasAuthority(Authorization.ADMIN_ACCESS.name()) + .anyRequest().permitAll(); + }) + .httpBasic(Customizer.withDefaults()); + return http.build(); + } + + protected void configureBaseHttpSecurity(final HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .exceptionHandling(httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) + .requiresChannel(this.requireChannel(sslEnabled)) + .formLogin(httpSecurityFormLoginConfigurer -> httpSecurityFormLoginConfigurer + .loginProcessingUrl(LOGIN_URL) + .successHandler(new HttpLoginSuccessHandler()) + .failureHandler(new Http401FailureHandler())) + .logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer + .logoutUrl(LOGOUT_URL) + .logoutSuccessHandler(new HttpEmptyLogoutSuccessHandler())); + } + + protected UserDto anonymous() { + UserDto anonymous = new UserDto(); + anonymous.setId(User.ANONYMOUS.id); + anonymous.setName(User.ANONYMOUS.id); + anonymous.grantAuthority("ANONYMOUS"); + return anonymous; + } + + private Customizer.ChannelRequestMatcherRegistry> requireChannel(Boolean sslEnabled) { + if (sslEnabled) { + return channelRequestMatcherRegistry -> channelRequestMatcherRegistry.anyRequest().requiresSecure(); + } else { + return channelRequestMatcherRegistry -> channelRequestMatcherRegistry.anyRequest().requiresInsecure(); + } + } + + private void configureSso(final HttpSecurity http, AuthenticationService authenticationService, ClientRegistrationRepository clientRegistrationRepository) throws Exception { + if (clientRegistrationRepository != null) { + OAuth2UserService oAuth2UserService = new OAuth2SsoUserService(authenticationService); + OAuth2TokenAuthenticationProvider oAuth2TokenAuthenticationProvider = new OAuth2TokenAuthenticationProvider(oAuth2UserService, clientRegistrationRepository.findByRegistrationId("my-provider")); + AuthenticationManager authenticationManager = new ProviderManager(Collections.singletonList(oAuth2TokenAuthenticationProvider)); + OAuth2TokenAuthenticationFilter tokenFilter = new OAuth2TokenAuthenticationFilter(authenticationManager); + http + .authenticationProvider(oAuth2TokenAuthenticationProvider) + .addFilterBefore(tokenFilter, BasicAuthenticationFilter.class) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)); + } } } diff --git a/chutney/server/src/main/java/com/chutneytesting/security/api/SsoOpenIdConnectController.java b/chutney/server/src/main/java/com/chutneytesting/security/api/SsoOpenIdConnectController.java index 966b4634f..0e5a6ce4f 100644 --- a/chutney/server/src/main/java/com/chutneytesting/security/api/SsoOpenIdConnectController.java +++ b/chutney/server/src/main/java/com/chutneytesting/security/api/SsoOpenIdConnectController.java @@ -8,8 +8,11 @@ package com.chutneytesting.security.api; import static com.chutneytesting.security.api.SsoOpenIdConnectMapper.toDto; +import static java.util.Optional.ofNullable; -import com.chutneytesting.security.domain.SsoOpenIdConnectConfigService; +import com.chutneytesting.security.infra.sso.SsoOpenIdConnectConfigProperties; +import java.util.NoSuchElementException; +import org.springframework.context.annotation.Profile; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; @@ -19,21 +22,20 @@ @RestController @RequestMapping(SsoOpenIdConnectController.BASE_URL) @CrossOrigin(origins = "*") +@Profile("sso-auth") public class SsoOpenIdConnectController { public static final String BASE_URL = "/api/v1/sso"; - private final SsoOpenIdConnectConfigService ssoOpenIdConnectConfigService; + private final SsoOpenIdConnectConfigProperties ssoOpenIdConnectConfigProperties; - SsoOpenIdConnectController(SsoOpenIdConnectConfigService ssoOpenIdConnectConfigService) { - this.ssoOpenIdConnectConfigService = ssoOpenIdConnectConfigService; + SsoOpenIdConnectController(SsoOpenIdConnectConfigProperties ssoOpenIdConnectConfigProperties) { + this.ssoOpenIdConnectConfigProperties = ssoOpenIdConnectConfigProperties; } @GetMapping(path = "/config", produces = MediaType.APPLICATION_JSON_VALUE) public SsoOpenIdConnectConfigDto getSsoOpenIdConnectConfig() { - if (ssoOpenIdConnectConfigService == null) { - return null; - } - return toDto(ssoOpenIdConnectConfigService.getSsoOpenIdConnectConfig()); + return ofNullable(toDto(ssoOpenIdConnectConfigProperties)) + .orElseThrow(NoSuchElementException::new); } } diff --git a/chutney/server/src/main/java/com/chutneytesting/security/api/SsoOpenIdConnectMapper.java b/chutney/server/src/main/java/com/chutneytesting/security/api/SsoOpenIdConnectMapper.java index c47e32110..59ef2d836 100644 --- a/chutney/server/src/main/java/com/chutneytesting/security/api/SsoOpenIdConnectMapper.java +++ b/chutney/server/src/main/java/com/chutneytesting/security/api/SsoOpenIdConnectMapper.java @@ -7,8 +7,10 @@ package com.chutneytesting.security.api; +import com.chutneytesting.security.infra.sso.SsoOpenIdConnectConfigProperties; + public class SsoOpenIdConnectMapper { - public static SsoOpenIdConnectConfigDto toDto(com.chutneytesting.security.domain.SsoOpenIdConnectConfig ssoOpenIdConnectConfig) { + public static SsoOpenIdConnectConfigDto toDto(SsoOpenIdConnectConfigProperties ssoOpenIdConnectConfig) { if (ssoOpenIdConnectConfig == null) { return null; } diff --git a/chutney/server/src/main/java/com/chutneytesting/security/domain/SsoOpenIdConnectConfig.java b/chutney/server/src/main/java/com/chutneytesting/security/domain/SsoOpenIdConnectConfig.java deleted file mode 100644 index 643e30eaf..000000000 --- a/chutney/server/src/main/java/com/chutneytesting/security/domain/SsoOpenIdConnectConfig.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2017-2024 Enedis - * - * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.chutneytesting.security.domain; - -public class SsoOpenIdConnectConfig { - public final String issuer; - public final String clientId; - public final String clientSecret; - public final String responseType; - public final String scope; - public final String redirectBaseUrl; - public final String ssoProviderName; - public final Boolean oidc; - - public SsoOpenIdConnectConfig(String issuer, String clientId, String clientSecret, String responseType, String scope, String redirectBaseUrl, String ssoProviderName, Boolean oidc) { - this.issuer = issuer; - this.clientId = clientId; - this.clientSecret = clientSecret; - this.responseType = responseType; - this.scope = scope; - this.redirectBaseUrl = redirectBaseUrl; - this.ssoProviderName = ssoProviderName; - this.oidc = oidc; - } -} diff --git a/chutney/server/src/main/java/com/chutneytesting/security/domain/SsoOpenIdConnectConfigService.java b/chutney/server/src/main/java/com/chutneytesting/security/domain/SsoOpenIdConnectConfigService.java deleted file mode 100644 index 04985c2fc..000000000 --- a/chutney/server/src/main/java/com/chutneytesting/security/domain/SsoOpenIdConnectConfigService.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2017-2024 Enedis - * - * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.chutneytesting.security.domain; - -import static com.chutneytesting.security.domain.SsoOpenIdConnectMapper.toDomain; - -import com.chutneytesting.security.infra.sso.SsoOpenIdConnectConfigProperties; - -public class SsoOpenIdConnectConfigService { - - private final SsoOpenIdConnectConfigProperties ssoOpenIdConnectConfigProperties; - - public SsoOpenIdConnectConfigService(SsoOpenIdConnectConfigProperties ssoOpenIdConnectConfigProperties) { - this.ssoOpenIdConnectConfigProperties = ssoOpenIdConnectConfigProperties; - } - - public SsoOpenIdConnectConfig getSsoOpenIdConnectConfig() { - return toDomain(ssoOpenIdConnectConfigProperties); - } -} diff --git a/chutney/server/src/main/java/com/chutneytesting/security/domain/SsoOpenIdConnectMapper.java b/chutney/server/src/main/java/com/chutneytesting/security/domain/SsoOpenIdConnectMapper.java deleted file mode 100644 index 12317290a..000000000 --- a/chutney/server/src/main/java/com/chutneytesting/security/domain/SsoOpenIdConnectMapper.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2017-2024 Enedis - * - * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.chutneytesting.security.domain; - -import com.chutneytesting.security.infra.sso.SsoOpenIdConnectConfigProperties; - -public class SsoOpenIdConnectMapper { - public static SsoOpenIdConnectConfig toDomain(SsoOpenIdConnectConfigProperties ssoOpenIdConnectConfigProperties) { - if (ssoOpenIdConnectConfigProperties == null) { - return null; - } - return new SsoOpenIdConnectConfig( - ssoOpenIdConnectConfigProperties.issuer, - ssoOpenIdConnectConfigProperties.clientId, - ssoOpenIdConnectConfigProperties.clientSecret, - ssoOpenIdConnectConfigProperties.responseType, - ssoOpenIdConnectConfigProperties.scope, - ssoOpenIdConnectConfigProperties.redirectBaseUrl, - ssoOpenIdConnectConfigProperties.ssoProviderName, - ssoOpenIdConnectConfigProperties.oidc - ); - } -} diff --git a/chutney/server/src/main/java/com/chutneytesting/security/infra/sso/OAuth2SsoSecurityConfiguration.java b/chutney/server/src/main/java/com/chutneytesting/security/infra/sso/OAuth2SsoSecurityConfiguration.java deleted file mode 100644 index a90729bf7..000000000 --- a/chutney/server/src/main/java/com/chutneytesting/security/infra/sso/OAuth2SsoSecurityConfiguration.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2017-2024 Enedis - * - * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.chutneytesting.security.infra.sso; - - -import com.chutneytesting.security.AbstractChutneyWebSecurityConfig; -import com.chutneytesting.security.domain.AuthenticationService; -import com.chutneytesting.security.domain.Authorizations; -import java.util.Collections; -import org.springframework.boot.autoconfigure.security.oauth2.server.servlet.OAuth2AuthorizationServerProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.ProviderManager; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; - -@Configuration -@Profile("sso-auth") -@EnableWebSecurity -@EnableMethodSecurity -@EnableConfigurationProperties({OAuth2AuthorizationServerProperties.class, SsoOpenIdConnectConfigProperties.class}) -public class OAuth2SsoSecurityConfiguration extends AbstractChutneyWebSecurityConfig { - - @Bean - public AuthenticationService authenticationService(Authorizations authorizations) { - return new AuthenticationService(authorizations); - } - - @Bean - public OAuth2UserService customOAuth2UserService(AuthenticationService authenticationService) { - return new OAuth2SsoUserService(authenticationService); - } - - @Bean - public OAuth2TokenAuthenticationProvider tokenAuthenticationProvider(AuthenticationService authenticationService, ClientRegistrationRepository clientRegistrationRepository) { - return new OAuth2TokenAuthenticationProvider(customOAuth2UserService(authenticationService), clientRegistrationRepository.findByRegistrationId("my-provider")); - } - - @Bean - public AuthenticationManager authenticationManager(OAuth2TokenAuthenticationProvider OAuth2TokenAuthenticationProvider) { - return new ProviderManager(Collections.singletonList(OAuth2TokenAuthenticationProvider)); - } - - @Bean - public SecurityFilterChain securityFilterChainOAuth2Sso(final HttpSecurity http, OAuth2TokenAuthenticationProvider OAuth2TokenAuthenticationProvider, AuthenticationManager authenticationManager) throws Exception { - OAuth2TokenAuthenticationFilter tokenFilter = new OAuth2TokenAuthenticationFilter(authenticationManager); - http.authenticationProvider(OAuth2TokenAuthenticationProvider) - .addFilterBefore(tokenFilter, BasicAuthenticationFilter.class) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)); - configureHttp(http); - return http.build(); - } -} diff --git a/chutney/ui/src/app/app.module.ts b/chutney/ui/src/app/app.module.ts index 25d969b5a..82e1c969a 100644 --- a/chutney/ui/src/app/app.module.ts +++ b/chutney/ui/src/app/app.module.ts @@ -28,7 +28,6 @@ import { ThemeService } from '@core/theme/theme.service'; import { DefaultMissingTranslationHandler, HttpLoaderFactory } from '@core/initializer/app.translate.factory'; import { themeInitializer } from '@core/initializer/theme.initializer'; import { OAuthModule } from 'angular-oauth2-oidc'; -import { ssoInitializer } from "@core/initializer/sso.initializer"; import { SsoService } from "@core/services/sso.service"; @NgModule({ @@ -71,17 +70,15 @@ import { SsoService } from "@core/services/sso.service"; useFactory: themeInitializer, deps: [ThemeService], multi: true - }, - { - provide: APP_INITIALIZER, - useFactory: ssoInitializer, - deps: [SsoService], - multi: true } ], bootstrap: [AppComponent] }) -export class ChutneyAppModule {} +export class ChutneyAppModule { + constructor(private ssoOpenIdConnectService: SsoService) { + this.ssoOpenIdConnectService.fetchSsoConfig() + } +} diff --git a/chutney/ui/src/app/core/guards/auth.guard.ts b/chutney/ui/src/app/core/guards/auth.guard.ts index a4bc17b18..1d5d1d736 100644 --- a/chutney/ui/src/app/core/guards/auth.guard.ts +++ b/chutney/ui/src/app/core/guards/auth.guard.ts @@ -29,7 +29,7 @@ export const authGuard: CanActivateFn = async (route: ActivatedRouteSnapshot, st 'Authorization': 'Bearer ' + loginService.oauth2Token })); } else { - loginService.initLogin(requestURL); + await loginService.initLogin(requestURL); return false; } } diff --git a/chutney/ui/src/app/core/initializer/sso.initializer.ts b/chutney/ui/src/app/core/initializer/sso.initializer.ts deleted file mode 100644 index 68bc1ee71..000000000 --- a/chutney/ui/src/app/core/initializer/sso.initializer.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2017-2024 Enedis - * - * SPDX-License-Identifier: Apache-2.0 - * - */ - -import { SsoService } from "@core/services/sso.service"; - -export function ssoInitializer(ssoOpenIdConnectService: SsoService): () => void { - return () => ssoOpenIdConnectService.fetchSsoConfig() -} diff --git a/chutney/ui/src/app/core/services/login.service.ts b/chutney/ui/src/app/core/services/login.service.ts index 2b20c820e..bc0197576 100644 --- a/chutney/ui/src/app/core/services/login.service.ts +++ b/chutney/ui/src/app/core/services/login.service.ts @@ -32,10 +32,10 @@ export class LoginService { private ssoService: SsoService ) { } - initLogin(url?: string, headers: HttpHeaders | { + async initLogin(url?: string, headers: HttpHeaders | { [header: string]: string | string[]; } = {}) { - this.initLoginObservable(url, headers).subscribe() + await firstValueFrom(this.initLoginObservable(url, headers)) } initLoginObservable(url?: string, headers?: HttpHeaders | { diff --git a/chutney/ui/src/app/core/services/sso.service.ts b/chutney/ui/src/app/core/services/sso.service.ts index 33fbc5aad..81c940137 100644 --- a/chutney/ui/src/app/core/services/sso.service.ts +++ b/chutney/ui/src/app/core/services/sso.service.ts @@ -6,9 +6,9 @@ */ import { HttpClient } from '@angular/common/http'; -import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; +import { OAuthService } from 'angular-oauth2-oidc'; import { environment } from '@env/environment'; -import {Observable, map, tap} from 'rxjs'; +import { map, tap } from 'rxjs'; import { Injectable } from '@angular/core'; interface SsoAuthConfig { @@ -48,9 +48,13 @@ export class SsoService { oidc: ssoConfig.oidc } }), - tap(ssoConfig => { - this.oauthService.configure(ssoConfig) - this.oauthService.loadDiscoveryDocumentAndTryLogin(); + tap(async ssoConfig => { + try { + this.oauthService.configure(ssoConfig) + await this.oauthService.loadDiscoveryDocumentAndTryLogin(); + } catch (e) { + console.error("SSO provider not available") + } }) ).subscribe() } diff --git a/chutney/ui/src/app/shared/error-interceptor.service.ts b/chutney/ui/src/app/shared/error-interceptor.service.ts index f096830bd..1479ba985 100644 --- a/chutney/ui/src/app/shared/error-interceptor.service.ts +++ b/chutney/ui/src/app/shared/error-interceptor.service.ts @@ -9,8 +9,8 @@ import { Injectable } from '@angular/core'; import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { EMPTY, Observable, throwError } from 'rxjs'; -import { catchError } from 'rxjs/operators'; +import { EMPTY, from, Observable, throwError } from 'rxjs'; +import { catchError, switchMap } from 'rxjs/operators'; import { LoginService } from '@core/services'; import { AlertService } from '@shared'; @@ -45,11 +45,13 @@ export class ErrorInterceptor implements HttpInterceptor { if (this.loginService.isAuthenticated()) { this.loginService.logout(); this.alertService.error(this.sessionExpiredMessage, { timeOut: 0, extendedTimeOut: 0, closeButton: true }); + return EMPTY } else { const requestURL = this.router.url !== undefined ? this.router.url : ''; - this.loginService.initLogin(requestURL); + return from(this.loginService.initLogin(requestURL)).pipe( + switchMap(() => EMPTY) + ); } - return EMPTY; } } return throwError(err);