Skip to content

Commit 0ccffd5

Browse files
authored
Merge pull request #1495 from utmstack/backlog/add-saml-oidc-corporate-authentication-to-backend
Backlog/add saml OIDC corporate authentication to backend
2 parents 6ee8ddf + cb99274 commit 0ccffd5

File tree

82 files changed

+3934
-233
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+3934
-233
lines changed

backend/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@
192192
<groupId>org.springframework.boot</groupId>
193193
<artifactId>spring-boot-starter-mail</artifactId>
194194
</dependency>
195+
<!-- SAML2 Service Provider -->
196+
<dependency>
197+
<groupId>org.springframework.security</groupId>
198+
<artifactId>spring-security-saml2-service-provider</artifactId>
199+
</dependency>
195200
<dependency>
196201
<groupId>org.springframework.boot</groupId>
197202
<artifactId>spring-boot-starter-security</artifactId>

backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
package com.park.utmstack.config;
22

3-
import com.park.utmstack.loggin.api_key.ApiKeyUsageLoggingService;
4-
import com.park.utmstack.loggin.filter.MdcCleanupFilter;
53
import com.park.utmstack.repository.UserRepository;
64
import com.park.utmstack.security.AuthoritiesConstants;
75
import com.park.utmstack.security.api_key.ApiKeyConfigurer;
86
import com.park.utmstack.security.api_key.ApiKeyFilter;
97
import com.park.utmstack.security.internalApiKey.InternalApiKeyConfigurer;
108
import com.park.utmstack.security.internalApiKey.InternalApiKeyProvider;
119
import com.park.utmstack.security.jwt.JWTConfigurer;
12-
import com.park.utmstack.security.jwt.JWTFilter;
1310
import com.park.utmstack.security.jwt.TokenProvider;
14-
import com.park.utmstack.service.api_key.ApiKeyService;
11+
import com.park.utmstack.security.saml.Saml2LoginFailureHandler;
12+
import com.park.utmstack.security.saml.Saml2LoginSuccessHandler;
1513
import lombok.RequiredArgsConstructor;
16-
import org.apache.commons.net.util.SubnetUtils;
1714
import org.springframework.beans.factory.BeanInitializationException;
18-
import org.springframework.boot.web.servlet.FilterRegistrationBean;
1915
import org.springframework.context.annotation.Bean;
2016
import org.springframework.context.annotation.Configuration;
2117
import org.springframework.context.annotation.Import;
@@ -35,10 +31,10 @@
3531
import org.springframework.web.filter.CorsFilter;
3632
import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport;
3733

34+
3835
import javax.annotation.PostConstruct;
3936
import javax.servlet.http.HttpServletResponse;
40-
import java.util.concurrent.ConcurrentHashMap;
41-
import java.util.concurrent.ConcurrentMap;
37+
4238

4339
@Configuration
4440
@RequiredArgsConstructor
@@ -53,6 +49,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
5349
private final CorsFilter corsFilter;
5450
private final InternalApiKeyProvider internalApiKeyProvider;
5551
private final ApiKeyFilter apiKeyFilter;
52+
private final UserRepository userRepository;
53+
5654

5755
@PostConstruct
5856
public void init() {
@@ -110,7 +108,9 @@ public void configure(HttpSecurity http) throws Exception {
110108
.antMatchers("/api/releaseInfo").permitAll()
111109
.antMatchers("/api/account/reset-password/init").permitAll()
112110
.antMatchers("/api/account/reset-password/finish").permitAll()
111+
.antMatchers("/api/utm-providers").permitAll()
113112
.antMatchers("/api/images/all").permitAll()
113+
.antMatchers("/api/info/version").permitAll()
114114
.antMatchers("/api/enrollment/**").hasAnyAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER)
115115
.antMatchers("/api/tfa/verify-code").hasAnyAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER, AuthoritiesConstants.USER, AuthoritiesConstants.ADMIN)
116116
.antMatchers("/api/tfa/refresh").hasAnyAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER, AuthoritiesConstants.USER, AuthoritiesConstants.ADMIN)
@@ -126,6 +126,12 @@ public void configure(HttpSecurity http) throws Exception {
126126
.antMatchers("/management/info").permitAll()
127127
.antMatchers("/management/**").hasAnyAuthority(AuthoritiesConstants.ADMIN, AuthoritiesConstants.USER)
128128
.and()
129+
.saml2Login()
130+
.successHandler(new Saml2LoginSuccessHandler(tokenProvider,
131+
userRepository,
132+
saml2LoginFailureHandler()))
133+
.failureHandler(new Saml2LoginFailureHandler())
134+
.and()
129135
.apply(securityConfigurerAdapterForJwt())
130136
.and()
131137
.apply(securityConfigurerAdapterForInternalApiKey())
@@ -147,4 +153,10 @@ private ApiKeyConfigurer securityConfigurerAdapterForApiKey() {
147153
return new ApiKeyConfigurer(apiKeyFilter);
148154
}
149155

156+
157+
@Bean
158+
public Saml2LoginFailureHandler saml2LoginFailureHandler() {
159+
return new Saml2LoginFailureHandler();
160+
}
161+
150162
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.park.utmstack.config.saml;
2+
3+
import com.park.utmstack.repository.idp_provider.IdentityProviderConfigRepository;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
7+
@Configuration
8+
public class OAuth2ClientConfig {
9+
10+
@Bean
11+
public SamlRelyingPartyRegistrationRepository clientRegistrationRepository(IdentityProviderConfigRepository repo) {
12+
return new SamlRelyingPartyRegistrationRepository(repo);
13+
}
14+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.park.utmstack.config.saml;
2+
3+
import com.park.utmstack.repository.idp_provider.IdentityProviderConfigRepository;
4+
import com.park.utmstack.util.events.ProviderChangedEvent;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.context.event.EventListener;
7+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
8+
import org.springframework.stereotype.Component;
9+
10+
@Component
11+
@RequiredArgsConstructor
12+
public class ProviderChangeListener {
13+
14+
private final RelyingPartyRegistrationRepository repository;
15+
private final IdentityProviderConfigRepository identityProviderConfigRepository;
16+
17+
@EventListener
18+
public void handleProviderChanged(ProviderChangedEvent event) {
19+
if (repository instanceof SamlRelyingPartyRegistrationRepository customRepo) {
20+
customRepo.reloadProviders(identityProviderConfigRepository);
21+
}
22+
}
23+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.park.utmstack.config.saml;
2+
3+
import com.park.utmstack.config.Constants;
4+
import com.park.utmstack.domain.idp_provider.IdentityProviderConfig;
5+
import com.park.utmstack.repository.idp_provider.IdentityProviderConfigRepository;
6+
import com.park.utmstack.util.CipherUtil;
7+
import com.park.utmstack.util.saml.PemUtils;
8+
import org.springframework.security.saml2.core.Saml2X509Credential;
9+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
10+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
11+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
12+
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
13+
14+
import java.security.PrivateKey;
15+
import java.security.cert.X509Certificate;
16+
import java.util.Map;
17+
import java.util.concurrent.ConcurrentHashMap;
18+
19+
public class SamlRelyingPartyRegistrationRepository implements RelyingPartyRegistrationRepository {
20+
21+
private final Map<String, RelyingPartyRegistration> registrations = new ConcurrentHashMap<>();
22+
23+
public SamlRelyingPartyRegistrationRepository(IdentityProviderConfigRepository jpaProviderRepository) {
24+
loadProviders(jpaProviderRepository);
25+
}
26+
27+
@Override
28+
public RelyingPartyRegistration findByRegistrationId(String registrationId) {
29+
return registrations.get(registrationId);
30+
}
31+
32+
public void reloadProviders(IdentityProviderConfigRepository jpaProviderRepository) {
33+
registrations.clear();
34+
loadProviders(jpaProviderRepository);
35+
}
36+
37+
private void loadProviders(IdentityProviderConfigRepository jpaProviderRepository) {
38+
jpaProviderRepository.findAllByActiveTrue().forEach(entity -> {
39+
RelyingPartyRegistration registration = buildRelyingPartyRegistration(entity);
40+
registrations.put(entity.getProviderType().name().toLowerCase(), registration);
41+
});
42+
}
43+
44+
private RelyingPartyRegistration buildRelyingPartyRegistration(IdentityProviderConfig entity) {
45+
46+
PrivateKey spKey = PemUtils.parsePrivateKey(CipherUtil.decrypt(
47+
entity.getSpPrivateKeyPem(),
48+
System.getenv(Constants.ENV_ENCRYPTION_KEY)
49+
));
50+
X509Certificate spCert = PemUtils.parseCertificate(entity.getSpCertificatePem());
51+
52+
return RelyingPartyRegistrations
53+
.fromMetadataLocation(entity.getMetadataUrl())
54+
.registrationId(entity.getName())
55+
.entityId(entity.getSpEntityId())
56+
.assertionConsumerServiceLocation(entity.getSpAcsUrl())
57+
.signingX509Credentials(c -> c.add(Saml2X509Credential.signing(spKey, spCert)))
58+
.build();
59+
}
60+
61+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.park.utmstack.domain.idp_provider;
2+
3+
import com.park.utmstack.domain.idp_provider.enums.ProviderType;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.EqualsAndHashCode;
7+
import lombok.NoArgsConstructor;
8+
import org.hibernate.annotations.Type;
9+
10+
import javax.persistence.*;
11+
import java.time.LocalDateTime;
12+
13+
@Entity
14+
@Table(name = "utm_identity_provider_config")
15+
@Data
16+
@NoArgsConstructor
17+
@AllArgsConstructor
18+
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
19+
public class IdentityProviderConfig {
20+
21+
@Id
22+
@GeneratedValue(strategy = GenerationType.IDENTITY)
23+
@EqualsAndHashCode.Include
24+
private Long id;
25+
26+
@Column(nullable = false)
27+
private String name;
28+
29+
@Enumerated(EnumType.STRING)
30+
@Column(nullable = false)
31+
private ProviderType providerType;
32+
33+
/**
34+
* Metadata URL of the IdP (Keycloak, Okta, Azure, etc.)
35+
* Example: https://localhost:8443/realms/UTMSTACK/protocol/saml/descriptor
36+
*/
37+
@Column(name = "metadata_url", nullable = false, length = 512)
38+
private String metadataUrl;
39+
40+
/**
41+
* Service Provider private key in PEM format
42+
* Used to sign AuthnRequests and other outgoing SAML messages
43+
*/
44+
@Type(type = "text")
45+
@Column(name = "sp_private_key_pem", nullable = false, columnDefinition = "TEXT")
46+
private String spPrivateKeyPem;
47+
48+
/**
49+
* Service Provider public certificate in PEM format
50+
* Shared with IdP so it can validate signed requests from the SP
51+
*/
52+
@Type(type = "text")
53+
@Column(name = "sp_certificate_pem", nullable = false, columnDefinition = "TEXT")
54+
private String spCertificatePem;
55+
56+
@Column(name = "sp_entity_id", nullable = false, length = 512)
57+
private String spEntityId;
58+
59+
@Column(name = "sp_acs_url", nullable = false, length = 512)
60+
private String spAcsUrl;
61+
62+
/**
63+
* Flag to enable or disable this IdP configuration
64+
*/
65+
@Column(nullable = false)
66+
private Boolean active;
67+
68+
/**
69+
* Timestamp when the record was created
70+
*/
71+
@Column(nullable = false)
72+
private LocalDateTime createdAt;
73+
74+
/**
75+
* Timestamp when the record was last updated
76+
*/
77+
@Column(nullable = false)
78+
private LocalDateTime updatedAt;
79+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.park.utmstack.domain.idp_provider.enums;
2+
3+
public enum ProviderType {
4+
GOOGLE,
5+
KEYCLOAK,
6+
OKTA,
7+
MICROSOFT;
8+
9+
public static ProviderType from(String value) {
10+
return ProviderType.valueOf(value.toUpperCase());
11+
}
12+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.park.utmstack.repository.idp_provider;
2+
3+
import com.park.utmstack.domain.idp_provider.IdentityProviderConfig;
4+
import com.park.utmstack.domain.idp_provider.enums.ProviderType;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
7+
import org.springframework.stereotype.Repository;
8+
9+
import java.util.List;
10+
import java.util.Optional;
11+
12+
@Repository
13+
public interface IdentityProviderConfigRepository extends JpaRepository<IdentityProviderConfig, Long>, JpaSpecificationExecutor<IdentityProviderConfig> {
14+
15+
Optional<IdentityProviderConfig> findByProviderTypeAndActiveTrue(ProviderType providerType);
16+
17+
List<IdentityProviderConfig> findAllByActiveTrue();
18+
}

backend/src/main/java/com/park/utmstack/security/jwt/JWTConfigurer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
public class JWTConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
99

10-
private TokenProvider tokenProvider;
10+
private final TokenProvider tokenProvider;
1111

1212
public JWTConfigurer(TokenProvider tokenProvider) {
1313
this.tokenProvider = tokenProvider;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.park.utmstack.security.saml;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.security.core.AuthenticationException;
5+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
6+
import org.springframework.web.util.UriComponentsBuilder;
7+
8+
import javax.servlet.http.HttpServletRequest;
9+
import javax.servlet.http.HttpServletResponse;
10+
import java.io.IOException;
11+
import java.net.URI;
12+
import java.util.Objects;
13+
14+
/**
15+
* Failure handler for SAML2 login.
16+
* Redirects the user to the frontend with an error parameter.
17+
*/
18+
@Slf4j
19+
public class Saml2LoginFailureHandler implements AuthenticationFailureHandler {
20+
21+
@Override
22+
public void onAuthenticationFailure(HttpServletRequest request,
23+
HttpServletResponse response,
24+
AuthenticationException exception) throws IOException {
25+
26+
String scheme = Objects.requireNonNullElse(request.getHeader("X-Forwarded-Proto"), request.getScheme());
27+
String host = Objects.requireNonNullElse(request.getHeader("Host"), request.getServerName());
28+
29+
String frontBaseUrl = scheme + "://" + host;
30+
31+
URI redirectUri = UriComponentsBuilder.fromHttpUrl(frontBaseUrl)
32+
.queryParam("error", "saml2")
33+
.build().toUri();
34+
35+
response.sendRedirect(redirectUri.toString());
36+
}
37+
}

0 commit comments

Comments
 (0)