Spring Security with SAML and Okta
This commit is contained in:
parent
25e43334c4
commit
bc4ea42b7d
|
@ -35,6 +35,7 @@
|
||||||
<module>spring-security-legacy-oidc</module>
|
<module>spring-security-legacy-oidc</module>
|
||||||
<module>spring-security-oidc</module>
|
<module>spring-security-oidc</module>
|
||||||
<module>spring-security-okta</module>
|
<module>spring-security-okta</module>
|
||||||
|
<module>spring-security-saml</module>
|
||||||
<module>spring-security-web-react</module>
|
<module>spring-security-web-react</module>
|
||||||
<module>spring-security-web-rest</module>
|
<module>spring-security-web-rest</module>
|
||||||
<module>spring-security-web-rest-basic-auth</module>
|
<module>spring-security-web-rest-basic-auth</module>
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
### Relevant Articles:
|
|
||||||
|
|
||||||
- [Spring Security With Okta](https://www.baeldung.com/spring-security-okta)
|
|
|
@ -31,31 +31,14 @@
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>nz.net.ultraq.thymeleaf</groupId>
|
|
||||||
<artifactId>thymeleaf-layout-dialect</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.security.extensions</groupId>
|
<groupId>org.springframework.security.extensions</groupId>
|
||||||
<artifactId>spring-security-saml2-core</artifactId>
|
<artifactId>spring-security-saml2-core</artifactId>
|
||||||
<version>1.0.10.RELEASE</version>
|
<version>${saml2-core.spring.version}</version>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.h2database</groupId>
|
|
||||||
<artifactId>h2</artifactId>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
@ -85,6 +68,6 @@
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<saml.spring.version>1.0.10.RELEASE</saml.spring.version>
|
<saml2-core.spring.version>1.0.10.RELEASE</saml2-core.spring.version>
|
||||||
</properties>
|
</properties>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -3,9 +3,6 @@ package com.baeldung.saml;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
/**
|
|
||||||
* @author jcavazos
|
|
||||||
*/
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class Application {
|
public class Application {
|
||||||
public static void main(String... args) {
|
public static void main(String... args) {
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
package com.baeldung.saml;
|
|
||||||
|
|
||||||
public class Constants {
|
|
||||||
public static final String OKTA_USERNAME_SUFFIX = "@oktaauth.com";
|
|
||||||
public static final String DB_USERNAME_SUFFIX = "@dbauth.com";
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package com.baeldung.saml.auth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author jcavazos
|
|
||||||
*/
|
|
||||||
public enum AuthMethod {
|
|
||||||
DATABASE,
|
|
||||||
SAML
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
package com.baeldung.saml.auth;
|
|
||||||
|
|
||||||
import com.baeldung.saml.repository.UserRepository;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|
||||||
import org.springframework.security.saml.SAMLCredential;
|
|
||||||
import org.springframework.security.saml.userdetails.SAMLUserDetailsService;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Supplies {@link UserDetails} information for both DB and SAML users
|
|
||||||
* @author jcavazos
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
public class CombinedUserDetailsService implements UserDetailsService, SAMLUserDetailsService {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(CombinedUserDetailsService.class);
|
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public CombinedUserDetailsService(UserRepository userRepository) {
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
|
|
||||||
StoredUser storedUser = lookupUser(s);
|
|
||||||
return new CustomUserDetails(
|
|
||||||
AuthMethod.DATABASE,
|
|
||||||
storedUser.getUsername(),
|
|
||||||
storedUser.getPasswordHash(),
|
|
||||||
new LinkedList<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException {
|
|
||||||
LOGGER.info("Loading UserDetails by SAMLCredentials: {}", credential.getNameID());
|
|
||||||
StoredUser storedUser = lookupUser(credential.getNameID().getValue());
|
|
||||||
return new CustomUserDetails(
|
|
||||||
AuthMethod.SAML,
|
|
||||||
storedUser.getUsername(),
|
|
||||||
storedUser.getPasswordHash(),
|
|
||||||
new LinkedList<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private StoredUser lookupUser(String username) {
|
|
||||||
LOGGER.info("Loading UserDetails by username: {}", username);
|
|
||||||
|
|
||||||
Optional<StoredUser> user = userRepository.findByUsernameIgnoreCase(username);
|
|
||||||
|
|
||||||
if (!user.isPresent()) {
|
|
||||||
LOGGER.error("User not found in database: {}", user);
|
|
||||||
throw new UsernameNotFoundException(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.get();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package com.baeldung.saml.auth;
|
|
||||||
|
|
||||||
import java.lang.annotation.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the current authenticated user during HTTP requests
|
|
||||||
*/
|
|
||||||
@Target(ElementType.PARAMETER)
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
public @interface CurrentUser {
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
package com.baeldung.saml.auth;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
import org.springframework.security.core.userdetails.User;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public class CustomUserDetails extends User {
|
|
||||||
|
|
||||||
private final AuthMethod authMethod;
|
|
||||||
|
|
||||||
public CustomUserDetails(AuthMethod authMethod,
|
|
||||||
String username,
|
|
||||||
String password, Collection<? extends GrantedAuthority> authorities) {
|
|
||||||
super(username, password, authorities);
|
|
||||||
this.authMethod = authMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CustomUserDetails(AuthMethod authMethod,
|
|
||||||
String username,
|
|
||||||
String password,
|
|
||||||
boolean enabled,
|
|
||||||
boolean accountNonExpired,
|
|
||||||
boolean credentialsNonExpired,
|
|
||||||
boolean accountNonLocked,
|
|
||||||
Collection<? extends GrantedAuthority> authorities) {
|
|
||||||
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
|
|
||||||
this.authMethod = authMethod;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
package com.baeldung.saml.auth;
|
|
||||||
|
|
||||||
import com.baeldung.saml.Constants;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.AuthenticationException;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides authentication for "database" users, i.e. users whose credentials
|
|
||||||
* are authenticated against the database instead of Okta/SAML
|
|
||||||
* @author jcavazos
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class DbAuthProvider implements AuthenticationProvider {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(DbAuthProvider.class);
|
|
||||||
|
|
||||||
private final CombinedUserDetailsService combinedUserDetailsService;
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public DbAuthProvider(CombinedUserDetailsService combinedUserDetailsService, PasswordEncoder passwordEncoder) {
|
|
||||||
this.combinedUserDetailsService = combinedUserDetailsService;
|
|
||||||
this.passwordEncoder = passwordEncoder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
|
||||||
if (!StringUtils.endsWithIgnoreCase(authentication.getPrincipal().toString(), Constants.DB_USERNAME_SUFFIX)) {
|
|
||||||
// this user is not supported by DB authentication
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserDetails user = combinedUserDetailsService.loadUserByUsername(authentication.getPrincipal().toString());
|
|
||||||
String rawPw = authentication.getCredentials() == null ? null : authentication.getCredentials().toString();
|
|
||||||
|
|
||||||
if (passwordEncoder.matches(rawPw, user.getPassword())) {
|
|
||||||
LOGGER.warn("User successfully logged in: {}", user.getUsername());
|
|
||||||
return new UsernamePasswordAuthenticationToken(
|
|
||||||
user.getUsername(),
|
|
||||||
rawPw,
|
|
||||||
Collections.emptyList());
|
|
||||||
} else {
|
|
||||||
LOGGER.error("User failed to log in: {}", user.getUsername());
|
|
||||||
throw new BadCredentialsException("Bad password");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supports(Class<?> aClass) {
|
|
||||||
return aClass.isAssignableFrom(UsernamePasswordAuthenticationToken.class);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
package com.baeldung.saml.auth;
|
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
import javax.persistence.Column;
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import javax.persistence.Table;
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "USER")
|
|
||||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
|
||||||
public class StoredUser implements Serializable {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@EqualsAndHashCode.Include
|
|
||||||
private String id = UUID.randomUUID().toString();
|
|
||||||
|
|
||||||
@Column(nullable = false, unique = true)
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String passwordHash;
|
|
||||||
|
|
||||||
public String getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(String id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUsername() {
|
|
||||||
return username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUsername(String username) {
|
|
||||||
this.username = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPasswordHash() {
|
|
||||||
return passwordHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPasswordHash(String passwordHash) {
|
|
||||||
this.passwordHash = passwordHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.baeldung.saml.authentication;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.providers.ExpiringUsernameAuthenticationToken;
|
||||||
|
import org.springframework.security.saml.SAMLAuthenticationProvider;
|
||||||
|
import org.springframework.security.saml.SAMLCredential;
|
||||||
|
|
||||||
|
public class CustomSAMLAuthenticationProvider extends SAMLAuthenticationProvider {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<? extends GrantedAuthority> getEntitlements(SAMLCredential credential, Object userDetail) {
|
||||||
|
|
||||||
|
if(userDetail instanceof ExpiringUsernameAuthenticationToken) {
|
||||||
|
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
|
||||||
|
authorities.addAll(((ExpiringUsernameAuthenticationToken) userDetail).getAuthorities());
|
||||||
|
return authorities;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
package com.baeldung.saml.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author jcavazos
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
public class DbSecurityConfig {
|
|
||||||
@Bean
|
|
||||||
public PasswordEncoder passwordEncoder() {
|
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package com.baeldung.saml.config;
|
|
||||||
|
|
||||||
import com.baeldung.saml.web.CurrentUserHandlerMethodArgumentResolver;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class MvcConfig implements WebMvcConfigurer {
|
|
||||||
|
|
||||||
final CurrentUserHandlerMethodArgumentResolver currentUserHandlerMethodArgumentResolver;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public MvcConfig(CurrentUserHandlerMethodArgumentResolver currentUserHandlerMethodArgumentResolver) {
|
|
||||||
this.currentUserHandlerMethodArgumentResolver = currentUserHandlerMethodArgumentResolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addViewControllers(ViewControllerRegistry registry) {
|
|
||||||
registry.addViewController("/").setViewName("index");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
|
||||||
if (!registry.hasMappingForPattern("/static/**")) {
|
|
||||||
registry.addResourceHandler("/static/**")
|
|
||||||
.addResourceLocations("/static/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
|
|
||||||
argumentResolvers.add(currentUserHandlerMethodArgumentResolver);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,16 @@
|
||||||
package com.baeldung.saml.config;
|
package com.baeldung.saml.config;
|
||||||
|
|
||||||
import com.baeldung.saml.auth.CombinedUserDetailsService;
|
import java.io.File;
|
||||||
import org.apache.commons.httpclient.HttpClient;
|
import java.util.ArrayList;
|
||||||
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
|
import java.util.HashMap;
|
||||||
import org.apache.velocity.app.VelocityEngine;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider;
|
import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider;
|
||||||
import org.opensaml.saml2.metadata.provider.MetadataProvider;
|
import org.opensaml.saml2.metadata.provider.MetadataProvider;
|
||||||
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
|
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
|
||||||
import org.opensaml.util.resource.ResourceException;
|
import org.opensaml.util.resource.ResourceException;
|
||||||
import org.opensaml.xml.parse.ParserPool;
|
|
||||||
import org.opensaml.xml.parse.StaticBasicParserPool;
|
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.Qualifier;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
@ -25,8 +25,6 @@ import org.springframework.security.saml.log.SAMLDefaultLogger;
|
||||||
import org.springframework.security.saml.metadata.CachingMetadataManager;
|
import org.springframework.security.saml.metadata.CachingMetadataManager;
|
||||||
import org.springframework.security.saml.metadata.ExtendedMetadata;
|
import org.springframework.security.saml.metadata.ExtendedMetadata;
|
||||||
import org.springframework.security.saml.metadata.ExtendedMetadataDelegate;
|
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.processor.*;
|
||||||
import org.springframework.security.saml.util.VelocityFactory;
|
import org.springframework.security.saml.util.VelocityFactory;
|
||||||
import org.springframework.security.saml.websso.*;
|
import org.springframework.security.saml.websso.*;
|
||||||
|
@ -36,12 +34,8 @@ import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
import java.io.File;
|
import com.baeldung.saml.authentication.CustomSAMLAuthenticationProvider;
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure settings and information required for SAML 2.0 authentication with Okta
|
|
||||||
*/
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SamlSecurityConfig {
|
public class SamlSecurityConfig {
|
||||||
|
|
||||||
|
@ -57,105 +51,52 @@ public class SamlSecurityConfig {
|
||||||
@Value("${saml.idp}")
|
@Value("${saml.idp}")
|
||||||
private String defaultIdp;
|
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")
|
@Bean(initMethod = "initialize")
|
||||||
public StaticBasicParserPool parserPool() {
|
public StaticBasicParserPool parserPool() {
|
||||||
return new StaticBasicParserPool();
|
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
|
@Bean
|
||||||
public SAMLAuthenticationProvider samlAuthenticationProvider() {
|
public SAMLAuthenticationProvider samlAuthenticationProvider() {
|
||||||
SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
|
return new CustomSAMLAuthenticationProvider();
|
||||||
samlAuthenticationProvider.setUserDetails(combinedUserDetailsService);
|
|
||||||
samlAuthenticationProvider.setForcePrincipalAsString(false);
|
|
||||||
return samlAuthenticationProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider of default SAML Context
|
|
||||||
@Bean
|
@Bean
|
||||||
public SAMLContextProviderImpl contextProvider() {
|
public SAMLContextProviderImpl contextProvider() {
|
||||||
return new SAMLContextProviderImpl();
|
return new SAMLContextProviderImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialization of OpenSAML library
|
|
||||||
@Bean
|
@Bean
|
||||||
public static SAMLBootstrap sAMLBootstrap() {
|
public static SAMLBootstrap samlBootstrap() {
|
||||||
return new SAMLBootstrap();
|
return new SAMLBootstrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger for SAML messages and events
|
|
||||||
@Bean
|
@Bean
|
||||||
public SAMLDefaultLogger samlLogger() {
|
public SAMLDefaultLogger samlLogger() {
|
||||||
return new SAMLDefaultLogger();
|
return new SAMLDefaultLogger();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAML 2.0 WebSSO Assertion Consumer
|
|
||||||
@Bean
|
@Bean
|
||||||
public WebSSOProfileConsumer webSSOprofileConsumer() {
|
public WebSSOProfileConsumer webSSOprofileConsumer() {
|
||||||
return new WebSSOProfileConsumerImpl();
|
return new WebSSOProfileConsumerImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAML 2.0 Holder-of-Key WebSSO Assertion Consumer
|
|
||||||
@Bean
|
@Bean
|
||||||
@Qualifier("hokWebSSOprofileConsumer")
|
@Qualifier("hokWebSSOprofileConsumer")
|
||||||
public WebSSOProfileConsumerHoKImpl hokWebSSOProfileConsumer() {
|
public WebSSOProfileConsumerHoKImpl hokWebSSOProfileConsumer() {
|
||||||
return new WebSSOProfileConsumerHoKImpl();
|
return new WebSSOProfileConsumerHoKImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAML 2.0 Web SSO profile
|
|
||||||
@Bean
|
@Bean
|
||||||
public WebSSOProfile webSSOprofile() {
|
public WebSSOProfile webSSOprofile() {
|
||||||
return new WebSSOProfileImpl();
|
return new WebSSOProfileImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAML 2.0 Holder-of-Key Web SSO profile
|
|
||||||
@Bean
|
@Bean
|
||||||
public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
|
public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
|
||||||
return new WebSSOProfileConsumerHoKImpl();
|
return new WebSSOProfileConsumerHoKImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAML 2.0 ECP profile
|
|
||||||
@Bean
|
@Bean
|
||||||
public WebSSOProfileECPImpl ecpProfile() {
|
public WebSSOProfileECPImpl ecpProfile() {
|
||||||
return new WebSSOProfileECPImpl();
|
return new WebSSOProfileECPImpl();
|
||||||
|
@ -166,7 +107,6 @@ public class SamlSecurityConfig {
|
||||||
return new SingleLogoutProfileImpl();
|
return new SingleLogoutProfileImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Central storage of cryptographic keys
|
|
||||||
@Bean
|
@Bean
|
||||||
public KeyManager keyManager() {
|
public KeyManager keyManager() {
|
||||||
DefaultResourceLoader loader = new DefaultResourceLoader();
|
DefaultResourceLoader loader = new DefaultResourceLoader();
|
||||||
|
@ -183,8 +123,6 @@ public class SamlSecurityConfig {
|
||||||
return webSSOProfileOptions;
|
return webSSOProfileOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry point to initialize authentication, default values taken from
|
|
||||||
// properties file
|
|
||||||
@Bean
|
@Bean
|
||||||
public SAMLEntryPoint samlEntryPoint() {
|
public SAMLEntryPoint samlEntryPoint() {
|
||||||
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
|
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
|
||||||
|
@ -192,43 +130,34 @@ public class SamlSecurityConfig {
|
||||||
return samlEntryPoint;
|
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
|
@Bean
|
||||||
public SAMLDiscovery samlIDPDiscovery() {
|
public SAMLDiscovery samlIDPDiscovery() {
|
||||||
SAMLDiscovery idpDiscovery = new SAMLDiscovery();
|
SAMLDiscovery idpDiscovery = new SAMLDiscovery();
|
||||||
idpDiscovery.setIdpSelectionPath("/saml/discovery");
|
|
||||||
return idpDiscovery;
|
return idpDiscovery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ExtendedMetadata extendedMetadata() {
|
||||||
|
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
|
||||||
|
extendedMetadata.setIdpDiscoveryEnabled(false);
|
||||||
|
extendedMetadata.setSignMetadata(false);
|
||||||
|
return extendedMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Qualifier("okta")
|
@Qualifier("okta")
|
||||||
public ExtendedMetadataDelegate oktaExtendedMetadataProvider() throws MetadataProviderException {
|
public ExtendedMetadataDelegate oktaExtendedMetadataProvider() throws MetadataProviderException {
|
||||||
File metadata = null;
|
File metadata = null;
|
||||||
try {
|
try {
|
||||||
metadata = new File("/Users/anshulbansal/eclipse-workspace/tutorials/spring-security-modules/spring-security-saml/src/main/resources/saml/metadata/sso.xml");
|
metadata = new File("./src/main/resources/saml/metadata/sso.xml");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
FilesystemMetadataProvider provider = null;
|
FilesystemMetadataProvider provider = new FilesystemMetadataProvider(metadata);
|
||||||
provider = new FilesystemMetadataProvider(metadata);
|
|
||||||
provider.setParserPool(parserPool());
|
provider.setParserPool(parserPool());
|
||||||
return new ExtendedMetadataDelegate(provider, extendedMetadata());
|
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
|
@Bean
|
||||||
@Qualifier("metadata")
|
@Qualifier("metadata")
|
||||||
public CachingMetadataManager metadata() throws MetadataProviderException, ResourceException {
|
public CachingMetadataManager metadata() throws MetadataProviderException, ResourceException {
|
||||||
|
@ -239,35 +168,23 @@ public class SamlSecurityConfig {
|
||||||
return metadataManager;
|
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
|
@Bean
|
||||||
@Qualifier("saml")
|
@Qualifier("saml")
|
||||||
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
|
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
|
||||||
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler =
|
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler = new SavedRequestAwareAuthenticationSuccessHandler();
|
||||||
new SavedRequestAwareAuthenticationSuccessHandler();
|
successRedirectHandler.setDefaultTargetUrl("/home");
|
||||||
successRedirectHandler.setDefaultTargetUrl("/landing");
|
|
||||||
return successRedirectHandler;
|
return successRedirectHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler deciding where to redirect user after failed login
|
|
||||||
@Bean
|
@Bean
|
||||||
@Qualifier("saml")
|
@Qualifier("saml")
|
||||||
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
|
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
|
||||||
SimpleUrlAuthenticationFailureHandler failureHandler =
|
SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
|
||||||
new SimpleUrlAuthenticationFailureHandler();
|
|
||||||
failureHandler.setUseForward(true);
|
failureHandler.setUseForward(true);
|
||||||
failureHandler.setDefaultFailureUrl("/error");
|
failureHandler.setDefaultFailureUrl("/error");
|
||||||
return failureHandler;
|
return failureHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler for successful logout
|
|
||||||
@Bean
|
@Bean
|
||||||
public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
|
public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
|
||||||
SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
|
SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
|
||||||
|
@ -275,27 +192,19 @@ public class SamlSecurityConfig {
|
||||||
return successLogoutHandler;
|
return successLogoutHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout handler terminating local session
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityContextLogoutHandler logoutHandler() {
|
public SecurityContextLogoutHandler logoutHandler() {
|
||||||
SecurityContextLogoutHandler logoutHandler =
|
SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
|
||||||
new SecurityContextLogoutHandler();
|
|
||||||
logoutHandler.setInvalidateHttpSession(true);
|
logoutHandler.setInvalidateHttpSession(true);
|
||||||
logoutHandler.setClearAuthentication(true);
|
logoutHandler.setClearAuthentication(true);
|
||||||
return logoutHandler;
|
return logoutHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter processing incoming logout messages
|
|
||||||
// First argument determines URL user will be redirected to after successful
|
|
||||||
// global logout
|
|
||||||
@Bean
|
@Bean
|
||||||
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
|
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
|
||||||
return new SAMLLogoutProcessingFilter(successLogoutHandler(),
|
return new SAMLLogoutProcessingFilter(successLogoutHandler(), logoutHandler());
|
||||||
logoutHandler());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overrides default logout processing filter with the one processing SAML
|
|
||||||
// messages
|
|
||||||
@Bean
|
@Bean
|
||||||
public SAMLLogoutFilter samlLogoutFilter() {
|
public SAMLLogoutFilter samlLogoutFilter() {
|
||||||
return new SAMLLogoutFilter(successLogoutHandler(),
|
return new SAMLLogoutFilter(successLogoutHandler(),
|
||||||
|
@ -303,27 +212,9 @@ public class SamlSecurityConfig {
|
||||||
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
|
@Bean
|
||||||
public HTTPPostBinding httpPostBinding() {
|
public HTTPPostBinding httpPostBinding() {
|
||||||
return new HTTPPostBinding(parserPool(), velocityEngine());
|
return new HTTPPostBinding(parserPool(), VelocityFactory.getEngine());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -331,25 +222,11 @@ public class SamlSecurityConfig {
|
||||||
return new HTTPRedirectDeflateBinding(parserPool());
|
return new HTTPRedirectDeflateBinding(parserPool());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public HTTPSOAP11Binding httpSOAP11Binding() {
|
|
||||||
return new HTTPSOAP11Binding(parserPool());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public HTTPPAOS11Binding httpPAOS11Binding() {
|
|
||||||
return new HTTPPAOS11Binding(parserPool());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Processor
|
|
||||||
@Bean
|
@Bean
|
||||||
public SAMLProcessorImpl processor() {
|
public SAMLProcessorImpl processor() {
|
||||||
Collection<SAMLBinding> bindings = new ArrayList<>();
|
ArrayList<SAMLBinding> bindings = new ArrayList<>();
|
||||||
bindings.add(httpRedirectDeflateBinding());
|
bindings.add(httpRedirectDeflateBinding());
|
||||||
bindings.add(httpPostBinding());
|
bindings.add(httpPostBinding());
|
||||||
bindings.add(artifactBinding(parserPool(), velocityEngine()));
|
|
||||||
bindings.add(httpSOAP11Binding());
|
|
||||||
bindings.add(httpPAOS11Binding());
|
|
||||||
return new SAMLProcessorImpl(bindings);
|
return new SAMLProcessorImpl(bindings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
package com.baeldung.saml.config;
|
package com.baeldung.saml.config;
|
||||||
|
|
||||||
import com.baeldung.saml.auth.AuthMethod;
|
import java.io.IOException;
|
||||||
import com.baeldung.saml.auth.CustomUserDetails;
|
import java.util.ArrayList;
|
||||||
import com.baeldung.saml.auth.DbAuthProvider;
|
import java.util.List;
|
||||||
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.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
@ -18,8 +15,6 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
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.*;
|
||||||
import org.springframework.security.saml.key.KeyManager;
|
import org.springframework.security.saml.key.KeyManager;
|
||||||
import org.springframework.security.saml.metadata.*;
|
import org.springframework.security.saml.metadata.*;
|
||||||
|
@ -33,19 +28,10 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
|
||||||
import org.springframework.security.web.csrf.CsrfFilter;
|
import org.springframework.security.web.csrf.CsrfFilter;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
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
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@EnableGlobalMethodSecurity(securedEnabled = true)
|
@EnableGlobalMethodSecurity(securedEnabled = true)
|
||||||
public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements DisposableBean {
|
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(WebSecurityConfig.class);
|
|
||||||
|
|
||||||
@Value("${saml.sp}")
|
@Value("${saml.sp}")
|
||||||
private String samlAudience;
|
private String samlAudience;
|
||||||
|
@ -58,23 +44,12 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements D
|
||||||
@Qualifier("saml")
|
@Qualifier("saml")
|
||||||
private SimpleUrlAuthenticationFailureHandler samlAuthFailureHandler;
|
private SimpleUrlAuthenticationFailureHandler samlAuthFailureHandler;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("saml")
|
|
||||||
private Timer samlBackgroundTaskTimer;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("saml")
|
|
||||||
private MultiThreadedHttpConnectionManager samlMultiThreadedHttpConnectionManager;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private SAMLEntryPoint samlEntryPoint;
|
private SAMLEntryPoint samlEntryPoint;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private SAMLLogoutFilter samlLogoutFilter;
|
private SAMLLogoutFilter samlLogoutFilter;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MetadataDisplayFilter metadataDisplayFilter;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private SAMLLogoutProcessingFilter samlLogoutProcessingFilter;
|
private SAMLLogoutProcessingFilter samlLogoutProcessingFilter;
|
||||||
|
|
||||||
|
@ -87,15 +62,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements D
|
||||||
@Autowired
|
@Autowired
|
||||||
private ExtendedMetadata extendedMetadata;
|
private ExtendedMetadata extendedMetadata;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private CachingMetadataManager cachingMetadataManager;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private KeyManager keyManager;
|
private KeyManager keyManager;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private DbAuthProvider dbAuthProvider;
|
|
||||||
|
|
||||||
public MetadataGenerator metadataGenerator() {
|
public MetadataGenerator metadataGenerator() {
|
||||||
MetadataGenerator metadataGenerator = new MetadataGenerator();
|
MetadataGenerator metadataGenerator = new MetadataGenerator();
|
||||||
metadataGenerator.setEntityId(samlAudience);
|
metadataGenerator.setEntityId(samlAudience);
|
||||||
|
@ -105,16 +74,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements D
|
||||||
return metadataGenerator;
|
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
|
@Bean
|
||||||
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
|
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
|
||||||
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
|
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
|
||||||
|
@ -124,36 +83,22 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements D
|
||||||
return samlWebSSOProcessingFilter;
|
return samlWebSSOProcessingFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Define the security filter chain in order to support SSO Auth by using SAML 2.0
|
|
||||||
*
|
|
||||||
* @return Filter chain proxy
|
|
||||||
*/
|
|
||||||
@Bean
|
@Bean
|
||||||
public FilterChainProxy samlFilter() throws Exception {
|
public FilterChainProxy samlFilter() throws Exception {
|
||||||
List<SecurityFilterChain> chains = new ArrayList<>();
|
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/**"),
|
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
|
||||||
samlWebSSOProcessingFilter()));
|
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/**"),
|
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"),
|
||||||
samlDiscovery));
|
samlDiscovery));
|
||||||
|
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
|
||||||
|
samlEntryPoint));
|
||||||
|
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
|
||||||
|
samlLogoutFilter));
|
||||||
|
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
|
||||||
|
samlLogoutProcessingFilter));
|
||||||
return new FilterChainProxy(chains);
|
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
|
@Bean
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationManager authenticationManagerBean() throws Exception {
|
public AuthenticationManager authenticationManagerBean() throws Exception {
|
||||||
|
@ -165,90 +110,40 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements D
|
||||||
return new MetadataGeneratorFilter(metadataGenerator());
|
return new MetadataGeneratorFilter(metadataGenerator());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the web based security configuration.
|
|
||||||
*
|
|
||||||
* @param http Allows configuring web based security for specific http requests.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(HttpSecurity http) throws Exception {
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
.csrf()
|
.csrf()
|
||||||
.disable();
|
.disable();
|
||||||
|
|
||||||
http
|
http
|
||||||
.httpBasic()
|
.httpBasic()
|
||||||
.authenticationEntryPoint((request, response, authException) -> {
|
.authenticationEntryPoint(samlEntryPoint);
|
||||||
/*
|
|
||||||
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
|
http
|
||||||
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
|
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
|
||||||
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
|
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
|
||||||
.addFilterBefore(samlFilter(), CsrfFilter.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
|
http
|
||||||
.logout()
|
.authorizeRequests()
|
||||||
.addLogoutHandler((request, response, authentication) -> {
|
.antMatchers("/").permitAll()
|
||||||
/*
|
.anyRequest().authenticated();
|
||||||
If the user is authenticated via SAML, we need to direct them to the appropriate
|
|
||||||
SAML logout flow
|
http
|
||||||
*/
|
.logout()
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
.addLogoutHandler((request, response, authentication) -> {
|
||||||
if (auth instanceof CustomUserDetails) {
|
try {
|
||||||
CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
|
response.sendRedirect("/saml/logout");
|
||||||
if (userDetails.getAuthMethod() == AuthMethod.SAML) {
|
} catch (IOException e) {
|
||||||
try {
|
e.printStackTrace();
|
||||||
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
|
@Override
|
||||||
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||||
auth.authenticationProvider(dbAuthProvider);
|
|
||||||
auth.authenticationProvider(samlAuthenticationProvider);
|
auth.authenticationProvider(samlAuthenticationProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy() throws Exception {
|
|
||||||
this.samlBackgroundTaskTimer.purge();
|
|
||||||
this.samlBackgroundTaskTimer.cancel();
|
|
||||||
this.samlMultiThreadedHttpConnectionManager.shutdown();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
package com.baeldung.saml.controller;
|
|
||||||
|
|
||||||
import com.baeldung.saml.controller.model.DbAuthCredentials;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles standard form-based logins used to authenticate users against the database.
|
|
||||||
* @author jcavazos
|
|
||||||
*/
|
|
||||||
@Controller
|
|
||||||
public class DbLoginController {
|
|
||||||
|
|
||||||
private final AuthenticationManager authenticationManager;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public DbLoginController(AuthenticationManager authenticationManager) {
|
|
||||||
this.authenticationManager = authenticationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/form-login")
|
|
||||||
public String formLogin(@RequestParam(required = false) String username, Model model) {
|
|
||||||
DbAuthCredentials credentials = new DbAuthCredentials();
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(username)) {
|
|
||||||
credentials.setUsername(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
model.addAttribute("credentials", credentials);
|
|
||||||
return "form-login";
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/form-login")
|
|
||||||
public String doLogin(@ModelAttribute DbAuthCredentials credentials,
|
|
||||||
RedirectAttributes redirectAttributes) {
|
|
||||||
try {
|
|
||||||
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
|
|
||||||
credentials.getUsername(), credentials.getPassword()));
|
|
||||||
|
|
||||||
if (authentication.isAuthenticated()) {
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
||||||
} else {
|
|
||||||
throw new Exception("Unauthenticated");
|
|
||||||
}
|
|
||||||
|
|
||||||
return "redirect:/landing";
|
|
||||||
} catch (Exception e) {
|
|
||||||
redirectAttributes.addFlashAttribute("error", "Login Failed");
|
|
||||||
return "redirect:/form-login?username="+credentials.getUsername();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.baeldung.saml.controller;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class HomeController {
|
||||||
|
|
||||||
|
@RequestMapping("/")
|
||||||
|
public String index() {
|
||||||
|
return "index";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/auth")
|
||||||
|
public String handleSamlAuth() {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (auth != null) {
|
||||||
|
return "redirect:/home";
|
||||||
|
} else {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping("/home")
|
||||||
|
public String home(Model model) {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
model.addAttribute("username", authentication.getPrincipal());
|
||||||
|
return "home";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,47 +0,0 @@
|
||||||
package com.baeldung.saml.controller;
|
|
||||||
|
|
||||||
import com.baeldung.saml.Constants;
|
|
||||||
import com.baeldung.saml.controller.model.PreAuthUsername;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Pre-auth" landing page in which the user will enter their username. Depending on
|
|
||||||
* the username suffix, the user will either be directed to provide a password for
|
|
||||||
* database authentication, or the SAML auth flow will be initiated.
|
|
||||||
*
|
|
||||||
* The redirect to /doSaml will be picked up by the custom auth entry point defined
|
|
||||||
* in {@link com.baeldung.saml.config.WebSecurityConfig}.
|
|
||||||
*
|
|
||||||
* @author jcavazos
|
|
||||||
*/
|
|
||||||
@Controller
|
|
||||||
public class IndexController {
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
public String index(Model model) {
|
|
||||||
model.addAttribute("username", new PreAuthUsername());
|
|
||||||
return "index";
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/pre-auth")
|
|
||||||
public String preAuth(@ModelAttribute PreAuthUsername username,
|
|
||||||
Model model,
|
|
||||||
RedirectAttributes redirectAttributes) {
|
|
||||||
if (StringUtils.endsWithIgnoreCase(username.getUsername(), Constants.OKTA_USERNAME_SUFFIX)) {
|
|
||||||
// redirect to SAML
|
|
||||||
return "redirect:/doSaml";
|
|
||||||
} else if (StringUtils.endsWithIgnoreCase(username.getUsername(), Constants.DB_USERNAME_SUFFIX)) {
|
|
||||||
// redirect to DB/form login
|
|
||||||
return "redirect:/form-login?username="+username.getUsername();
|
|
||||||
} else {
|
|
||||||
redirectAttributes.addFlashAttribute("error", "Invalid Username");
|
|
||||||
return "redirect:/";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package com.baeldung.saml.controller;
|
|
||||||
|
|
||||||
import com.baeldung.saml.auth.CurrentUser;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.security.core.userdetails.User;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This page should be guarded by the app's combined authentication. Thus, only users who
|
|
||||||
* have successfully authenticated via DB or SAML auth should have access
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Controller
|
|
||||||
public class LandingController {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(LandingController.class);
|
|
||||||
|
|
||||||
@RequestMapping("/landing")
|
|
||||||
public String landing(@CurrentUser User user, Model model) {
|
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
LOGGER.info("Current auth: {}", authentication==null?"NULL":authentication.getPrincipal());
|
|
||||||
model.addAttribute("username", user.getUsername());
|
|
||||||
return "landing";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
package com.baeldung.saml.controller;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.security.saml.metadata.MetadataManager;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page to select which Identity Provider (IDP) to authenticate against when beginning SAML auth flow.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Controller
|
|
||||||
@RequestMapping("/saml")
|
|
||||||
public class SSOController {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(SSOController.class);
|
|
||||||
|
|
||||||
private final MetadataManager metadata;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public SSOController(MetadataManager metadata) {
|
|
||||||
this.metadata = metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequestMapping(value = "/discovery", method = RequestMethod.GET)
|
|
||||||
public String idpSelection(Model model) {
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
LOGGER.info("Current auth: {}", auth==null?"NULL":auth.getPrincipal());
|
|
||||||
if (auth == null || (auth instanceof AnonymousAuthenticationToken)) {
|
|
||||||
Set<String> idps = metadata.getIDPEntityNames();
|
|
||||||
idps.forEach(idp -> LOGGER.info("Configured Identity Provider for SSO: {}", idp));
|
|
||||||
model.addAttribute("idps", idps);
|
|
||||||
return "discovery";
|
|
||||||
} else {
|
|
||||||
LOGGER.warn("The current user is already logged in: {}", auth);
|
|
||||||
return "redirect:/landing";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
package com.baeldung.saml.controller;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The SAML auth workflow is initiated by redirecting the request to the /doSaml
|
|
||||||
* endpoint. When the SAML flow is complete, the browser will be directed back to /doSaml.
|
|
||||||
*
|
|
||||||
* This class handles the result of that flow. If the user is successfully authenticated,
|
|
||||||
* they should be directed to the guarded landing page.
|
|
||||||
*
|
|
||||||
* @see IndexController
|
|
||||||
* @see com.baeldung.saml.config.WebSecurityConfig
|
|
||||||
*
|
|
||||||
* @author jcavazos
|
|
||||||
*/
|
|
||||||
@Controller
|
|
||||||
public class SamlResponseController {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(SamlResponseController.class);
|
|
||||||
|
|
||||||
@GetMapping(value = "/doSaml")
|
|
||||||
public String handleSamlAuth() {
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
LOGGER.info("doSaml auth result: {}", auth);
|
|
||||||
if (auth != null) {
|
|
||||||
return "redirect:/landing";
|
|
||||||
} else {
|
|
||||||
return "/";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package com.baeldung.saml.controller.model;
|
|
||||||
|
|
||||||
public class DbAuthCredentials {
|
|
||||||
private String username;
|
|
||||||
private String password;
|
|
||||||
public String getUsername() {
|
|
||||||
return username;
|
|
||||||
}
|
|
||||||
public void setUsername(String username) {
|
|
||||||
this.username = username;
|
|
||||||
}
|
|
||||||
public String getPassword() {
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
public void setPassword(String password) {
|
|
||||||
this.password = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
package com.baeldung.saml.controller.model;
|
|
||||||
|
|
||||||
public class PreAuthUsername {
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
public String getUsername() {
|
|
||||||
return username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUsername(String username) {
|
|
||||||
this.username = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package com.baeldung.saml.repository;
|
|
||||||
|
|
||||||
import com.baeldung.saml.auth.StoredUser;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface UserRepository extends JpaRepository<StoredUser, String> {
|
|
||||||
Optional<StoredUser> findByUsernameIgnoreCase(String username);
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package com.baeldung.saml.web;
|
|
||||||
|
|
||||||
import com.baeldung.saml.auth.CombinedUserDetailsService;
|
|
||||||
import com.baeldung.saml.auth.CurrentUser;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.core.MethodParameter;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.userdetails.User;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.bind.support.WebArgumentResolver;
|
|
||||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
|
||||||
import org.springframework.web.context.request.NativeWebRequest;
|
|
||||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
|
||||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class CurrentUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
|
||||||
|
|
||||||
private final CombinedUserDetailsService combinedUserDetailsService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public CurrentUserHandlerMethodArgumentResolver(CombinedUserDetailsService combinedUserDetailsService) {
|
|
||||||
this.combinedUserDetailsService = combinedUserDetailsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsParameter(MethodParameter methodParameter) {
|
|
||||||
return methodParameter.getParameterAnnotation(CurrentUser.class) != null
|
|
||||||
&& methodParameter.getParameterType().equals(User.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object resolveArgument(MethodParameter methodParameter,
|
|
||||||
ModelAndViewContainer modelAndViewContainer,
|
|
||||||
NativeWebRequest nativeWebRequest,
|
|
||||||
WebDataBinderFactory webDataBinderFactory) throws Exception {
|
|
||||||
if (this.supportsParameter(methodParameter)) {
|
|
||||||
Authentication authentication = (Authentication) nativeWebRequest.getUserPrincipal();
|
|
||||||
|
|
||||||
if (authentication.getPrincipal() instanceof User) {
|
|
||||||
return authentication.getPrincipal();
|
|
||||||
} else if (authentication.getPrincipal() instanceof String) {
|
|
||||||
return combinedUserDetailsService.loadUserByUsername((String)authentication.getPrincipal());
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new RuntimeException("Unsupported principal: "+authentication.getPrincipal());
|
|
||||||
} else {
|
|
||||||
return WebArgumentResolver.UNRESOLVED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,8 +2,5 @@ saml.keystore.location=classpath:/saml/samlKeystore.jks
|
||||||
saml.keystore.password=oktaiscool
|
saml.keystore.password=oktaiscool
|
||||||
saml.keystore.alias=oktasaml
|
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.idp=http://www.okta.com/exk26fxqrz8LLk9dV4x7
|
||||||
saml.sp=http://localhost:8080/saml/metadata
|
saml.sp=http://localhost:8080/saml/metadata
|
||||||
|
|
||||||
logging.level.root: info
|
|
|
@ -1,4 +0,0 @@
|
||||||
-- 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');
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,7 +0,0 @@
|
||||||
body {
|
|
||||||
padding-top: 5rem;
|
|
||||||
}
|
|
||||||
.starter-template {
|
|
||||||
padding: 3rem 1.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 5.3 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,32 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,12 +1,6 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html
|
<html>
|
||||||
lang="en"
|
|
||||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
|
||||||
layout:decorate="~{layout}"
|
|
||||||
>
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Content starts -->
|
|
||||||
<section layout:fragment="content">
|
|
||||||
<h6 class="border-bottom border-gray pb-2 mb-0">Something went wrong</h6>
|
<h6 class="border-bottom border-gray pb-2 mb-0">Something went wrong</h6>
|
||||||
<div class="media text-muted pt-3">
|
<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>
|
<i class="fas fa-door-closed fa-2x fa-fw mr-2 spring-green" data-fa-transform="shrink-4"></i>
|
||||||
|
@ -14,8 +8,6 @@
|
||||||
An error occurred
|
An error occurred
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
<!-- Content ends -->
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -1,22 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,14 +1,9 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html
|
<html>
|
||||||
lang="en"
|
<head>
|
||||||
xmlns:th="http://www.thymeleaf.org"
|
<title>Baeldung Spring Security SAML: Home</title>
|
||||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
</head>
|
||||||
layout:decorate="~{layout}"
|
|
||||||
xmlns="http://www.w3.org/1999/html">
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Content starts -->
|
|
||||||
<section layout:fragment="content">
|
|
||||||
<h3><Strong>Welcome!</strong><br/>You are successfully logged in!</h3>
|
<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>
|
<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">
|
<small class="d-block text-right mt-3" id="sso-btn">
|
||||||
|
@ -16,8 +11,5 @@
|
||||||
<i class="far fa-user-circle"></i> Logout
|
<i class="far fa-user-circle"></i> Logout
|
||||||
</a>
|
</a>
|
||||||
</small>
|
</small>
|
||||||
</section>
|
|
||||||
<!-- Content ends -->
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -1,21 +1,12 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html
|
<html>
|
||||||
lang="en"
|
<head>
|
||||||
xmlns:th="http://www.thymeleaf.org"
|
<title>Baeldung Spring Security SAML</title>
|
||||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
</head>
|
||||||
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>
|
<body>
|
||||||
|
<h3><Strong>Welcome to Baeldung Spring Security SAML</strong></h3>
|
||||||
|
<a th:href="@{/auth}" class="btn btn-spring btn-sm">
|
||||||
|
<i class="far fa-user-circle"></i> Login
|
||||||
|
</a>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,57 +0,0 @@
|
||||||
<!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…
Reference in New Issue