Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui, server): Generic SSO OAuth2 OpenID Connect #188

Closed
wants to merge 26 commits into from
Closed
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fa39410
[WIP] feat(ui, server): Generic SSO OAuth2 OpenID Connect
DelaunayAlex Oct 1, 2024
6988c7c
[WIP] feat(server, ui): Conf SSO OAuth2
DelaunayAlex Oct 3, 2024
8c5d14f
feat(server, ui): SSO OAuth2 with mock oidc-provider, authenticate SS…
DelaunayAlex Oct 10, 2024
0e8ac00
feat(server, ui): SSO OAuth2 with mock oidc-provider, authenticate SS…
DelaunayAlex Oct 10, 2024
b492609
feat(ui, server): Doc local oidc provider
DelaunayAlex Oct 14, 2024
9ffca82
feat(server, ui): Fix PR
DelaunayAlex Oct 20, 2024
e9f42b4
feat(server, ui): Add licence
DelaunayAlex Oct 22, 2024
1fa05b0
feat(ui): Use app initializer
DelaunayAlex Oct 22, 2024
0853d58
feat(server, ui): Fix PR
DelaunayAlex Oct 29, 2024
583c67c
feat(server): Add proxy
DelaunayAlex Oct 30, 2024
fb4efde
feat(ui): Update headers OAuth2
DelaunayAlex Nov 4, 2024
d8b7122
feat(ui): Update headers OAuth2
DelaunayAlex Nov 4, 2024
0e7caa2
feat(ui): SSO : Replace idToken with accessToken
DelaunayAlex Nov 6, 2024
975bf19
feat(ui, server): Ui parameter via server, add logo sso
DelaunayAlex Nov 6, 2024
d4029d4
[WIP] feat(ui, server): add param id_token_hint
DelaunayAlex Nov 6, 2024
562b35b
[WIP] feat(ui, server): add param id_token_hint
DelaunayAlex Nov 6, 2024
5b6fcc3
feat(ui): fix logout sso
DelaunayAlex Nov 7, 2024
4645511
feat(ui): SSO : Fix redirect uri
DelaunayAlex Nov 12, 2024
8fb7b70
feat(ui): fix retry sso
DelaunayAlex Nov 12, 2024
c7d26f2
feat(ui): fix test
DelaunayAlex Nov 13, 2024
56ccf36
feat(ui): fix PR
DelaunayAlex Nov 14, 2024
33c2a05
feat(ui): fix PR
DelaunayAlex Nov 19, 2024
95e6a5d
chore(): Allow to test sso with a local-dev server running on 8443
boddissattva Nov 20, 2024
8f51a23
chore(): Clean and fix flaky test
boddissattva Nov 20, 2024
36ddfb2
chore(): Fix flaky test
boddissattva Nov 20, 2024
5a38e60
feat(ui, server): Alert message when sso auth fails, fix PR
DelaunayAlex Nov 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(server, ui): Fix PR
DelaunayAlex committed Nov 19, 2024
commit 9ffca82c3cb8978428ee6edd050b9423badccc14
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ spring:
client-id: my-client
client-secret: my-client-secret
authorization-grant-type: authorization_code
redirect-uri: "https://localhost:4200/login/oauth2/code/{registrationId}"
redirect-uri: "https://${server.http.interface}:${server.port}/login/oauth2/code/{registrationId}"
scope: openid, profile, email
client-name: My Provider
provider:
Original file line number Diff line number Diff line change
@@ -55,7 +55,6 @@ spring:
- ldap
- mem-auth
- db-sqlite
- sso-auth

chutney:
configuration-folder: .chutney/conf
4 changes: 0 additions & 4 deletions chutney/server/pom.xml
Original file line number Diff line number Diff line change
@@ -86,10 +86,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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<ChannelSecurityConfigurer<HttpSecurity>.ChannelRequestMatcherRegistry> requireChannel(Boolean sslEnabled) {
if (sslEnabled) {
return channelRequestMatcherRegistry -> channelRequestMatcherRegistry.anyRequest().requiresSecure();
} else {
return channelRequestMatcherRegistry -> channelRequestMatcherRegistry.anyRequest().requiresInsecure();
}
}
}
Original file line number Diff line number Diff line change
@@ -7,119 +7,33 @@

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 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.SsoOpenIdConnectConfig;
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.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
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 {

public static final String LOGIN_URL = UserController.BASE_URL + "/login";
public static final String LOGOUT_URL = UserController.BASE_URL + "/logout";
public static final String API_BASE_URL_PATTERN = "/api/**";

@Value("${management.endpoints.web.base-path:/actuator}")
public static String ACTUATOR_BASE_URL;

@Value("${server.ssl.enabled:true}")
Boolean sslEnabled;
public class ChutneyWebSecurityConfig extends AbstractChutneyWebSecurityConfig {

@Bean
public AuthenticationService authenticationService(Authorizations authorizations) {
return new AuthenticationService(authorizations);
}

@Bean
@ConditionalOnMissingBean
public SsoOpenIdConnectConfig emptySsoOpenIdConnectConfig() {
return new SsoOpenIdConnectConfig();
}


@Bean
@Order()
@ConditionalOnMissingBean(value = SecurityFilterChain.class)
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
configureBaseHttpSecurity(http, sslEnabled);
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, SsoOpenIdConnectController.BASE_URL + "/**")).permitAll()
.requestMatchers(new MvcRequestMatcher(introspector, API_BASE_URL_PATTERN)).authenticated()
.requestMatchers(new MvcRequestMatcher(introspector, ACTUATOR_BASE_URL + "/**")).hasAuthority(Authorization.ADMIN_ACCESS.name())
.anyRequest().permitAll();
})
.httpBasic(Customizer.withDefaults());
return http.build();
}

public void configureBaseHttpSecurity(final HttpSecurity http, Boolean sslEnabled) 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()));
}

public UserDto anonymous() {
UserDto anonymous = new UserDto();
anonymous.setId(User.ANONYMOUS.id);
anonymous.setName(User.ANONYMOUS.id);
anonymous.grantAuthority("ANONYMOUS");
return anonymous;
}

private Customizer<ChannelSecurityConfigurer<HttpSecurity>.ChannelRequestMatcherRegistry> requireChannel(Boolean sslEnabled) {
if (sslEnabled) {
return channelRequestMatcherRegistry -> channelRequestMatcherRegistry.anyRequest().requiresSecure();
} else {
return channelRequestMatcherRegistry -> channelRequestMatcherRegistry.anyRequest().requiresInsecure();
}
return configureHttp(http).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2017-2024 Enedis
*
* SPDX-License-Identifier: Apache-2.0
*
*/

package com.chutneytesting.security.api;

public class SsoOpenIdConnectConfigDto {
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 SsoOpenIdConnectConfigDto(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;
}
}
Original file line number Diff line number Diff line change
@@ -7,7 +7,9 @@

package com.chutneytesting.security.api;

import com.chutneytesting.security.infra.sso.SsoOpenIdConnectConfig;
import static com.chutneytesting.security.api.SsoOpenIdConnectMapper.toDto;

import com.chutneytesting.security.domain.SsoOpenIdConnectConfigService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
@@ -21,14 +23,17 @@ public class SsoOpenIdConnectController {

public static final String BASE_URL = "/api/v1/sso";

private final SsoOpenIdConnectConfig ssoOpenIdConnectConfig;
private final SsoOpenIdConnectConfigService ssoOpenIdConnectConfigService;

SsoOpenIdConnectController(SsoOpenIdConnectConfig ssoOpenIdConnectConfig) {
this.ssoOpenIdConnectConfig = ssoOpenIdConnectConfig;
SsoOpenIdConnectController(SsoOpenIdConnectConfigService ssoOpenIdConnectConfigService) {
this.ssoOpenIdConnectConfigService = ssoOpenIdConnectConfigService;
}

@GetMapping(path = "/config", produces = MediaType.APPLICATION_JSON_VALUE)
public SsoOpenIdConnectConfig getLastCampaignExecution() {
return ssoOpenIdConnectConfig;
public SsoOpenIdConnectConfigDto getSsoOpenIdConnectConfig() {
if (ssoOpenIdConnectConfigService == null) {
return null;
}
return toDto(ssoOpenIdConnectConfigService.getSsoOpenIdConnectConfig());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2017-2024 Enedis
*
* SPDX-License-Identifier: Apache-2.0
*
*/

package com.chutneytesting.security.api;

public class SsoOpenIdConnectMapper {
public static SsoOpenIdConnectConfigDto toDto(com.chutneytesting.security.domain.SsoOpenIdConnectConfig ssoOpenIdConnectConfig) {
if (ssoOpenIdConnectConfig == null) {
return null;
}
return new SsoOpenIdConnectConfigDto(
ssoOpenIdConnectConfig.issuer,
ssoOpenIdConnectConfig.clientId,
ssoOpenIdConnectConfig.clientSecret,
ssoOpenIdConnectConfig.responseType,
ssoOpenIdConnectConfig.scope,
ssoOpenIdConnectConfig.redirectBaseUrl,
ssoOpenIdConnectConfig.ssoProviderName,
ssoOpenIdConnectConfig.oidc
);
}
}
Original file line number Diff line number Diff line change
@@ -5,10 +5,9 @@
*
*/

package com.chutneytesting.security.infra.sso;
package com.chutneytesting.security.domain;

public class SsoOpenIdConnectConfig {

public final String issuer;
public final String clientId;
public final String clientSecret;
@@ -18,17 +17,6 @@ public class SsoOpenIdConnectConfig {
public final String ssoProviderName;
public final Boolean oidc;

public SsoOpenIdConnectConfig() {
this.issuer = null;
this.clientId = null;
this.clientSecret = null;
this.responseType = null;
this.scope = null;
this.redirectBaseUrl = null;
this.ssoProviderName = null;
this.oidc = null;
}

public SsoOpenIdConnectConfig(String issuer, String clientId, String clientSecret, String responseType, String scope, String redirectBaseUrl, String ssoProviderName, Boolean oidc) {
this.issuer = issuer;
this.clientId = clientId;
Loading