Skip to content

Authenticating SAML user by UserDetailService #7550

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory;
Expand Down Expand Up @@ -103,6 +104,8 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>> extend

private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;

private UserDetailsService userDetailsService;

/**
* Sets the {@code RelyingPartyRegistrationRepository} of relying parties, each party representing a
* service provider, SP and this host, and identity provider, IDP pair that communicate with each other.
Expand Down Expand Up @@ -143,6 +146,17 @@ protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingU
return new AntPathRequestMatcher(loginProcessingUrl);
}

/**
* Set a {@link UserDetailsService} to lookup the SAML user with a local user service.
* The service can look up the SAML user in a local database, reject the authentication, if an exception is thrown and set authorities.
* @param userDetailsService the user details service
*/
public Saml2LoginConfigurer<B> userDetailsService(UserDetailsService userDetailsService) {
Assert.notNull(userDetailsService, "userDetailsService cannot be null");
this.userDetailsService = userDetailsService;
return this;
}

/**
* {@inheritDoc}
*
Expand Down Expand Up @@ -211,7 +225,10 @@ public void configure(B http) throws Exception {
}

private AuthenticationProvider getAuthenticationProvider() {
AuthenticationProvider provider = new OpenSamlAuthenticationProvider();
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
if (this.userDetailsService != null) {
provider.setUserDetailsService(this.userDetailsService);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that you can explicitly set it. In addition to this, what if there is a

@Bean 
UserDetailsService

in the context. We should pick up that bean too

Copy link
Author

@herrminni herrminni Nov 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello Fillip,
thank you for your review!

I like your idea of automatically using the UserDetailsService from @Bean registry, if this is present.

However my implementation of manually setting (or maybe overriding) to a specific UserDetailsService was explicitly intended by me:
This way you would be able to have two different UserDetailsServices at the same time and handle SAML authentication in a different way. For example, you could explicitly create a new User as a sideeffect, if that one was not known in a local database. The other UserDetailsService could be used to only authenticate already known users an authenticate by username+password.

Maybe we could combine your and my idea:
User the spring @Bean UserDetailsService as default and also be able to overrride it with the method I already included (setUserDetailsService)?

return postProcess(provider);
}

Expand Down Expand Up @@ -311,4 +328,5 @@ private Saml2AuthenticationRequestFactory getResolver(B http) {
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,28 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
}
----
Optionally, a `UserDetailsService` can be set, to authorize the SAML user name by a (local) database, add metadata and set authorities like roles.
[source,java]
----
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private CustomUserDetailsService userDetailsServiceImp;

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.saml2Login()
.userDetailsService(userDetailsServiceImpl)

;
}
}
----

==== RelyingPartyRegistration
The https://github.com/spring-projects/spring-security/blob/5.2.0.RELEASE/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java[`RelyingPartyRegistration`]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
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.saml2.Saml2Exception;
import org.springframework.security.saml2.credentials.Saml2X509Credential;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -101,9 +104,10 @@
* The SAML response object can be signed. If the Response is signed, a signature will not be required on the assertion.
* </p>
* <p>
* While a response object can contain a list of assertion, this provider will only leverage
* the first valid assertion for the purpose of authentication. Assertions that do not pass validation
* will be ignored. If no valid assertions are found a {@link Saml2AuthenticationException} is thrown.
* While a response object can contain a list of assertion, this provider will only
* leverage the first valid assertion for the purpose of authentication. Assertions that
* do not pass validation will be ignored. If no valid assertions are found a
* {@link Saml2AuthenticationException} is thrown.
* </p>
* <p>
* This provider supports two types of encrypted SAML elements
Expand All @@ -130,6 +134,7 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi
(a -> singletonList(new SimpleGrantedAuthority("ROLE_USER")));
private GrantedAuthoritiesMapper authoritiesMapper = (a -> a);
private Duration responseTimeValidationSkew = Duration.ofMinutes(5);
private UserDetailsService userDetailsService;

/**
* Sets the {@link Converter} used for extracting assertion attributes that
Expand Down Expand Up @@ -162,6 +167,10 @@ public void setResponseTimeValidationSkew(Duration responseTimeValidationSkew) {
this.responseTimeValidationSkew = responseTimeValidationSkew;
}

public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}

/**
* @param authentication the authentication request object, must be of type
* {@link Saml2AuthenticationToken}
Expand All @@ -176,11 +185,27 @@ public Authentication authenticate(Authentication authentication) throws Authent
Response samlResponse = getSaml2Response(token);
Assertion assertion = validateSaml2Response(token, token.getRecipientUri(), samlResponse);
String username = getUsername(token, assertion);
return new Saml2Authentication(
() -> username, token.getSaml2Response(),
this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion))
);
} catch (Saml2AuthenticationException e) {

if (this.userDetailsService != null) {
// user details authentication
UserDetails userDetails = this.userDetailsService
.loadUserByUsername(username);
if (userDetails == null) {
throw new UsernameNotFoundException(
"SAML authenticated user with username '" + username
+ "' not found by user details service.");
}
return new Saml2Authentication(userDetails, token.getSaml2Response(),
userDetails.getAuthorities());
}
else {
// original authentication, sent by SAML
return new Saml2Authentication(username, token.getSaml2Response(),
this.authoritiesMapper
.mapAuthorities(getAssertionAuthorities(assertion)));
}
}
catch (Saml2AuthenticationException e) {
throw e;
} catch (Exception e) {
throw authException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, e.getMessage(), e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package org.springframework.security.saml2.provider.service.authentication;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
Expand All @@ -37,10 +36,10 @@
*/
public class Saml2Authentication extends AbstractAuthenticationToken {

private final AuthenticatedPrincipal principal;
private final Object principal;
private final String saml2Response;

public Saml2Authentication(AuthenticatedPrincipal principal,
public Saml2Authentication(Object principal,
String saml2Response,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
Expand Down
Loading