Spring Security with SAML and Okta
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
<module>spring-security-legacy-oidc</module>
|
||||
<module>spring-security-oidc</module>
|
||||
<module>spring-security-okta</module>
|
||||
<module>spring-security-saml</module>
|
||||
<module>spring-security-web-react</module>
|
||||
<module>spring-security-web-rest</module>
|
||||
<module>spring-security-web-rest-basic-auth</module>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
### Relevant Articles:
|
||||
|
||||
- [Spring Security With Okta](https://www.baeldung.com/spring-security-okta)
|
||||
@@ -31,31 +31,14 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>nz.net.ultraq.thymeleaf</groupId>
|
||||
<artifactId>thymeleaf-layout-dialect</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.extensions</groupId>
|
||||
<artifactId>spring-security-saml2-core</artifactId>
|
||||
<version>1.0.10.RELEASE</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<version>${saml2-core.spring.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -85,6 +68,6 @@
|
||||
</build>
|
||||
|
||||
<properties>
|
||||
<saml.spring.version>1.0.10.RELEASE</saml.spring.version>
|
||||
<saml2-core.spring.version>1.0.10.RELEASE</saml2-core.spring.version>
|
||||
</properties>
|
||||
</project>
|
||||
|
||||
-3
@@ -3,9 +3,6 @@ package com.baeldung.saml;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* @author jcavazos
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
public static void main(String... args) {
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
package com.baeldung.saml;
|
||||
|
||||
public class Constants {
|
||||
public static final String OKTA_USERNAME_SUFFIX = "@oktaauth.com";
|
||||
public static final String DB_USERNAME_SUFFIX = "@dbauth.com";
|
||||
}
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
package com.baeldung.saml.auth;
|
||||
|
||||
/**
|
||||
* @author jcavazos
|
||||
*/
|
||||
public enum AuthMethod {
|
||||
DATABASE,
|
||||
SAML
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
package com.baeldung.saml.auth;
|
||||
|
||||
import com.baeldung.saml.repository.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.saml.SAMLCredential;
|
||||
import org.springframework.security.saml.userdetails.SAMLUserDetailsService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Supplies {@link UserDetails} information for both DB and SAML users
|
||||
* @author jcavazos
|
||||
*/
|
||||
@Service
|
||||
public class CombinedUserDetailsService implements UserDetailsService, SAMLUserDetailsService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CombinedUserDetailsService.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
public CombinedUserDetailsService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
|
||||
StoredUser storedUser = lookupUser(s);
|
||||
return new CustomUserDetails(
|
||||
AuthMethod.DATABASE,
|
||||
storedUser.getUsername(),
|
||||
storedUser.getPasswordHash(),
|
||||
new LinkedList<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException {
|
||||
LOGGER.info("Loading UserDetails by SAMLCredentials: {}", credential.getNameID());
|
||||
StoredUser storedUser = lookupUser(credential.getNameID().getValue());
|
||||
return new CustomUserDetails(
|
||||
AuthMethod.SAML,
|
||||
storedUser.getUsername(),
|
||||
storedUser.getPasswordHash(),
|
||||
new LinkedList<>());
|
||||
}
|
||||
|
||||
private StoredUser lookupUser(String username) {
|
||||
LOGGER.info("Loading UserDetails by username: {}", username);
|
||||
|
||||
Optional<StoredUser> user = userRepository.findByUsernameIgnoreCase(username);
|
||||
|
||||
if (!user.isPresent()) {
|
||||
LOGGER.error("User not found in database: {}", user);
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
|
||||
return user.get();
|
||||
}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package com.baeldung.saml.auth;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Represents the current authenticated user during HTTP requests
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface CurrentUser {
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
package com.baeldung.saml.auth;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@Getter
|
||||
public class CustomUserDetails extends User {
|
||||
|
||||
private final AuthMethod authMethod;
|
||||
|
||||
public CustomUserDetails(AuthMethod authMethod,
|
||||
String username,
|
||||
String password, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(username, password, authorities);
|
||||
this.authMethod = authMethod;
|
||||
}
|
||||
|
||||
public CustomUserDetails(AuthMethod authMethod,
|
||||
String username,
|
||||
String password,
|
||||
boolean enabled,
|
||||
boolean accountNonExpired,
|
||||
boolean credentialsNonExpired,
|
||||
boolean accountNonLocked,
|
||||
Collection<? extends GrantedAuthority> authorities) {
|
||||
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
|
||||
this.authMethod = authMethod;
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
package com.baeldung.saml.auth;
|
||||
|
||||
import com.baeldung.saml.Constants;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Provides authentication for "database" users, i.e. users whose credentials
|
||||
* are authenticated against the database instead of Okta/SAML
|
||||
* @author jcavazos
|
||||
*/
|
||||
@Component
|
||||
public class DbAuthProvider implements AuthenticationProvider {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DbAuthProvider.class);
|
||||
|
||||
private final CombinedUserDetailsService combinedUserDetailsService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
public DbAuthProvider(CombinedUserDetailsService combinedUserDetailsService, PasswordEncoder passwordEncoder) {
|
||||
this.combinedUserDetailsService = combinedUserDetailsService;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
if (!StringUtils.endsWithIgnoreCase(authentication.getPrincipal().toString(), Constants.DB_USERNAME_SUFFIX)) {
|
||||
// this user is not supported by DB authentication
|
||||
return null;
|
||||
}
|
||||
|
||||
UserDetails user = combinedUserDetailsService.loadUserByUsername(authentication.getPrincipal().toString());
|
||||
String rawPw = authentication.getCredentials() == null ? null : authentication.getCredentials().toString();
|
||||
|
||||
if (passwordEncoder.matches(rawPw, user.getPassword())) {
|
||||
LOGGER.warn("User successfully logged in: {}", user.getUsername());
|
||||
return new UsernamePasswordAuthenticationToken(
|
||||
user.getUsername(),
|
||||
rawPw,
|
||||
Collections.emptyList());
|
||||
} else {
|
||||
LOGGER.error("User failed to log in: {}", user.getUsername());
|
||||
throw new BadCredentialsException("Bad password");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> aClass) {
|
||||
return aClass.isAssignableFrom(UsernamePasswordAuthenticationToken.class);
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
package com.baeldung.saml.auth;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "USER")
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class StoredUser implements Serializable {
|
||||
|
||||
@Id
|
||||
@EqualsAndHashCode.Include
|
||||
private String id = UUID.randomUUID().toString();
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String passwordHash;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
public void setPasswordHash(String passwordHash) {
|
||||
this.passwordHash = passwordHash;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baeldung.saml.authentication;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.providers.ExpiringUsernameAuthenticationToken;
|
||||
import org.springframework.security.saml.SAMLAuthenticationProvider;
|
||||
import org.springframework.security.saml.SAMLCredential;
|
||||
|
||||
public class CustomSAMLAuthenticationProvider extends SAMLAuthenticationProvider {
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getEntitlements(SAMLCredential credential, Object userDetail) {
|
||||
|
||||
if(userDetail instanceof ExpiringUsernameAuthenticationToken) {
|
||||
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
|
||||
authorities.addAll(((ExpiringUsernameAuthenticationToken) userDetail).getAuthorities());
|
||||
return authorities;
|
||||
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
package com.baeldung.saml.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
/**
|
||||
* @author jcavazos
|
||||
*/
|
||||
@Configuration
|
||||
public class DbSecurityConfig {
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
package com.baeldung.saml.config;
|
||||
|
||||
import com.baeldung.saml.web.CurrentUserHandlerMethodArgumentResolver;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class MvcConfig implements WebMvcConfigurer {
|
||||
|
||||
final CurrentUserHandlerMethodArgumentResolver currentUserHandlerMethodArgumentResolver;
|
||||
|
||||
@Autowired
|
||||
public MvcConfig(CurrentUserHandlerMethodArgumentResolver currentUserHandlerMethodArgumentResolver) {
|
||||
this.currentUserHandlerMethodArgumentResolver = currentUserHandlerMethodArgumentResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
registry.addViewController("/").setViewName("index");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
if (!registry.hasMappingForPattern("/static/**")) {
|
||||
registry.addResourceHandler("/static/**")
|
||||
.addResourceLocations("/static/");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
|
||||
argumentResolvers.add(currentUserHandlerMethodArgumentResolver);
|
||||
}
|
||||
}
|
||||
+31
-154
File diff suppressed because it is too large
Load Diff
+35
-140
@@ -1,12 +1,9 @@
|
||||
package com.baeldung.saml.config;
|
||||
|
||||
import com.baeldung.saml.auth.AuthMethod;
|
||||
import com.baeldung.saml.auth.CustomUserDetails;
|
||||
import com.baeldung.saml.auth.DbAuthProvider;
|
||||
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -18,8 +15,6 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
|
||||
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.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.saml.*;
|
||||
import org.springframework.security.saml.key.KeyManager;
|
||||
import org.springframework.security.saml.metadata.*;
|
||||
@@ -33,19 +28,10 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
|
||||
/**
|
||||
* Main configuration class for wiring together DB + SAML authentication
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(securedEnabled = true)
|
||||
public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements DisposableBean {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WebSecurityConfig.class);
|
||||
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Value("${saml.sp}")
|
||||
private String samlAudience;
|
||||
@@ -58,23 +44,12 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements D
|
||||
@Qualifier("saml")
|
||||
private SimpleUrlAuthenticationFailureHandler samlAuthFailureHandler;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("saml")
|
||||
private Timer samlBackgroundTaskTimer;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("saml")
|
||||
private MultiThreadedHttpConnectionManager samlMultiThreadedHttpConnectionManager;
|
||||
|
||||
@Autowired
|
||||
private SAMLEntryPoint samlEntryPoint;
|
||||
|
||||
@Autowired
|
||||
private SAMLLogoutFilter samlLogoutFilter;
|
||||
|
||||
@Autowired
|
||||
private MetadataDisplayFilter metadataDisplayFilter;
|
||||
|
||||
@Autowired
|
||||
private SAMLLogoutProcessingFilter samlLogoutProcessingFilter;
|
||||
|
||||
@@ -87,15 +62,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements D
|
||||
@Autowired
|
||||
private ExtendedMetadata extendedMetadata;
|
||||
|
||||
@Autowired
|
||||
private CachingMetadataManager cachingMetadataManager;
|
||||
|
||||
@Autowired
|
||||
private KeyManager keyManager;
|
||||
|
||||
@Autowired
|
||||
private DbAuthProvider dbAuthProvider;
|
||||
|
||||
public MetadataGenerator metadataGenerator() {
|
||||
MetadataGenerator metadataGenerator = new MetadataGenerator();
|
||||
metadataGenerator.setEntityId(samlAudience);
|
||||
@@ -105,16 +74,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements D
|
||||
return metadataGenerator;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter() throws Exception {
|
||||
SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter = new SAMLWebSSOHoKProcessingFilter();
|
||||
samlWebSSOHoKProcessingFilter.setAuthenticationSuccessHandler(samlAuthSuccessHandler);
|
||||
samlWebSSOHoKProcessingFilter.setAuthenticationManager(authenticationManager());
|
||||
samlWebSSOHoKProcessingFilter.setAuthenticationFailureHandler(samlAuthFailureHandler);
|
||||
return samlWebSSOHoKProcessingFilter;
|
||||
}
|
||||
|
||||
// Processing filter for WebSSO profile messages
|
||||
@Bean
|
||||
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
|
||||
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
|
||||
@@ -124,36 +83,22 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements D
|
||||
return samlWebSSOProcessingFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the security filter chain in order to support SSO Auth by using SAML 2.0
|
||||
*
|
||||
* @return Filter chain proxy
|
||||
*/
|
||||
@Bean
|
||||
public FilterChainProxy samlFilter() throws Exception {
|
||||
List<SecurityFilterChain> chains = new ArrayList<>();
|
||||
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
|
||||
samlEntryPoint));
|
||||
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
|
||||
samlLogoutFilter));
|
||||
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
|
||||
metadataDisplayFilter));
|
||||
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
|
||||
samlWebSSOProcessingFilter()));
|
||||
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSOHoK/**"),
|
||||
samlWebSSOHoKProcessingFilter()));
|
||||
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
|
||||
samlLogoutProcessingFilter));
|
||||
samlWebSSOProcessingFilter()));
|
||||
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"),
|
||||
samlDiscovery));
|
||||
samlDiscovery));
|
||||
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
|
||||
samlEntryPoint));
|
||||
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
|
||||
samlLogoutFilter));
|
||||
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
|
||||
samlLogoutProcessingFilter));
|
||||
return new FilterChainProxy(chains);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authentication manager currently used by Spring.
|
||||
* It represents a bean definition with the aim allow wiring from
|
||||
* other classes performing the Inversion of Control (IoC).
|
||||
*/
|
||||
@Bean
|
||||
@Override
|
||||
public AuthenticationManager authenticationManagerBean() throws Exception {
|
||||
@@ -165,90 +110,40 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements D
|
||||
return new MetadataGeneratorFilter(metadataGenerator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the web based security configuration.
|
||||
*
|
||||
* @param http Allows configuring web based security for specific http requests.
|
||||
*/
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf()
|
||||
.disable();
|
||||
|
||||
.csrf()
|
||||
.disable();
|
||||
|
||||
http
|
||||
.httpBasic()
|
||||
.authenticationEntryPoint((request, response, authException) -> {
|
||||
/*
|
||||
Unauthenticated requests will be routed through this class.
|
||||
|
||||
If a request is intended to begin the SAML auth workflow, it will be
|
||||
initiated here {@see IndexController.preAuth()}.
|
||||
|
||||
Otherwise, the user will be redirected to the pre-auth landing page.
|
||||
*/
|
||||
if (request.getRequestURI().endsWith("doSaml")) {
|
||||
samlEntryPoint.commence(request, response, authException);
|
||||
} else {
|
||||
response.sendRedirect("/");
|
||||
}
|
||||
});
|
||||
|
||||
.httpBasic()
|
||||
.authenticationEntryPoint(samlEntryPoint);
|
||||
|
||||
http
|
||||
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
|
||||
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
|
||||
.addFilterBefore(samlFilter(), CsrfFilter.class);
|
||||
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
|
||||
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
|
||||
.addFilterBefore(samlFilter(), CsrfFilter.class);
|
||||
|
||||
http
|
||||
.authorizeRequests()
|
||||
.antMatchers("/").permitAll()
|
||||
.antMatchers("/pre-auth**").permitAll()
|
||||
.antMatchers("/form-login**").permitAll()
|
||||
.antMatchers("/error").permitAll()
|
||||
.antMatchers("/saml/**").permitAll()
|
||||
.antMatchers("/css/**").permitAll()
|
||||
.antMatchers("/img/**").permitAll()
|
||||
.antMatchers("/js/**").permitAll()
|
||||
.antMatchers("/sw.js").permitAll()
|
||||
.anyRequest().authenticated();
|
||||
|
||||
.authorizeRequests()
|
||||
.antMatchers("/").permitAll()
|
||||
.anyRequest().authenticated();
|
||||
|
||||
http
|
||||
.logout()
|
||||
.addLogoutHandler((request, response, authentication) -> {
|
||||
/*
|
||||
If the user is authenticated via SAML, we need to direct them to the appropriate
|
||||
SAML logout flow
|
||||
*/
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth instanceof CustomUserDetails) {
|
||||
CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
|
||||
if (userDetails.getAuthMethod() == AuthMethod.SAML) {
|
||||
try {
|
||||
response.sendRedirect("/saml/logout");
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error processing logout for SAML user", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
.logout()
|
||||
.addLogoutHandler((request, response, authentication) -> {
|
||||
try {
|
||||
response.sendRedirect("/saml/logout");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a chain of authentication providers, starting with DB auth.
|
||||
*
|
||||
* If the user is not supported by DB authentication, it will fall through
|
||||
* to the SAML auth provider.
|
||||
*/
|
||||
@Override
|
||||
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||
auth.authenticationProvider(dbAuthProvider);
|
||||
auth.authenticationProvider(samlAuthenticationProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
this.samlBackgroundTaskTimer.purge();
|
||||
this.samlBackgroundTaskTimer.cancel();
|
||||
this.samlMultiThreadedHttpConnectionManager.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
package com.baeldung.saml.controller;
|
||||
|
||||
import com.baeldung.saml.controller.model.DbAuthCredentials;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
/**
|
||||
* Handles standard form-based logins used to authenticate users against the database.
|
||||
* @author jcavazos
|
||||
*/
|
||||
@Controller
|
||||
public class DbLoginController {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
|
||||
@Autowired
|
||||
public DbLoginController(AuthenticationManager authenticationManager) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
}
|
||||
|
||||
@GetMapping("/form-login")
|
||||
public String formLogin(@RequestParam(required = false) String username, Model model) {
|
||||
DbAuthCredentials credentials = new DbAuthCredentials();
|
||||
|
||||
if (StringUtils.isNotBlank(username)) {
|
||||
credentials.setUsername(username);
|
||||
}
|
||||
|
||||
model.addAttribute("credentials", credentials);
|
||||
return "form-login";
|
||||
}
|
||||
|
||||
@PostMapping("/form-login")
|
||||
public String doLogin(@ModelAttribute DbAuthCredentials credentials,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
try {
|
||||
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
|
||||
credentials.getUsername(), credentials.getPassword()));
|
||||
|
||||
if (authentication.isAuthenticated()) {
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
} else {
|
||||
throw new Exception("Unauthenticated");
|
||||
}
|
||||
|
||||
return "redirect:/landing";
|
||||
} catch (Exception e) {
|
||||
redirectAttributes.addFlashAttribute("error", "Login Failed");
|
||||
return "redirect:/form-login?username="+credentials.getUsername();
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.baeldung.saml.controller;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@RequestMapping("/")
|
||||
public String index() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/auth")
|
||||
public String handleSamlAuth() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null) {
|
||||
return "redirect:/home";
|
||||
} else {
|
||||
return "/";
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping("/home")
|
||||
public String home(Model model) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
model.addAttribute("username", authentication.getPrincipal());
|
||||
return "home";
|
||||
}
|
||||
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
package com.baeldung.saml.controller;
|
||||
|
||||
import com.baeldung.saml.Constants;
|
||||
import com.baeldung.saml.controller.model.PreAuthUsername;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
/**
|
||||
* "Pre-auth" landing page in which the user will enter their username. Depending on
|
||||
* the username suffix, the user will either be directed to provide a password for
|
||||
* database authentication, or the SAML auth flow will be initiated.
|
||||
*
|
||||
* The redirect to /doSaml will be picked up by the custom auth entry point defined
|
||||
* in {@link com.baeldung.saml.config.WebSecurityConfig}.
|
||||
*
|
||||
* @author jcavazos
|
||||
*/
|
||||
@Controller
|
||||
public class IndexController {
|
||||
|
||||
@GetMapping
|
||||
public String index(Model model) {
|
||||
model.addAttribute("username", new PreAuthUsername());
|
||||
return "index";
|
||||
}
|
||||
|
||||
@PostMapping("/pre-auth")
|
||||
public String preAuth(@ModelAttribute PreAuthUsername username,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
if (StringUtils.endsWithIgnoreCase(username.getUsername(), Constants.OKTA_USERNAME_SUFFIX)) {
|
||||
// redirect to SAML
|
||||
return "redirect:/doSaml";
|
||||
} else if (StringUtils.endsWithIgnoreCase(username.getUsername(), Constants.DB_USERNAME_SUFFIX)) {
|
||||
// redirect to DB/form login
|
||||
return "redirect:/form-login?username="+username.getUsername();
|
||||
} else {
|
||||
redirectAttributes.addFlashAttribute("error", "Invalid Username");
|
||||
return "redirect:/";
|
||||
}
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
package com.baeldung.saml.controller;
|
||||
|
||||
import com.baeldung.saml.auth.CurrentUser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
/**
|
||||
* This page should be guarded by the app's combined authentication. Thus, only users who
|
||||
* have successfully authenticated via DB or SAML auth should have access
|
||||
*
|
||||
*/
|
||||
@Controller
|
||||
public class LandingController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LandingController.class);
|
||||
|
||||
@RequestMapping("/landing")
|
||||
public String landing(@CurrentUser User user, Model model) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
LOGGER.info("Current auth: {}", authentication==null?"NULL":authentication.getPrincipal());
|
||||
model.addAttribute("username", user.getUsername());
|
||||
return "landing";
|
||||
}
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
package com.baeldung.saml.controller;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.saml.metadata.MetadataManager;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Page to select which Identity Provider (IDP) to authenticate against when beginning SAML auth flow.
|
||||
*
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/saml")
|
||||
public class SSOController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SSOController.class);
|
||||
|
||||
private final MetadataManager metadata;
|
||||
|
||||
@Autowired
|
||||
public SSOController(MetadataManager metadata) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/discovery", method = RequestMethod.GET)
|
||||
public String idpSelection(Model model) {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
LOGGER.info("Current auth: {}", auth==null?"NULL":auth.getPrincipal());
|
||||
if (auth == null || (auth instanceof AnonymousAuthenticationToken)) {
|
||||
Set<String> idps = metadata.getIDPEntityNames();
|
||||
idps.forEach(idp -> LOGGER.info("Configured Identity Provider for SSO: {}", idp));
|
||||
model.addAttribute("idps", idps);
|
||||
return "discovery";
|
||||
} else {
|
||||
LOGGER.warn("The current user is already logged in: {}", auth);
|
||||
return "redirect:/landing";
|
||||
}
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
package com.baeldung.saml.controller;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
/**
|
||||
* The SAML auth workflow is initiated by redirecting the request to the /doSaml
|
||||
* endpoint. When the SAML flow is complete, the browser will be directed back to /doSaml.
|
||||
*
|
||||
* This class handles the result of that flow. If the user is successfully authenticated,
|
||||
* they should be directed to the guarded landing page.
|
||||
*
|
||||
* @see IndexController
|
||||
* @see com.baeldung.saml.config.WebSecurityConfig
|
||||
*
|
||||
* @author jcavazos
|
||||
*/
|
||||
@Controller
|
||||
public class SamlResponseController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SamlResponseController.class);
|
||||
|
||||
@GetMapping(value = "/doSaml")
|
||||
public String handleSamlAuth() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
LOGGER.info("doSaml auth result: {}", auth);
|
||||
if (auth != null) {
|
||||
return "redirect:/landing";
|
||||
} else {
|
||||
return "/";
|
||||
}
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package com.baeldung.saml.controller.model;
|
||||
|
||||
public class DbAuthCredentials {
|
||||
private String username;
|
||||
private String password;
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package com.baeldung.saml.controller.model;
|
||||
|
||||
public class PreAuthUsername {
|
||||
private String username;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package com.baeldung.saml.repository;
|
||||
|
||||
import com.baeldung.saml.auth.StoredUser;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<StoredUser, String> {
|
||||
Optional<StoredUser> findByUsernameIgnoreCase(String username);
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
package com.baeldung.saml.web;
|
||||
|
||||
import com.baeldung.saml.auth.CombinedUserDetailsService;
|
||||
import com.baeldung.saml.auth.CurrentUser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.support.WebArgumentResolver;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
/**
|
||||
*/
|
||||
@Component
|
||||
public class CurrentUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final CombinedUserDetailsService combinedUserDetailsService;
|
||||
|
||||
@Autowired
|
||||
public CurrentUserHandlerMethodArgumentResolver(CombinedUserDetailsService combinedUserDetailsService) {
|
||||
this.combinedUserDetailsService = combinedUserDetailsService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter methodParameter) {
|
||||
return methodParameter.getParameterAnnotation(CurrentUser.class) != null
|
||||
&& methodParameter.getParameterType().equals(User.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter methodParameter,
|
||||
ModelAndViewContainer modelAndViewContainer,
|
||||
NativeWebRequest nativeWebRequest,
|
||||
WebDataBinderFactory webDataBinderFactory) throws Exception {
|
||||
if (this.supportsParameter(methodParameter)) {
|
||||
Authentication authentication = (Authentication) nativeWebRequest.getUserPrincipal();
|
||||
|
||||
if (authentication.getPrincipal() instanceof User) {
|
||||
return authentication.getPrincipal();
|
||||
} else if (authentication.getPrincipal() instanceof String) {
|
||||
return combinedUserDetailsService.loadUserByUsername((String)authentication.getPrincipal());
|
||||
}
|
||||
|
||||
throw new RuntimeException("Unsupported principal: "+authentication.getPrincipal());
|
||||
} else {
|
||||
return WebArgumentResolver.UNRESOLVED;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -2,8 +2,5 @@ saml.keystore.location=classpath:/saml/samlKeystore.jks
|
||||
saml.keystore.password=oktaiscool
|
||||
saml.keystore.alias=oktasaml
|
||||
|
||||
saml.metadataUrl=https://dev-926666.okta.com/app/dev-926666_baeldungspringsecuritysaml_1/exk26fxqrz8LLk9dV4x7/sso/saml
|
||||
saml.idp=http://www.okta.com/exk26fxqrz8LLk9dV4x7
|
||||
saml.sp=http://localhost:8080/saml/metadata
|
||||
|
||||
logging.level.root: info
|
||||
saml.sp=http://localhost:8080/saml/metadata
|
||||
@@ -1,4 +0,0 @@
|
||||
-- PW: oktaiscool
|
||||
INSERT INTO user (ID, USERNAME, PASSWORD_HASH) VALUES
|
||||
('17e3d83c-6e09-41b8-b4ee-b4b14cb8a797', '[email protected]', '$2y$12$0uXgosmcpIRWLxcAhCx0s.dUEZbrAHa0F0pkn6MO4Y57PCJ5fDjfa'),
|
||||
('17e3d83c-6e09-41b8-b4ee-b4b14cb8a798', '[email protected]', '$2y$12$0uXgosmcpIRWLxcAhCx0s.dUEZbrAHa0F0pkn6MO4Y57PCJ5fDjfa');
|
||||
Vendored
-7
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user