/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/
package org.cloudfoundry.identity.uaa.provider.saml;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication;
import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal;
import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationSuccessEvent;
import org.cloudfoundry.identity.uaa.authentication.manager.ExternalGroupAuthorizationEvent;
import org.cloudfoundry.identity.uaa.authentication.manager.InvitedUserAuthenticatedEvent;
import org.cloudfoundry.identity.uaa.authentication.manager.NewUserAuthenticatedEvent;
import org.cloudfoundry.identity.uaa.constants.OriginKeys;
import org.cloudfoundry.identity.uaa.provider.IdentityProvider;
import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning;
import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition;
import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMember;
import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMembershipManager;
import org.cloudfoundry.identity.uaa.user.UaaUser;
import org.cloudfoundry.identity.uaa.user.UaaUserDatabase;
import org.cloudfoundry.identity.uaa.user.UaaUserPrototype;
import org.cloudfoundry.identity.uaa.user.UserInfo;
import org.cloudfoundry.identity.uaa.util.UaaStringUtils;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.joda.time.DateTime;
import org.opensaml.saml2.core.Attribute;
import org.opensaml.saml2.core.AuthnStatement;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.schema.XSAny;
import org.opensaml.xml.schema.XSBase64Binary;
import org.opensaml.xml.schema.XSBoolean;
import org.opensaml.xml.schema.XSBooleanValue;
import org.opensaml.xml.schema.XSDateTime;
import org.opensaml.xml.schema.XSInteger;
import org.opensaml.xml.schema.XSQName;
import org.opensaml.xml.schema.XSString;
import org.opensaml.xml.schema.XSURI;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ProviderNotFoundException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.providers.ExpiringUsernameAuthenticationToken;
import org.springframework.security.saml.SAMLAuthenticationProvider;
import org.springframework.security.saml.SAMLAuthenticationToken;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.security.saml.context.SAMLMessageContext;
import org.springframework.security.saml.userdetails.SAMLUserDetailsService;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.EMAIL_ATTRIBUTE_NAME;
import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.FAMILY_NAME_ATTRIBUTE_NAME;
import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.GIVEN_NAME_ATTRIBUTE_NAME;
import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME;
import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.PHONE_NUMBER_ATTRIBUTE_NAME;
import static org.cloudfoundry.identity.uaa.provider.saml.LoginSamlAuthenticationToken.AUTHENTICATION_CONTEXT_CLASS_REFERENCE;
import static org.cloudfoundry.identity.uaa.util.UaaHttpRequestUtils.isAcceptedInvitationAuthentication;
public class LoginSamlAuthenticationProvider extends SAMLAuthenticationProvider implements ApplicationEventPublisherAware {
private final static Log logger = LogFactory.getLog(LoginSamlAuthenticationProvider.class);
private UaaUserDatabase userDatabase;
private ApplicationEventPublisher eventPublisher;
private IdentityProviderProvisioning identityProviderProvisioning;
private ScimGroupExternalMembershipManager externalMembershipManager;
public void setIdentityProviderProvisioning(IdentityProviderProvisioning identityProviderProvisioning) {
this.identityProviderProvisioning = identityProviderProvisioning;
}
public void setUserDatabase(UaaUserDatabase userDatabase) {
this.userDatabase = userDatabase;
}
public void setExternalMembershipManager(ScimGroupExternalMembershipManager externalMembershipManager) {
this.externalMembershipManager = externalMembershipManager;
}
@Override
public void setUserDetails(SAMLUserDetailsService userDetails) {
super.setUserDetails(userDetails);
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!supports(authentication.getClass())) {
throw new IllegalArgumentException("Only SAMLAuthenticationToken is supported, " + authentication.getClass() + " was attempted");
}
IdentityZone zone = IdentityZoneHolder.get();
SAMLAuthenticationToken token = (SAMLAuthenticationToken) authentication;
SAMLMessageContext context = token.getCredentials();
String alias = context.getPeerExtendedMetadata().getAlias();
boolean addNew;
IdentityProvider<SamlIdentityProviderDefinition> idp;
SamlIdentityProviderDefinition samlConfig;
try {
idp = identityProviderProvisioning.retrieveByOrigin(alias, IdentityZoneHolder.get().getId());
samlConfig = idp.getConfig();
addNew = samlConfig.isAddShadowUserOnLogin();
if (!idp.isActive()) {
throw new ProviderNotFoundException("Identity Provider has been disabled by administrator for alias:"+alias);
}
} catch (EmptyResultDataAccessException x) {
throw new ProviderNotFoundException("No SAML identity provider found in zone for alias:"+alias);
}
ExpiringUsernameAuthenticationToken result = getExpiringUsernameAuthenticationToken(authentication);
UaaPrincipal samlPrincipal = new UaaPrincipal(OriginKeys.NotANumber, result.getName(), result.getName(), alias, result.getName(), zone.getId());
Collection<? extends GrantedAuthority> samlAuthorities = retrieveSamlAuthorities(samlConfig, (SAMLCredential) result.getCredentials());
Collection<? extends GrantedAuthority> authorities = null;
SamlIdentityProviderDefinition.ExternalGroupMappingMode groupMappingMode = idp.getConfig().getGroupMappingMode();
switch (groupMappingMode) {
case EXPLICITLY_MAPPED:
authorities = mapAuthorities(idp.getOriginKey(), samlAuthorities);
break;
case AS_SCOPES:
authorities = new LinkedList<>(samlAuthorities);
break;
}
Set<String> filteredExternalGroups = filterSamlAuthorities(samlConfig, samlAuthorities);
MultiValueMap<String, String> userAttributes = retrieveUserAttributes(samlConfig, (SAMLCredential) result.getCredentials());
UaaUser user = createIfMissing(samlPrincipal, addNew, authorities, userAttributes);
UaaPrincipal principal = new UaaPrincipal(user);
UaaAuthentication resultUaaAuthentication = new LoginSamlAuthenticationToken(principal, result).getUaaAuthentication(user.getAuthorities(), filteredExternalGroups, userAttributes);
publish(new UserAuthenticationSuccessEvent(user, resultUaaAuthentication));
if (samlConfig.isStoreCustomAttributes()) {
userDatabase.storeUserInfo(user.getId(),
new UserInfo()
.setUserAttributes(resultUaaAuthentication.getUserAttributes())
.setRoles(new LinkedList(resultUaaAuthentication.getExternalGroups()))
);
}
return resultUaaAuthentication;
}
protected ExpiringUsernameAuthenticationToken getExpiringUsernameAuthenticationToken(Authentication authentication) {
return (ExpiringUsernameAuthenticationToken)super.authenticate(authentication);
}
protected void publish(ApplicationEvent event) {
if (eventPublisher != null) {
eventPublisher.publishEvent(event);
}
}
protected Set<String> filterSamlAuthorities(SamlIdentityProviderDefinition definition, Collection<? extends GrantedAuthority> samlAuthorities) {
List<String> whiteList = Optional.of(definition.getExternalGroupsWhitelist()).orElse(Collections.EMPTY_LIST);
Set<String> authorities = samlAuthorities.stream().map(s -> s.getAuthority()).collect(Collectors.toSet());
return UaaStringUtils.retainAllMatches(authorities, whiteList);
}
protected Collection<? extends GrantedAuthority> mapAuthorities(String origin, Collection<? extends GrantedAuthority> authorities) {
Collection<GrantedAuthority> result = new LinkedList<>();
for (GrantedAuthority authority : authorities ) {
String externalGroup = authority.getAuthority();
for (ScimGroupExternalMember internalGroup : externalMembershipManager.getExternalGroupMapsByExternalGroup(externalGroup, origin)) {
result.add(new SimpleGrantedAuthority(internalGroup.getDisplayName()));
}
}
return result;
}
public Collection<? extends GrantedAuthority> retrieveSamlAuthorities(SamlIdentityProviderDefinition definition, SAMLCredential credential) {
Collection<SamlUserAuthority> authorities = new ArrayList<>();
if (definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME)!=null) {
List<String> attributeNames = new LinkedList<>();
if (definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME) instanceof String) {
attributeNames.add((String) definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME));
} else if (definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME) instanceof Collection) {
attributeNames.addAll((Collection) definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME));
}
for (Attribute attribute : credential.getAttributes()) {
if ((attributeNames.contains(attribute.getName())) || (attributeNames.contains(attribute.getFriendlyName()))) {
if (attribute.getAttributeValues() != null && attribute.getAttributeValues().size() > 0) {
for (XMLObject group : attribute.getAttributeValues()) {
authorities.add(new SamlUserAuthority(getStringValue(attribute.getName(),definition,group)));
}
}
}
}
}
return authorities == null ? Collections.EMPTY_LIST : authorities;
}
public MultiValueMap<String, String> retrieveUserAttributes(SamlIdentityProviderDefinition definition, SAMLCredential credential) {
logger.debug(String.format("Retrieving SAML user attributes [zone:%s, origin:%s]", definition.getZoneId(), definition.getIdpEntityAlias()));
MultiValueMap<String, String> userAttributes = new LinkedMultiValueMap<>();
if (definition != null && definition.getAttributeMappings() != null) {
for (Entry<String, Object> attributeMapping : definition.getAttributeMappings().entrySet()) {
if (attributeMapping.getValue() instanceof String) {
if (credential.getAttribute((String)attributeMapping.getValue()) != null) {
String key = attributeMapping.getKey();
for (XMLObject xmlObject : credential.getAttribute((String) attributeMapping.getValue()).getAttributeValues()) {
String value = getStringValue(key, definition, xmlObject);
if (value!=null) {
userAttributes.add(key, value);
}
}
}
}
}
}
if (credential.getAuthenticationAssertion() != null && credential.getAuthenticationAssertion().getAuthnStatements() != null) {
for (AuthnStatement statement : credential.getAuthenticationAssertion().getAuthnStatements()) {
if (statement.getAuthnContext() != null && statement.getAuthnContext().getAuthnContextClassRef() != null) {
userAttributes.add(AUTHENTICATION_CONTEXT_CLASS_REFERENCE, statement.getAuthnContext().getAuthnContextClassRef().getAuthnContextClassRef());
}
}
}
return userAttributes;
}
protected String getStringValue(String key, SamlIdentityProviderDefinition definition, XMLObject xmlObject) {
String value = null;
if (xmlObject instanceof XSString) {
value = ((XSString) xmlObject).getValue();
} else if (xmlObject instanceof XSAny) {
value = ((XSAny)xmlObject).getTextContent();
} else if (xmlObject instanceof XSInteger) {
Integer i = ((XSInteger)xmlObject).getValue();
value = i!=null ? i.toString() : null;
} else if (xmlObject instanceof XSBoolean) {
XSBooleanValue b = ((XSBoolean)xmlObject).getValue();
value = b!=null && b.getValue()!=null ? b.getValue().toString() : null;
} else if (xmlObject instanceof XSDateTime) {
DateTime d = ((XSDateTime)xmlObject).getValue();
value = d!=null ? d.toString() : null;
} else if (xmlObject instanceof XSQName) {
QName name = ((XSQName) xmlObject).getValue();
value = name!=null ? name.toString() : null;
} else if (xmlObject instanceof XSURI) {
value = ((XSURI) xmlObject).getValue();
} else if (xmlObject instanceof XSBase64Binary) {
value = ((XSBase64Binary) xmlObject).getValue();
}
if (value!=null) {
logger.debug(String.format("Found SAML user attribute %s of value %s [zone:%s, origin:%s]", key, value, definition.getZoneId(), definition.getIdpEntityAlias()));
return value;
} else if (xmlObject !=null){
logger.debug(String.format("SAML user attribute %s at is not of type XSString or other recognizable type, %s [zone:%s, origin:%s]", key, xmlObject.getClass().getName(),definition.getZoneId(), definition.getIdpEntityAlias()));
}
return null;
}
protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Collection<? extends GrantedAuthority> authorities, MultiValueMap<String, String> userAttributes) {
UaaUser user = null;
String invitedUserId = null;
boolean is_invitation_acceptance = isAcceptedInvitationAuthentication();
if (is_invitation_acceptance) {
invitedUserId = (String) RequestContextHolder.currentRequestAttributes().getAttribute("user_id", RequestAttributes.SCOPE_SESSION);
user = userDatabase.retrieveUserById(invitedUserId);
if ( userAttributes.getFirst(EMAIL_ATTRIBUTE_NAME) != null ) {
if (!userAttributes.getFirst(EMAIL_ATTRIBUTE_NAME).equalsIgnoreCase(user.getEmail()) ) {
throw new BadCredentialsException("SAML User email mismatch. Authenticated email doesn't match invited email.");
}
} else {
userAttributes = new LinkedMultiValueMap<>(userAttributes);
userAttributes.add(EMAIL_ATTRIBUTE_NAME, user.getEmail());
}
addNew = false;
if(user.getUsername().equals(user.getEmail()) && !user.getUsername().equals(samlPrincipal.getName())) {
user.setVerified(true);
user = user.modifyUsername(samlPrincipal.getName());
}
publish(new InvitedUserAuthenticatedEvent(user));
user = userDatabase.retrieveUserById(invitedUserId);
}
boolean userModified = false;
UaaUser userWithSamlAttributes = getUser(samlPrincipal, userAttributes);
try {
if (user==null) {
user = userDatabase.retrieveUserByName(samlPrincipal.getName(), samlPrincipal.getOrigin());
}
} catch (UsernameNotFoundException e) {
UaaUser uaaUser = userDatabase.retrieveUserByEmail(userWithSamlAttributes.getEmail(), samlPrincipal.getOrigin());
if (uaaUser != null) {
user = uaaUser.modifyUsername(samlPrincipal.getName());
} else {
if (!addNew) {
throw new LoginSAMLException("SAML user does not exist. "
+ "You can correct this by creating a shadow user for the SAML user.", e);
}
// Register new users automatically
publish(new NewUserAuthenticatedEvent(userWithSamlAttributes));
try {
user = userDatabase.retrieveUserByName(samlPrincipal.getName(), samlPrincipal.getOrigin());
} catch (UsernameNotFoundException ex) {
throw new BadCredentialsException("Unable to establish shadow user for SAML user:"+ samlPrincipal.getName());
}
}
}
if (haveUserAttributesChanged(user, userWithSamlAttributes)) {
userModified = true;
user = user.modifyAttributes(userWithSamlAttributes.getEmail(), userWithSamlAttributes.getGivenName(), userWithSamlAttributes.getFamilyName(), userWithSamlAttributes.getPhoneNumber());
}
publish(
new ExternalGroupAuthorizationEvent(
user,
userModified,
authorities,
true
)
);
user = userDatabase.retrieveUserById(user.getId());
return user;
}
protected UaaUser getUser(UaaPrincipal principal, MultiValueMap<String,String> userAttributes) {
String name = principal.getName();
String email = userAttributes.getFirst(EMAIL_ATTRIBUTE_NAME);
String givenName = userAttributes.getFirst(GIVEN_NAME_ATTRIBUTE_NAME);
String familyName = userAttributes.getFirst(FAMILY_NAME_ATTRIBUTE_NAME);
String phoneNumber = userAttributes.getFirst(PHONE_NUMBER_ATTRIBUTE_NAME);
String userId = OriginKeys.NotANumber;
String origin = principal.getOrigin()!=null?principal.getOrigin(): OriginKeys.LOGIN_SERVER;
String zoneId = principal.getZoneId();
if (name == null && email != null) {
name = email;
}
if (name == null && OriginKeys.NotANumber.equals(userId)) {
throw new BadCredentialsException("Cannot determine username from credentials supplied");
} else if (name==null) {
//we have user_id, name is irrelevant
name="unknown";
}
if (email == null) {
if (name.contains("@")) {
if (name.split("@").length == 2 && !name.startsWith("@") && !name.endsWith("@")) {
email = name;
} else {
email = name.replaceAll("@", "") + "@unknown.org";
}
}
else {
email = name + "@unknown.org";
}
}
if (givenName == null) {
givenName = email.split("@")[0];
}
if (familyName == null) {
familyName = email.split("@")[1];
}
return new UaaUser(
new UaaUserPrototype()
.withEmail(email)
.withGivenName(givenName)
.withFamilyName(familyName)
.withPhoneNumber(phoneNumber)
.withModified(new Date())
.withId(userId)
.withUsername(name)
.withPassword("")
.withAuthorities(Collections.EMPTY_LIST)
.withCreated(new Date())
.withOrigin(origin)
.withExternalId(name)
.withVerified(true)
.withZoneId(zoneId)
.withSalt(null)
.withPasswordLastModified(null));
}
private boolean haveUserAttributesChanged(UaaUser existingUser, UaaUser user) {
if (!StringUtils.equals(existingUser.getGivenName(), user.getGivenName()) || !StringUtils.equals(existingUser.getFamilyName(), user.getFamilyName()) ||
!StringUtils.equals(existingUser.getPhoneNumber(), user.getPhoneNumber()) || !StringUtils.equals(existingUser.getEmail(), user.getEmail())) {
return true;
}
return false;
}
}