Initial Commit
This commit is contained in:
parent
d89a51b555
commit
25e43334c4
3
spring-security-modules/spring-security-saml/README.md
Normal file
3
spring-security-modules/spring-security-saml/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
### Relevant Articles:
|
||||||
|
|
||||||
|
- [Spring Security With Okta](https://www.baeldung.com/spring-security-okta)
|
90
spring-security-modules/spring-security-saml/pom.xml
Normal file
90
spring-security-modules/spring-security-saml/pom.xml
Normal file
@ -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,355 @@
|
|||||||
|
package com.baeldung.saml.config;
|
||||||
|
|
||||||
|
import com.baeldung.saml.auth.CombinedUserDetailsService;
|
||||||
|
import org.apache.commons.httpclient.HttpClient;
|
||||||
|
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
|
||||||
|
import org.apache.velocity.app.VelocityEngine;
|
||||||
|
import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider;
|
||||||
|
import org.opensaml.saml2.metadata.provider.MetadataProvider;
|
||||||
|
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
|
||||||
|
import org.opensaml.util.resource.ResourceException;
|
||||||
|
import org.opensaml.xml.parse.ParserPool;
|
||||||
|
import org.opensaml.xml.parse.StaticBasicParserPool;
|
||||||
|
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.core.io.DefaultResourceLoader;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.security.saml.*;
|
||||||
|
import org.springframework.security.saml.context.SAMLContextProviderImpl;
|
||||||
|
import org.springframework.security.saml.key.JKSKeyManager;
|
||||||
|
import org.springframework.security.saml.key.KeyManager;
|
||||||
|
import org.springframework.security.saml.log.SAMLDefaultLogger;
|
||||||
|
import org.springframework.security.saml.metadata.CachingMetadataManager;
|
||||||
|
import org.springframework.security.saml.metadata.ExtendedMetadata;
|
||||||
|
import org.springframework.security.saml.metadata.ExtendedMetadataDelegate;
|
||||||
|
import org.springframework.security.saml.metadata.MetadataDisplayFilter;
|
||||||
|
import org.springframework.security.saml.parser.ParserPoolHolder;
|
||||||
|
import org.springframework.security.saml.processor.*;
|
||||||
|
import org.springframework.security.saml.util.VelocityFactory;
|
||||||
|
import org.springframework.security.saml.websso.*;
|
||||||
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
|
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||||
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure settings and information required for SAML 2.0 authentication with Okta
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class SamlSecurityConfig {
|
||||||
|
|
||||||
|
@Value("${saml.keystore.location}")
|
||||||
|
private String samlKeystoreLocation;
|
||||||
|
|
||||||
|
@Value("${saml.keystore.password}")
|
||||||
|
private String samlKeystorePassword;
|
||||||
|
|
||||||
|
@Value("${saml.keystore.alias}")
|
||||||
|
private String samlKeystoreAlias;
|
||||||
|
|
||||||
|
@Value("${saml.idp}")
|
||||||
|
private String defaultIdp;
|
||||||
|
|
||||||
|
@Value("${saml.metadataUrl}")
|
||||||
|
private String metadataUrl;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CombinedUserDetailsService combinedUserDetailsService;
|
||||||
|
|
||||||
|
private final Timer backgroundTaskTimer = new Timer(true);
|
||||||
|
private final MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager
|
||||||
|
= new MultiThreadedHttpConnectionManager();
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Qualifier("saml")
|
||||||
|
public Timer getBackgroundTaskTimer() {
|
||||||
|
return backgroundTaskTimer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Qualifier("saml")
|
||||||
|
public MultiThreadedHttpConnectionManager getMultiThreadedHttpConnectionManager() {
|
||||||
|
return multiThreadedHttpConnectionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialization of the velocity engine
|
||||||
|
@Bean
|
||||||
|
public VelocityEngine velocityEngine() {
|
||||||
|
return VelocityFactory.getEngine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// XML parser pool needed for OpenSAML parsing
|
||||||
|
@Bean(initMethod = "initialize")
|
||||||
|
public StaticBasicParserPool parserPool() {
|
||||||
|
return new StaticBasicParserPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "parserPoolHolder")
|
||||||
|
public ParserPoolHolder parserPoolHolder() {
|
||||||
|
return new ParserPoolHolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bindings, encoders and decoders used for creating and parsing messages
|
||||||
|
@Bean
|
||||||
|
public HttpClient httpClient() {
|
||||||
|
return new HttpClient(this.multiThreadedHttpConnectionManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAML Authentication Provider responsible for validating of received SAML
|
||||||
|
// messages
|
||||||
|
@Bean
|
||||||
|
public SAMLAuthenticationProvider samlAuthenticationProvider() {
|
||||||
|
SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
|
||||||
|
samlAuthenticationProvider.setUserDetails(combinedUserDetailsService);
|
||||||
|
samlAuthenticationProvider.setForcePrincipalAsString(false);
|
||||||
|
return samlAuthenticationProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider of default SAML Context
|
||||||
|
@Bean
|
||||||
|
public SAMLContextProviderImpl contextProvider() {
|
||||||
|
return new SAMLContextProviderImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialization of OpenSAML library
|
||||||
|
@Bean
|
||||||
|
public static SAMLBootstrap sAMLBootstrap() {
|
||||||
|
return new SAMLBootstrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger for SAML messages and events
|
||||||
|
@Bean
|
||||||
|
public SAMLDefaultLogger samlLogger() {
|
||||||
|
return new SAMLDefaultLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAML 2.0 WebSSO Assertion Consumer
|
||||||
|
@Bean
|
||||||
|
public WebSSOProfileConsumer webSSOprofileConsumer() {
|
||||||
|
return new WebSSOProfileConsumerImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAML 2.0 Holder-of-Key WebSSO Assertion Consumer
|
||||||
|
@Bean
|
||||||
|
@Qualifier("hokWebSSOprofileConsumer")
|
||||||
|
public WebSSOProfileConsumerHoKImpl hokWebSSOProfileConsumer() {
|
||||||
|
return new WebSSOProfileConsumerHoKImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAML 2.0 Web SSO profile
|
||||||
|
@Bean
|
||||||
|
public WebSSOProfile webSSOprofile() {
|
||||||
|
return new WebSSOProfileImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAML 2.0 Holder-of-Key Web SSO profile
|
||||||
|
@Bean
|
||||||
|
public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
|
||||||
|
return new WebSSOProfileConsumerHoKImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAML 2.0 ECP profile
|
||||||
|
@Bean
|
||||||
|
public WebSSOProfileECPImpl ecpProfile() {
|
||||||
|
return new WebSSOProfileECPImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SingleLogoutProfile logoutProfile() {
|
||||||
|
return new SingleLogoutProfileImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Central storage of cryptographic keys
|
||||||
|
@Bean
|
||||||
|
public KeyManager keyManager() {
|
||||||
|
DefaultResourceLoader loader = new DefaultResourceLoader();
|
||||||
|
Resource storeFile = loader.getResource(samlKeystoreLocation);
|
||||||
|
Map<String, String> passwords = new HashMap<>();
|
||||||
|
passwords.put(samlKeystoreAlias, samlKeystorePassword);
|
||||||
|
return new JKSKeyManager(storeFile, samlKeystorePassword, passwords, samlKeystoreAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebSSOProfileOptions defaultWebSSOProfileOptions() {
|
||||||
|
WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
|
||||||
|
webSSOProfileOptions.setIncludeScoping(false);
|
||||||
|
return webSSOProfileOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry point to initialize authentication, default values taken from
|
||||||
|
// properties file
|
||||||
|
@Bean
|
||||||
|
public SAMLEntryPoint samlEntryPoint() {
|
||||||
|
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
|
||||||
|
samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
|
||||||
|
return samlEntryPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup advanced info about metadata
|
||||||
|
@Bean
|
||||||
|
public ExtendedMetadata extendedMetadata() {
|
||||||
|
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
|
||||||
|
extendedMetadata.setIdpDiscoveryEnabled(true);
|
||||||
|
extendedMetadata.setSigningAlgorithm("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
|
||||||
|
extendedMetadata.setSignMetadata(true);
|
||||||
|
extendedMetadata.setEcpEnabled(true);
|
||||||
|
return extendedMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDP Discovery Service
|
||||||
|
@Bean
|
||||||
|
public SAMLDiscovery samlIDPDiscovery() {
|
||||||
|
SAMLDiscovery idpDiscovery = new SAMLDiscovery();
|
||||||
|
idpDiscovery.setIdpSelectionPath("/saml/discovery");
|
||||||
|
return idpDiscovery;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Qualifier("okta")
|
||||||
|
public ExtendedMetadataDelegate oktaExtendedMetadataProvider() throws MetadataProviderException {
|
||||||
|
File metadata = null;
|
||||||
|
try {
|
||||||
|
metadata = new File("/Users/anshulbansal/eclipse-workspace/tutorials/spring-security-modules/spring-security-saml/src/main/resources/saml/metadata/sso.xml");
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
FilesystemMetadataProvider provider = null;
|
||||||
|
provider = new FilesystemMetadataProvider(metadata);
|
||||||
|
provider.setParserPool(parserPool());
|
||||||
|
return new ExtendedMetadataDelegate(provider, extendedMetadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDP Metadata configuration - paths to metadata of IDPs in circle of trust
|
||||||
|
// is here
|
||||||
|
// Do no forget to call initialize method on providers
|
||||||
|
@Bean
|
||||||
|
@Qualifier("metadata")
|
||||||
|
public CachingMetadataManager metadata() throws MetadataProviderException, ResourceException {
|
||||||
|
List<MetadataProvider> providers = new ArrayList<>();
|
||||||
|
providers.add(oktaExtendedMetadataProvider());
|
||||||
|
CachingMetadataManager metadataManager = new CachingMetadataManager(providers);
|
||||||
|
metadataManager.setDefaultIDP(defaultIdp);
|
||||||
|
return metadataManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The filter is waiting for connections on URL suffixed with filterSuffix
|
||||||
|
// and presents SP metadata there
|
||||||
|
@Bean
|
||||||
|
public MetadataDisplayFilter metadataDisplayFilter() {
|
||||||
|
return new MetadataDisplayFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler deciding where to redirect user after successful login
|
||||||
|
@Bean
|
||||||
|
@Qualifier("saml")
|
||||||
|
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
|
||||||
|
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler =
|
||||||
|
new SavedRequestAwareAuthenticationSuccessHandler();
|
||||||
|
successRedirectHandler.setDefaultTargetUrl("/landing");
|
||||||
|
return successRedirectHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler deciding where to redirect user after failed login
|
||||||
|
@Bean
|
||||||
|
@Qualifier("saml")
|
||||||
|
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
|
||||||
|
SimpleUrlAuthenticationFailureHandler failureHandler =
|
||||||
|
new SimpleUrlAuthenticationFailureHandler();
|
||||||
|
failureHandler.setUseForward(true);
|
||||||
|
failureHandler.setDefaultFailureUrl("/error");
|
||||||
|
return failureHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler for successful logout
|
||||||
|
@Bean
|
||||||
|
public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
|
||||||
|
SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
|
||||||
|
successLogoutHandler.setDefaultTargetUrl("/");
|
||||||
|
return successLogoutHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout handler terminating local session
|
||||||
|
@Bean
|
||||||
|
public SecurityContextLogoutHandler logoutHandler() {
|
||||||
|
SecurityContextLogoutHandler logoutHandler =
|
||||||
|
new SecurityContextLogoutHandler();
|
||||||
|
logoutHandler.setInvalidateHttpSession(true);
|
||||||
|
logoutHandler.setClearAuthentication(true);
|
||||||
|
return logoutHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter processing incoming logout messages
|
||||||
|
// First argument determines URL user will be redirected to after successful
|
||||||
|
// global logout
|
||||||
|
@Bean
|
||||||
|
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
|
||||||
|
return new SAMLLogoutProcessingFilter(successLogoutHandler(),
|
||||||
|
logoutHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides default logout processing filter with the one processing SAML
|
||||||
|
// messages
|
||||||
|
@Bean
|
||||||
|
public SAMLLogoutFilter samlLogoutFilter() {
|
||||||
|
return new SAMLLogoutFilter(successLogoutHandler(),
|
||||||
|
new LogoutHandler[] { logoutHandler() },
|
||||||
|
new LogoutHandler[] { logoutHandler() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bindings
|
||||||
|
private ArtifactResolutionProfile artifactResolutionProfile() {
|
||||||
|
final ArtifactResolutionProfileImpl artifactResolutionProfile =
|
||||||
|
new ArtifactResolutionProfileImpl(httpClient());
|
||||||
|
artifactResolutionProfile.setProcessor(new SAMLProcessorImpl(soapBinding()));
|
||||||
|
return artifactResolutionProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HTTPArtifactBinding artifactBinding(ParserPool parserPool, VelocityEngine velocityEngine) {
|
||||||
|
return new HTTPArtifactBinding(parserPool, velocityEngine, artifactResolutionProfile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HTTPSOAP11Binding soapBinding() {
|
||||||
|
return new HTTPSOAP11Binding(parserPool());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HTTPPostBinding httpPostBinding() {
|
||||||
|
return new HTTPPostBinding(parserPool(), velocityEngine());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() {
|
||||||
|
return new HTTPRedirectDeflateBinding(parserPool());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HTTPSOAP11Binding httpSOAP11Binding() {
|
||||||
|
return new HTTPSOAP11Binding(parserPool());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HTTPPAOS11Binding httpPAOS11Binding() {
|
||||||
|
return new HTTPPAOS11Binding(parserPool());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processor
|
||||||
|
@Bean
|
||||||
|
public SAMLProcessorImpl processor() {
|
||||||
|
Collection<SAMLBinding> bindings = new ArrayList<>();
|
||||||
|
bindings.add(httpRedirectDeflateBinding());
|
||||||
|
bindings.add(httpPostBinding());
|
||||||
|
bindings.add(artifactBinding(parserPool(), velocityEngine()));
|
||||||
|
bindings.add(httpSOAP11Binding());
|
||||||
|
bindings.add(httpPAOS11Binding());
|
||||||
|
return new SAMLProcessorImpl(bindings);
|
||||||
|
}
|
||||||
|
}
|
@ -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', 'dbuser@dbauth.com', '$2y$12$0uXgosmcpIRWLxcAhCx0s.dUEZbrAHa0F0pkn6MO4Y57PCJ5fDjfa'),
|
||||||
|
('17e3d83c-6e09-41b8-b4ee-b4b14cb8a798', 'samluser@oktaauth.com', '$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>
|
Binary file not shown.
7
spring-security-modules/spring-security-saml/src/main/resources/static/css/bootstrap.min.css
vendored
Normal file
7
spring-security-modules/spring-security-saml/src/main/resources/static/css/bootstrap.min.css
vendored
Normal file
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;
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
7
spring-security-modules/spring-security-saml/src/main/resources/static/js/bootstrap.min.js
vendored
Normal file
7
spring-security-modules/spring-security-saml/src/main/resources/static/js/bootstrap.min.js
vendored
Normal file
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,32 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{layout}"
|
||||||
|
>
|
||||||
|
<body>
|
||||||
|
<!-- Content starts -->
|
||||||
|
<section layout:fragment="content">
|
||||||
|
<h6 class="border-bottom border-gray pb-2 mb-0">Select your Identity Provider:</h6>
|
||||||
|
<form th:action="${idpDiscoReturnURL}" method="get">
|
||||||
|
<fieldset class="form-group">
|
||||||
|
<div class="form-check" th:each="idp : ${idps}">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input type="radio" class="form-check-input" th:name="${idpDiscoReturnParam}" th:id="'idp_' + ${idp}" th:value="${idp}" />
|
||||||
|
<span class="badge badge-dark" th:text="${idp}">null</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<small class="d-block text-right mt-3" id="sso-btn">
|
||||||
|
<button type="submit" class="btn btn-spring btn-sm">
|
||||||
|
<i class="fas fa-handshake"></i> Start 3rd Party Login
|
||||||
|
</button>
|
||||||
|
</small>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
<!-- Content ends -->
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -0,0 +1,21 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{layout}"
|
||||||
|
>
|
||||||
|
<body>
|
||||||
|
<!-- Content starts -->
|
||||||
|
<section layout:fragment="content">
|
||||||
|
<h6 class="border-bottom border-gray pb-2 mb-0">Something went wrong</h6>
|
||||||
|
<div class="media text-muted pt-3">
|
||||||
|
<i class="fas fa-door-closed fa-2x fa-fw mr-2 spring-green" data-fa-transform="shrink-4"></i>
|
||||||
|
<p class="media-body pb-3 mb-0 small lh-125 border-bottom border-gray">
|
||||||
|
An error occurred
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Content ends -->
|
||||||
|
<body>
|
||||||
|
|
||||||
|
</html>
|
@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{layout}"
|
||||||
|
>
|
||||||
|
<body>
|
||||||
|
<section layout:fragment="content">
|
||||||
|
<h6 class="border-bottom border-gray pb-2 mb-0">Database Login:</h6>
|
||||||
|
<div class="media text-muted pt-3">
|
||||||
|
<form action="#" th:action="@{/form-login}" th:object="${credentials}" method="post">
|
||||||
|
<p>Username: <input type="text" th:field="*{username}" /></p>
|
||||||
|
<p>Password: <input type="password" th:field="*{password}" /></p>
|
||||||
|
<p><input type="submit" value="Submit" /></p>
|
||||||
|
</form>
|
||||||
|
<br/>
|
||||||
|
<p th:text="${error}" style="color: red"></p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<body>
|
||||||
|
</html>
|
@ -0,0 +1,21 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{layout}"
|
||||||
|
>
|
||||||
|
<body>
|
||||||
|
<section layout:fragment="content">
|
||||||
|
<h6 class="border-bottom border-gray pb-2 mb-0">Please Log In:</h6>
|
||||||
|
<div class="media text-muted pt-3">
|
||||||
|
<form action="#" th:action="@{/pre-auth}" th:object="${username}" method="post">
|
||||||
|
<p>Username: <input type="text" th:field="*{username}" /></p>
|
||||||
|
<p><input type="submit" value="Submit" /></p>
|
||||||
|
</form>
|
||||||
|
<br/>
|
||||||
|
<p th:text="${error}" style="color: red"></p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<body>
|
||||||
|
</html>
|
@ -0,0 +1,23 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{layout}"
|
||||||
|
xmlns="http://www.w3.org/1999/html">
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Content starts -->
|
||||||
|
<section layout:fragment="content">
|
||||||
|
<h3><Strong>Welcome!</strong><br/>You are successfully logged in!</h3>
|
||||||
|
<p>You are logged as <span class="badge badge-dark" th:text="${username}">null</span>.</p>
|
||||||
|
<small class="d-block text-right mt-3" id="sso-btn">
|
||||||
|
<a th:href="@{/logout}" class="btn btn-spring btn-sm">
|
||||||
|
<i class="far fa-user-circle"></i> Logout
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</section>
|
||||||
|
<!-- Content ends -->
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -0,0 +1,57 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="Spring Boot - SAML 2.0 Service Provider">
|
||||||
|
<meta name="author" content="Joe Cavazos">
|
||||||
|
<link rel="icon" th:href="@{/img/favicon.ico}">
|
||||||
|
<title>Spring Boot — SAML 2.0 Service Provider</title>
|
||||||
|
<!-- Bootstrap core CSS -->
|
||||||
|
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
|
||||||
|
<!-- Font Awesome CSS -->
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css">
|
||||||
|
<!-- Custom styles for this template -->
|
||||||
|
<link th:href="@{/css/site.css}" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-light">
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
|
||||||
|
<a class="navbar-brand" href="#">Spring Boot + SAML + Okta</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
|
||||||
|
<ul class="navbar-nav mr-auto">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/landing">Landing</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main role="main" class="container">
|
||||||
|
|
||||||
|
<div class="starter-template">
|
||||||
|
<section role="content" layout:fragment="content">
|
||||||
|
<!-- Page content goes here! -->
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main><!-- /.container -->
|
||||||
|
|
||||||
|
<!-- Main ends -->
|
||||||
|
<!-- Bootstrap scripts -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||||
|
<script th:src="@{/js/bootstrap.min.js}"></script>
|
||||||
|
<!-- Font Awesome scripts -->
|
||||||
|
<script defer src="https://use.fontawesome.com/releases/v5.0.13/js/all.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user