Spring Security with SAML and Okta

This commit is contained in:
Anshul BANSAL
2021-02-19 10:18:44 +02:00
parent 25e43334c4
commit bc4ea42b7d
40 changed files with 147 additions and 1123 deletions
+1
View File
@@ -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,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) {
@@ -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";
}
@@ -1,9 +0,0 @@
package com.baeldung.saml.auth;
/**
* @author jcavazos
*/
public enum AuthMethod {
DATABASE,
SAML
}
@@ -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();
}
}
@@ -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 {
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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();
}
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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();
}
}
}
@@ -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";
}
}
@@ -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:/";
}
}
}
@@ -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";
}
}
@@ -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";
}
}
}
@@ -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 "/";
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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;
}
}
}
@@ -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');
File diff suppressed because one or more lines are too long
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