Initial Commit

This commit is contained in:
Anshul BANSAL
2021-02-14 18:11:20 +02:00
parent d89a51b555
commit 25e43334c4
39 changed files with 1594 additions and 0 deletions
@@ -0,0 +1,3 @@
### Relevant Articles:
- [Spring Security With Okta](https://www.baeldung.com/spring-security-okta)
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-saml</artifactId>
<version>1.0-SNAPSHOT</version>
<name>spring-security-saml</name>
<packaging>war</packaging>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../parent-boot-2</relativePath>
</parent>
<repositories>
<repository>
<id>Shibboleth</id>
<name>Shibboleth</name>
<url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<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>
</dependency>
</dependencies>
<build>
<finalName>spring-security-saml</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<addResources>true</addResources>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<saml.spring.version>1.0.10.RELEASE</saml.spring.version>
</properties>
</project>
@@ -0,0 +1,14 @@
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) {
SpringApplication.run(Application.class, args);
}
}
@@ -0,0 +1,6 @@
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";
}
@@ -0,0 +1,9 @@
package com.baeldung.saml.auth;
/**
* @author jcavazos
*/
public enum AuthMethod {
DATABASE,
SAML
}
@@ -0,0 +1,66 @@
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();
}
}
@@ -0,0 +1,12 @@
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 {
}
@@ -0,0 +1,32 @@
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;
}
}
@@ -0,0 +1,64 @@
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);
}
}
@@ -0,0 +1,52 @@
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,17 @@
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();
}
}
@@ -0,0 +1,40 @@
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);
}
}
@@ -0,0 +1,254 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.*;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
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);
@Value("${saml.sp}")
private String samlAudience;
@Autowired
@Qualifier("saml")
private SavedRequestAwareAuthenticationSuccessHandler samlAuthSuccessHandler;
@Autowired
@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;
@Autowired
private SAMLDiscovery samlDiscovery;
@Autowired
private SAMLAuthenticationProvider samlAuthenticationProvider;
@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);
metadataGenerator.setExtendedMetadata(extendedMetadata);
metadataGenerator.setIncludeDiscoveryExtension(false);
metadataGenerator.setKeyManager(keyManager);
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();
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(samlAuthSuccessHandler);
samlWebSSOProcessingFilter.setAuthenticationFailureHandler(samlAuthFailureHandler);
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));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"),
samlDiscovery));
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 {
return super.authenticationManagerBean();
}
@Bean
public MetadataGeneratorFilter metadataGeneratorFilter() {
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();
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("/");
}
});
http
.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();
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);
}
}
}
});
}
/**
* 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();
}
}
@@ -0,0 +1,63 @@
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,47 @@
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:/";
}
}
}
@@ -0,0 +1,30 @@
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";
}
}
@@ -0,0 +1,48 @@
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";
}
}
}
@@ -0,0 +1,37 @@
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 "/";
}
}
}
@@ -0,0 +1,20 @@
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;
}
}
@@ -0,0 +1,15 @@
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;
}
}
@@ -0,0 +1,12 @@
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);
}
@@ -0,0 +1,53 @@
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;
}
}
}
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,4 @@
-- 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');
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor
entityID="http://www.okta.com/exk26fxqrz8LLk9dV4x7"
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
<md:IDPSSODescriptor
WantAuthnRequestsSigned="false"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIDpDCCAoygAwIBAgIGAXGiSQ7ZMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEG
A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi05MjY2NjYxHDAaBgkqhkiG9w0BCQEW
DWluZm9Ab2t0YS5jb20wHhcNMjAwNDIyMTQyNjA5WhcNMzAwNDIyMTQyNzA5WjCBkjELMAkGA1UE
BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV
BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtOTI2NjY2MRwwGgYJ
KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
g1rQYYqeVx2gl/UUnLJzp5hrm06VOILJB9hIUmNqXgWV3UjzDq/zX0KW8MENjsO7+S8a+LLnYRkb
N5egH9FSt8AHtB1pmfXDtpUQmWe9yJbNxbCISoc6XzCmaRw3HRv9pK5SciIutciz9lvFaHMWAWtP
MmQSKdhMet52tuf6sTy4ODeXjyMnD9q5QOKww1SJ678wjHbGRRhNvCxvTSAH33sa4oNCf2RvP9hp
NiJRcYW9yLZXmZArPQOuAx5PIXfHhK2e4ac39YO4fgO7gwU5TZ+vL7o6iEmd9tk44PrND0ZV5yzZ
+Y33Hiun3fIiZu/nZZGUjm4k4exl8JJpwrVTHQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBcfHcL
2DjTjZGoANF4dPpGXTYdVnL/XzGiLS+3LR/HDrEz/EqsHouF40RnzdZ7Ax7RReKBYCUUqHpSE+LU
ductz2ANguzyseGEn72I4Ym4ytQWnFyTXeW+xI9CoCLGfOUhT1hlKjsu/qNM8qwKFPWkzQp7mDN8
S9MGhsnbiyeD/lceAEKw16Os73/sX2j7F+43WVCYRDCRB8pRIPfcqYLXUIUSstQlwEvCF7HyeO4+
jxKHA1tp9Cpmj7/VD9TE3fyvrbVmfjTbKjF7/0wYQNfbHDDko0ratDMAizG5/d3i9wk9KbGCHSxT
ph5nl1pdjKgAYPK0iNDnGCZbGKzXOrqV
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
</md:NameIDFormat>
<md:SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://dev-926666.okta.com/app/dev-926666_baeldungspringsecuritysaml_1/exk26fxqrz8LLk9dV4x7/sso/saml" />
<md:SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://dev-926666.okta.com/app/dev-926666_baeldungspringsecuritysaml_1/exk26fxqrz8LLk9dV4x7/sso/saml" />
</md:IDPSSODescriptor>
</md:EntityDescriptor>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
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