Can not get user info with Spring Security SAML WITHOUT Spring Boot - spring

I´m working on SAML integration in an older project but I can´t get the user information.
I've guided me with the response of this question:
https://stackoverflow.com/questions/70275050/spring-security-saml-identity-metadata-without-spring-boot
The project has these versions:
spring framework 5.3.24
spring security 5.6.10
opensaml 3.4.6
This is my code:
#Configuration
public class SAMLSecurityConfig {
private static final String URL_METADATA = "https://auth-dev.mycompany.com/app/id/sso/saml/metadata";
#Bean("samlRegistration")
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(URL_METADATA)
.registrationId("id")
.build();
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
}
}
#EnableWebSecurity
public class WebSecurity {
#Configuration
#Order(2)
public static class SAMLSecurityFilter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.saml2Login(Customizer.withDefaults())
.antMatcher("/login/assertion")
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
}
#Controller("loginController")
public class BoCRLoginController {
#RequestMapping(value = "/login/assertion", method = {RequestMethod.POST},
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<String> assertLoginData(#AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) {
System.out.println(principal); //here I get a null
return new ResponseEntity<>(HttpStatus.OK);
}
}
Once I did the login on okta the class: Saml2AuthenticatedPrincipal comes null value.
Could you help me to know why I received null value on the object Saml2AuthenticatedPrincipal where suppose have to receive the user information?

Related

How to inject a test UserDetailsManager into CustomProviderManager during SpringBootTest?

Given is a Spring Boot application with a custom ProviderManager:
#Component
public class CustomProviderManager extends ProviderManager {
public CustomProviderManager(
AuthenticationProvider internalAuthenticationProvider,
AuthenticationProvider devUserAuthenticationProvider) {
super(internalAuthenticationProvider, devUserAuthenticationProvider);
}
}
The SecurityFilterChain is setup with a custom UsernamePasswordAuthenticationFilter:
#Bean
public SecurityFilterChain mvcFilterChain(HttpSecurity http) throws Exception {
return http
//....
.addFilterAt(internalUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
//....
}
And here the custom UsernamePasswordAuthenticationFilter:
#Component
public class InternalUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final SecurityContextRepository securityContextRepository;
private final AuthenticationFailureHandler authenticationFailureHandler;
private final AuthenticationSuccessHandler authenticationSuccessHandler;
#PostConstruct
private void setup() {
super.setUsernameParameter("identifier");
super.setPasswordParameter("password");
super.setFilterProcessesUrl("/authenticate");
super.setSecurityContextRepository(securityContextRepository);
super.setAuthenticationFailureHandler(authenticationFailureHandler);
super.setAuthenticationSuccessHandler(authenticationSuccessHandler);
super.afterPropertiesSet();
}
public InternalUsernamePasswordAuthenticationFilter(
AuthenticationManager customProviderManager,
SecurityContextRepository delegatingSecurityContextRepository,
AuthenticationFailureHandler authenticationFailureHandler,
AuthenticationSuccessHandler authenticationSuccessHandler) {
this.securityContextRepository = delegatingSecurityContextRepository;
this.authenticationFailureHandler = authenticationFailureHandler;
this.authenticationSuccessHandler = authenticationSuccessHandler;
super.setAuthenticationManager(customProviderManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//....
}
}
What I want to do now is testing the authentication logic. But instead of using the authentication providers of the application, I want to use a special UserDetailsManager for testing only. The current TestConfiguration class containing a TestUserDetailsManager looks like that:
#TestConfiguration
public class TestUserDetailsManagerConfig {
#Bean
#Primary
public UserDetailsManager testUserDetailsManager() {
User.UserBuilder users = User.builder();
UserDetails testUser = users
.username("test-user#example.com")
.password("test-user")
.roles("USER")
.build();
UserDetails testAdmin = users
.username("test-admin#example.com")
.password("test-admin")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(testUser, testAdmin);
}
}
And finally, a test method that should authenticate against the TestUserDetailsManager:
#SpringBootTest
#Import(TestUserDetailsManagerConfig.class)
public class InternalAuthenticationTest {
#Autowired WebApplicationContext context;
MockMvc mvc;
#BeforeEach
void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
#Test
void form_login_redirects_role_admin_to_page_admin_after_authentication() throws Exception {
MvcResult result = mvc
.perform(SecurityMockMvcRequestBuilders
.formLogin()
.loginProcessingUrl("/authenticate")
.user("identifier", "test-admin#example.com")
.password("password", "test-admin"))
.andExpect(MockMvcResultMatchers.redirectedUrl(AUTH_LOGIN_SUCCESS_ADMIN_REDIRECT_URL))
.andExpect(SecurityMockMvcResultMatchers.authenticated()
.withUsername("test-admin#example.com").withRoles("ADMIN")
.withAuthentication(auth -> assertThat(auth).isInstanceOf(UsernamePasswordAuthenticationToken.class)))
.andReturn();
}
}
My naive approach unfortunately does not work, and as the log shows, the authentication checks are done against the application provider, but not against the TestUserDetailsManager:
Invoking InternalUsernamePasswordAuthenticationFilter (7/12)
Authenticating request with InternalAuthenticationProvider (1/2)
Failed to find user credential for email 'test-admin#example.com'
Authenticating request with $Proxy157 (2/2)
Failed to find user 'test-admin#example.com'
Failed to process authentication request
-> Bad credentials
My question now:
How can I inject the TestUserDetailsManager into the CustomProviderManager so that the authentication (not authorization) tests work with special test users?
edit:
The question somewhat more generally:
How can I test the authentication of a Spring Boot application using a special UserDetailsManager for test cases only?
Many thanks in advance

Customise user login behaviour in OAuth based B2B multi tenant Spring Boot application using Spring Security

I am working on a Spring Boot application, which has two tenants as of now: tenant1 and tenant2 (in the future, I will add more tenants). Each of the tenants has its own authentication providers.
In order to achieve the same, as of now, I have made the following changes in my application:
config changes are as follows:
spring.security.oauth2.client.registration.tenant1.client-id=abcd
spring.security.oauth2.client.registration.tenant1.client-authentication-method=basic
spring.security.oauth2.client.registration.tenant1.authorization-grant-type=authorization_code
myapp.oauth2.path=https://external.authorization.server/services/oauth2/
spring.security.oauth2.client.provider.tenant1.token-uri=${myapp.oauth2.path}token
spring.security.oauth2.client.provider.tenant1.authorization-uri=${myapp.oauth2.path}authorize
spring.security.oauth2.client.provider.tenant1.user-info-uri=${myapp.oauth2.path}userinfo
spring.security.oauth2.client.provider.tenant1.user-name-attribute=name
spring.security.oauth2.client.registration.tenant2.client-id=efgh
spring.security.oauth2.client.registration.tenant2.client-secret=secret
spring.security.oauth2.client.registration.tenant2.client-authentication-method=basic
spring.security.oauth2.client.registration.tenant2.authorization-grant-type=authorization_code
spring.security.oauth2.client.provider.tenant2.token-uri=${myapp.oauth2.path}token
spring.security.oauth2.client.provider.tenant2.authorization-uri=${myapp.oauth2.path}authorize
spring.security.oauth2.client.provider.tenant2.user-info-uri=${myapp.oauth2.path}userinfo
spring.security.oauth2.client.provider.tenant2.user-name-attribute=name
As of now, I am fetching client secrets for both tenants from Vault, so I had to define the OAuth2 configuration as follows:
#EnableConfigurationProperties(OAuth2ClientProperties.class)
#Conditional(ClientsConfiguredCondition.class)
#Configuration
public class OAuth2Configuration {
static final String OAUTH2_CLIENT_SECRET_KEY = "oauth2_client_secret";
private static final Logger log = LoggerFactory.getLogger(OAuth2Configuration.class);
private static final String OAUTH2_REGISTRATION_MISSING =
"oAuth2 registration properties are missing";
private final ApplicationSecretProvider applicationSecretProvider;
private final Map<String, ClientAuthenticationMethod> clientAuthenticationMethodMap =
new HashMap<>();
private final String authenticationMethod;
public OAuth2Configuration(
#Value("${spring.security.oauth2.client.registration.tenant1.client-authentication-method}")
final String authenticationMethod,
final ApplicationSecretProvider applicationSecretProvider) {
this.authenticationMethod = authenticationMethod;
this.applicationSecretProvider = applicationSecretProvider;
this.clientAuthenticationMethodMap
.put(ClientAuthenticationMethod.POST.getValue(), ClientAuthenticationMethod.POST);
this.clientAuthenticationMethodMap
.put(ClientAuthenticationMethod.BASIC.getValue(), ClientAuthenticationMethod.BASIC);
this.clientAuthenticationMethodMap
.put(ClientAuthenticationMethod.NONE.getValue(), ClientAuthenticationMethod.NONE);
}
#Bean
public InMemoryClientRegistrationRepository getClientRegistrationRepository(
OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList<>(
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
//We will have only one client registered for oAuth
if (CollectionUtils.isEmpty(registrations)) {
log.error(OAUTH2_REGISTRATION_MISSING);
throw new IllegalStateException(OAUTH2_REGISTRATION_MISSING);
}
ClientRegistration registration = registrations.get(0);
ClientRegistration.Builder builder = ClientRegistration.withClientRegistration(registration);
ClientAuthenticationMethod clientAuthenticationMethod =
getClientAuthenticationMethod(authenticationMethod);
ClientRegistration completeRegistration = builder
.clientSecret(applicationSecretProvider.getSecretForKey(OAUTH2_CLIENT_SECRET_KEY))
.clientAuthenticationMethod(clientAuthenticationMethod)
.build();
ClientRegistration testRegistration = registrations.get(1);
return new InMemoryClientRegistrationRepository(List.of(completeRegistration, testRegistration));
}
protected ClientAuthenticationMethod getClientAuthenticationMethod(String grantType) {
ClientAuthenticationMethod retValue = clientAuthenticationMethodMap.get(grantType);
if (retValue == null) {
return ClientAuthenticationMethod.NONE;
}
return retValue;
}
}
Then I extended DefaultOAuth2UserService in order to save user details in my application as follows:
#Component
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private UserRepository userRepository;
private AuthorityRepository authRepository;
#Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Autowired
public void setAuthorityRepository(AuthorityRepository
authorityRepository) {
this.authorityRepository = authorityRepository;
}
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
DefaultOAuth2User oAuth2User = (DefaultOAuth2User) super.loadUser(userRequest);
Collection<GrantedAuthority> authorities = new HashSet<>(oAuth2User.getAuthorities());
Map<String, Object> attributes = oAuth2User.getAttributes();
...
return new DefaultOAuth2User(authorities, oAuth2User.getAttributes(), userNameAttributeName);
}
}
Security configuration is as follows:
#EnableWebSecurity
#Import(SecurityProblemSupport.class)
#ConditionalOnProperty(
value = "myapp.authentication.type",
havingValue = "oauth",
matchIfMissing = true
)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customoAuth2UserService;
public SecurityConfiguration(CustomOAuth2UserService customoAuth2UserService) {
this.customoAuth2UserService = customoAuth2UserService;
}
public void configure(HttpSecurity http) throws Exception {
http
.csrf()
.authorizeRequests()
.antMatchers("/login**").permitAll()
.antMatchers("/manage/**").permitAll()
.antMatchers("/api/auth-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.anyRequest().authenticated()
//.and().oauth2ResourceServer().jwt()
.and()
//.and()
.oauth2Login()
.redirectionEndpoint()
.baseUri("/oauth2**")
.and()
.failureUrl("/api/redirectToHome")
.userInfoEndpoint().userService(customoAuth2UserService);
http.cors().disable();
}
}
With this on /login page, users from both the tenants are able to see both the login links.
I have following question in this regard:
(1) Instead of showing multiple links on the login page, would like to have a common entry page for all users, where users can enter the email and based on tenant id (derived from email), user can be redirected to the appropriate authentication provider and post successful authentication, authenticated user details can be saved in application database as done in CustomOAuth2UserService. How would I achieve this?
In this regard, I have gone through several articles/posts but did not get any concrete idea regarding what changes should I do in the existing code base to achieve this.

Spring Security OAuth2 - Getting a custom principal from the Authentication object?

I am currently trying to get a custom UserInformation object to come back when I try to access the principal field from the Authentication object (Authentication.getPrincipal()) when using OAuth2. I am enabling OAuth2 in the WebSecurityConfingAdapter by adding the .oauth2Login() property:
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2Login();
}
}
When I add this property and try to access the principal from the controller, it says that the type of the principal is DefaultOidcUser.
#RestController
public class OAuthController {
#GetMapping("/getPrincipal")
public String authenticate(Authentication authenticate) {
return "PRINCIPAL CLASS: " + authenticate.getPrincipal().getClass().getName();
}
}
Is there a way I can have the Authentication principal return a custom object (not an OidcUser)? I tried to write a custom OidcService, but it still needs to return an OidcUser.
Here are the dependencies I am pulling in:
spring-boot-starter-security: 2.2.4-RELEASE
spring-security-oauth2-client: 5.2.1-RELEASE
spring-security-oauth2-jose: 5.2.1-RELEASE
Below are the custom OidcServer and UserInformation object I have so far:
#Configuration
public class CustomOIDCUserService extends OidcUserService {
#Override
public OidcUser loadUser(OidcUserRequest oidcUserRequestst) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(oidcUserRequestst);
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
//Add roles to mappedAuthorities
UserInformation userInfo = new UserInformation(new DefaultOidcUser(mappedAuthorities,
oidcUser.getIdToken(),
oidcUser.getUserInfo()));
//Initialize other fields from oidcUser
return userInfo;
}
}
public class UserInformation implements OidcUser, UserDetails {
private OidcUser oidcUser;
String name;
String email;
public UserInformation(OidcUser oidcUser) {
this.oidcUser = oidcUser;
}
//Setting constructors, getters, and setters
Thank you!

How to extract custom Principal in OAuth2 Resource Server?

I'm using Keycloak as my OAuth2 Authorization Server and I configured an OAuth2 Resource Server for Multitenancy following this official example on GitHub.
The current Tenant is resolved considering the Issuer field of the JWT token.
Hence the token is verified against the JWKS exposed at the corresponding OpenID Connect well known endpoint.
This is my Security Configuration:
#EnableWebSecurity
#RequiredArgsConstructor
#EnableAutoConfiguration(exclude = UserDetailsServiceAutoConfiguration.class)
public class OrganizationSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final TenantService tenantService;
private List<Tenant> tenants;
#PostConstruct
public void init() {
this.tenants = this.tenantService.findAllWithRelationships();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.authenticationManagerResolver(new MultiTenantAuthenticationManagerResolver(this.tenants));
}
}
and this is my custom AuthenticationManagerResolver:
public class MultiTenantAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
private final AuthenticationManagerResolver<HttpServletRequest> resolver;
private List<Tenant> tenants;
public MultiTenantAuthenticationManagerResolver(List<Tenant> tenants) {
this.tenants = tenants;
List<String> trustedIssuers = this.tenants.stream()
.map(Tenant::getIssuers)
.flatMap(urls -> urls.stream().map(URL::toString))
.collect(Collectors.toList());
this.resolver = new JwtIssuerAuthenticationManagerResolver(trustedIssuers);
}
#Override
public AuthenticationManager resolve(HttpServletRequest context) {
return this.resolver.resolve(context);
}
}
Now, because of the design of org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver
which is private, the only way I can think in order to extract a custom principal is to reimplement everything that follows:
TrustedIssuerJwtAuthenticationManagerResolver
the returned AuthenticationManager
the AuthenticationConverter
the CustomAuthenticationToken which extends JwtAuthenticationToken
the CustomPrincipal
To me it seems a lot of Reinventing the wheel, where my only need would be to have a custom Principal.
The examples that I found don't seem to suit my case since they refer to OAuth2Client or are not tought for Multitenancy.
https://www.baeldung.com/spring-security-oauth-principal-authorities-extractor
How to extend OAuth2 principal
Do I really need to reimplement all such classes/interfaes or is there a smarter approach?
This is how I did it, without reimplementing a huge amount of classes. This is without using a JwtAuthenticationToken however.
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver()));
}
#Bean
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver() {
List<String> issuers = ... // get this from list of tennants or config, whatever
Predicate<String> trustedIssuer = issuers::contains;
Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>();
AuthenticationManagerResolver<String> resolver = (String issuer) -> {
if (trustedIssuer.test(issuer)) {
return authenticationManagers.computeIfAbsent(issuer, k -> {
var jwtDecoder = JwtDecoders.fromIssuerLocation(issuer);
var provider = new JwtAuthenticationProvider(jwtDecoder);
provider.setJwtAuthenticationConverter(jwtAuthenticationService::loadUserByJwt);
return provider::authenticate;
});
}
return null;
};
return new JwtIssuerAuthenticationManagerResolver(resolver);
}
}
#Service
public class JwtAuthenticationService {
public AbstractAuthenticationToken loadUserByJwt(Jwt jwt) {
UserDetails userDetails = ... // or your choice of principal
List<GrantedAuthority> authorities = ... // extract from jwt or db
...
return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
}
}

PreAuthorize not getting honored over ResourceServerConfigurerAdaptor

I have a Spring Resource Server with Spring Security enabled. In Resource Server, i am extending the ResourceServerConfigurerAdaptor, some like the following.
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http
.anonymous().disable().requestMatcher(new OAuthRequestedMatcher()).authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/admin/**").hasAnyRole("ADMIN")
.anyRequest().authenticated();
}
private static class OAuthRequestedMatcher implements RequestMatcher {
public boolean matches(HttpServletRequest request) {
String auth = request.getHeader("Authorization");
boolean haveOauth2Token = (auth != null) && auth.toLowerCase().startsWith("bearer");
boolean haveAccessToken = request.getParameter("access_token") != null;
return haveOauth2Token || haveAccessToken;
}
}
}
Here i am expecting /api/admin/** to be accessible to ADMIN Role only.
Everything works fine till now.
But now i am trying to override this behavior at the method level using #PreAuthorize method level annotation.
Following is the RestController
#RestController
#RequestMapping("/api/admin/event")
public class ShunyaEventResource {
#Autowired
private ShunyaEventService eventService;
#PreAuthorize("hasRole('ADMIN') or #oauth2.hasScope('write')")
#PostMapping
public void createEvent(#RequestBody ShunyaEvent event, Principal user) {
eventService.create(event);
}
}
So, i want to allow /api/admin/event to be accessible to #oauth2 write scope as well. But this does not work, unless either i remove /api/admin/** from antmatcher altogether or i add #oauth2.hasScope('write') in antmatcher itself.
I have already defined the below configuration
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
Here my question is why Method Level security not working for an endpoint that is already covered by ResourceServerConfigurerAdapter? What is precedence of security filter when same endpoint is covered by MethodLevel Security (using PreAuthorize) and HttpSecurity antMatcher?
Really appreciate any help on this.

Resources