Spring Security with SAML and Okta
This commit is contained in:
@@ -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
@@ -1,7 +0,0 @@
|
||||
body {
|
||||
padding-top: 5rem;
|
||||
}
|
||||
.starter-template {
|
||||
padding: 3rem 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user