Reputation: 86
I am hoping someone can give me a more concrete example than the one I found in the documentation.
Using SpringBoot/Spring Security 5.6.0. I am migrating the authentication process based on SpringSecurity/SAML to SAML2.
I need to add to the Authentication a UserDetails built from the responseToken information. Something like what we can read in the documentation: https://docs.spring.io/spring-security/reference/5.6.0-RC1/servlet/saml2/index.html#servlet-saml2login-opensamlauthenticationprovider-userdetailsservice
But I don't understand the third point: return a custom authentication that includes the user details: "return MySaml2Authentication(userDetails, authentication);"
In any case you would have to do "return new MySaml2Authentication(userDetails, authentication);" right?
In any case, when the process continues it is executed:
Authentication authenticate = provider.authenticate(authentication);
Which as we can see replaces the Details value with the original.
authenticationResponse.setDetails(authentication.getDetails());
/**
* @param authentication the authentication request object, must be of type
* {@link Saml2AuthenticationToken}
* @return {@link Saml2Authentication} if the assertion is valid
* @throws AuthenticationException if a validation exception occurs
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication;
String serializedResponse = token.getSaml2Response();
Response response = parse(serializedResponse);
process(token, response);
AbstractAuthenticationToken authenticationResponse = this.responseAuthenticationConverter
.convert(new ResponseToken(response, token));
if (authenticationResponse != null) {
authenticationResponse.setDetails(authentication.getDetails());
}
return authenticationResponse;
}
catch (Saml2AuthenticationException ex) {
throw ex;
}
catch (Exception ex) {
throw createAuthenticationException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, ex.getMessage(), ex);
}
}
How can you add a UserDetails that depends on the information obtained from the tokens? I can't extend OpenSaml4AuthenticationProvider because it is "final".
The only thing I can think of is the option: https://docs.spring.io/spring-security/reference/5.6.0-RC1/servlet/saml2/index.html#servlet-saml2login-authenticationmanager-custom
And in the MySaml2AuthenticationManager set the Details after executing the Authentication authenticate = provider.authenticate(authentication); but it doesn't seem right to me.
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseAuthenticationConverter(responseToken -> {
Saml2Authentication auth = OpenSaml4AuthenticationProvider
.createDefaultResponseAuthenticationConverter()// First, call the default converter, which extracts attributes and authorities from the response
.convert(responseToken);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
String role = getRole(auth.getName());
grantedAuthorities.add(new SimpleGrantedAuthority(role));
Saml2Authentication saml2Authentication = new Saml2Authentication((AuthenticatedPrincipal) auth.getPrincipal(), auth.getSaml2Response(), grantedAuthorities);
/*
//The details are replaced by the authentication.getDetails().
MyUserDetails userDetails = new MyUserDetails(authenticate.getName(),auth.getPrincipal());
saml2Authentication.setDetails(userDetails);
*/
return saml2Authentication;
});
Authentication authenticate = provider.authenticate(authentication);
//Doesn't sound like a good idea
if ((authenticate.getPrincipal() instanceof DefaultSaml2AuthenticatedPrincipal)) {
DefaultSaml2AuthenticatedPrincipal samlPrincipal = (DefaultSaml2AuthenticatedPrincipal) authenticate.getPrincipal();
MyUserDetails userDetails = new MyUserDetails(authenticate.getName(),
samlPrincipal.getFirstAttribute("SMFIRSTNAME")
, samlPrincipal.getFirstAttribute("SMLASTNAME")
, samlPrincipal.getFirstAttribute("SMEMAIL"));
((Saml2Authentication)authenticate).setDetails(defaultDISUserDetails);
}
return authenticate;
Any better option?
Thank you very much for your help
Best regards
Upvotes: 6
Views: 1338
Reputation: 11
I had to add an additional condition on Principal so as to leverage the default spring SAML logout functionality, which is determined by the principal being instanceof Saml2Authentication. In addition the code sets a boolean parameter isSAML on the UserDetails during the login process.
public class CustomSaml2Authentication extends Saml2Authentication{
private final UserDetails user;
public CustomSaml2Authentication(UserDetails user, Saml2Authentication authentication) {
super((AuthenticatedPrincipal) authentication.getPrincipal(), authentication.getSaml2Response(), authentication.getAuthorities());
this.user = user;
}
/**
* this method is a hack to use User object as principal throughout the application
* while using Saml2Authentication as Principal for the /logout url alone.
* this was done as the Spring 6 SAML security send a SAML logout request to the IDP/Asserting party,
* based on the type of Principal object.
* <p>
* SAML logout is initiated only if the principal of type Saml2Authentication.
* else a local logout is initiated
*
* @return principal/User
*/
@Override
public Object getPrincipal() {
HttpServletRequest httpServletRequest =((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
AntPathRequestMatcher matcher = new AntPathRequestMatcher("/logout", HttpMethod.POST.name());
if (matcher.matches(httpServletRequest) && ((UserImpl)user).isSAML()) {
return getSaml2Principal();
}
return this.user;
}
public Saml2AuthenticatedPrincipal getSaml2Principal(){
return (Saml2AuthenticatedPrincipal) super.getPrincipal();
}
}
SecurityConfig looks like this
.saml2Login(saml2Login -> saml2Login.authenticationManager(new ProviderManager(getOpenSaml4AuthenticationProvider())))
And the getOpenSaml4AuthenticationProvider() method as follows:
private OpenSaml4AuthenticationProvider getOpenSaml4AuthenticationProvider() {
OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(
responseToken -> {
Saml2Authentication authentication = OpenSaml4AuthenticationProvider
.createDefaultResponseAuthenticationConverter()
.convert(responseToken);
assert authentication != null;
UserDetails user = samlUserDetailsService.loadUserBySAML(authentication);
return new CustomSaml2Authentication(user, authentication);
});
return authenticationProvider;
}
Upvotes: 0
Reputation: 11
I figured this out way too late as well. But you should actually use your own implementation of an AbstractAuthenticationToken and use an extra field to stuff your own object in. Here is mine:
public class CustomSaml2Authentication extends Saml2Authentication {
private User user;
public CustomSaml2Authentication(AuthenticatedPrincipal principal, String saml2Response,
Collection<? extends GrantedAuthority> authorities) {
super(principal, saml2Response, authorities);
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
Upvotes: 1