Ho avuto la necessità di integrare liferay 6.2 con un cas server per la gestione della login centralizzata. In questo post vedremo come autenticarci tramite facebook/linkedin attravero il cas.
Per prima cosa scarichiamo il cas-overlay (nel mi caso quello relativo alla versione 5.0.4)
Modifichiamo il build.gradle aggiungendo le seguenti dipendenze
compile "org.apereo.cas:cas-server-support-ldap:${project.'cas.version'}"
compile "org.apereo.cas:cas-server-support-pac4j-webflow:${project.'cas.version'}"
La prima è relativa al supporto ldap (facoltativa) la seconda è relativa alle librerie pac4j
che saranno utilizzate dal cas server per delegare l'autenticazione presso sistemi terzi (facebook/linkdin nel nostro caso)
Una volta compilato e deployato il war, modifichiamo il file application.properties
Aggiungiamo le properties relative a linkedin
cas.authn.pac4j.linkedIn.id=<app_id>
cas.authn.pac4j.linkedIn.secret=<app_secret>
cas.authn.pac4j.linkedIn.fields=id,first-name,last-name,email-address,picture-url,positions
cas.authn.pac4j.linkedIn.scope=r_basicprofile,r_emailaddress
e quelle relative a facebook (i fields e gli scope compilati sono quelli approvati automaticamente alla creazione dell'app)
cas.authn.pac4j.facebook.fields=id,name,first_name,last_name,gender,locale,languages,link,timezone,updated_time,verified,email,picture
cas.authn.pac4j.facebook.id=<app_id>
cas.authn.pac4j.facebook.secret=app_secret>
cas.authn.pac4j.facebook.scope=email,public_profile,user_friends
Settiamo le url del cas (verranno usate per le url di redirect)
cas.server.name=https://<cas-server-host>:<port>
cas.server.prefix=https://<cas-server-host>:<port>/cas-server
Aggiungiamo la seguente property. Questa verrà utilizzata per far si che il principal venga popolato con un indicazione
del sistema su cui ci siamo autenticati
cas.authn.pac4j.typedIdUsed=true
Es: org.pac4j.oauth.profile.facebook.FacebookProfile#<facebook_principal_id>
Abilitiamo il cas (utilizzando il protollo cas3) a rilasciare gli attributi opzionali a sistemi terzi (nel nostro caso Liferay)
cas.view.cas3.releaseProtocolAttributes=true
e configuriamo gli attributi che andranno propagati di default
cas.authn.attributeRepository.defaultAttributesToRelease=id,access_token,firstName,lastName,emailAddress,email,first_name,last_name,gender,locale
La configurazione del cas è terminata.
Ora dobbiamo modificare Liferay per recepire queste configurazioni.
Creiamo un ext plugin e aggiungiamo alla cartella ext-lib/portal
cas-client-core-3.4.1.jar (la versione utlizzata dal cas 5.0.4, rinominandola il cas-client-core.jar)
cas-client-integration-tomcat-v7-3.4.1.jar
Facciamo override del cas filter modificando il metoto
protected TicketValidator getTicketValidator(long companyId)
throws Exception {
TicketValidator ticketValidator = _ticketValidators.get(companyId);
if (ticketValidator != null) {
return ticketValidator;
}
String serverName = PrefsPropsUtil.getString(companyId,
PropsKeys.CAS_SERVER_NAME, PropsValues.CAS_SERVER_NAME);
String serverUrl = PrefsPropsUtil.getString(companyId,
PropsKeys.CAS_SERVER_URL, PropsValues.CAS_SERVER_URL);
String loginUrl = PrefsPropsUtil.getString(companyId,
PropsKeys.CAS_LOGIN_URL, PropsValues.CAS_LOGIN_URL);
Cas30ProxyTicketValidator cas30ProxyTicketValidator = new Cas30ProxyTicketValidator(
serverUrl);
Map<String, String> parameters = new HashMap<String, String>();
parameters.put("serverName", serverName);
parameters.put("casServerUrlPrefix", serverUrl);
parameters.put("casServerLoginUrl", loginUrl);
parameters.put("redirectAfterValidation", "false");
cas30ProxyTicketValidator.setCustomParameters(parameters);
_ticketValidators.put(companyId, cas30ProxyTicketValidator);
return cas30ProxyTicketValidator;
}
in modo da ulizzare Cas30ProxyTicketValidator
invece di Cas20ProxyTicketValidator
. In questo modo anche Liferay utilizzerà il protocollo CAS3 per la verifica del ticket.
Modifichiamo il processFilter
dopo la parte di validazione
Assertion assertion = ticketValidator.validate(ticket, serviceUrl);
if (assertion != null) {
AttributePrincipal attributePrincipal = assertion
.getPrincipal();
login = attributePrincipal.getName();
if (_log.isInfoEnabled()) {
_log.info("Cas Login string [" + login + "]");
}
//in questa mappa sono contenuti tutti gli attributi propagati da cas
Map<String, Object> attributes = attributePrincipal
.getAttributes();
if (_log.isInfoEnabled()) {
_log.info("Cas attributes for social login" + attributes);
}
//nel caso di una login direttamente da cas e non da social network
if (Validator.isEmailAddress(login)) {
session.setAttribute(WebKeys.CAS_LOGIN, login);
} else {
String[] casProfile = StringUtil.split(login,
StringPool.POUND);
String className = casProfile[0];
String id = casProfile[1];
ProfileBean profileBean = new ProfileBean();
String accessToken = MapUtil.getString(attributes,
CasConstants.ACCESS_TOKEN);
profileBean.setAccessToken(accessToken);
profileBean.setId(id);
if (StringUtil.equalsIgnoreCase(className,
CasConstants.FACEBOOK_PAC4J_CLASSNAME)) {
String emailAddress = MapUtil.getString(attributes,
CasConstants.FACEBOOK_EMAIL);
String firstName = MapUtil.getString(attributes,
CasConstants.FACEBOOK_FIRST_NAME);
String lastName = MapUtil.getString(attributes,
CasConstants.FACEBOOK_LAST_NAME);
String gender = MapUtil.getString(attributes,
CasConstants.FACEBOOK_GENDER);
String locale = MapUtil.getString(attributes,
CasConstants.FACEBOOK_LOCALE);
profileBean.setEmailAddress(emailAddress);
profileBean.setFirstName(firstName);
profileBean.setLastName(lastName);
profileBean.setGender(gender);
profileBean.setLocale(locale);
profileBean.setType(CasConstants.FACEBOOK_TYPE);
} else if (StringUtil.equalsIgnoreCase(className,
CasConstants.LINKEDIN_PAC4J_CLASSNAME)) {
String emailAddress = MapUtil.getString(attributes,
CasConstants.LINKEDIN_EMAIL);
String firstName = MapUtil.getString(attributes,
CasConstants.LINKEDIN_FIRST_NAME);
String lastName = MapUtil.getString(attributes,
CasConstants.LINKEDIN_LAST_NAME);
profileBean.setEmailAddress(emailAddress);
profileBean.setFirstName(firstName);
profileBean.setLastName(lastName);
profileBean.setType(CasConstants.LINKEDIN_TYPE);
}
session.setAttribute(CasConstants.PROFILE_BEAN, profileBean);
}
In questo modo recuperiamo gli attributi propagati dal cas, popoliamo un oggetto che verrà messo in sessione e che sarà utilizzato dalle classi di autologin per creare/loggare l'utente.
Di seguito un'implementazione della FacebookAutoLogin
@Override
protected String[] doLogin(HttpServletRequest request,
HttpServletResponse response) throws Exception {
HttpSession session = request.getSession();
ProfileBean profileBean = (ProfileBean) session
.getAttribute(CasConstants.PROFILE_BEAN);
String[] credentials = new String[3];
if (Validator.isNotNull(profileBean) && profileBean.isFacebook()) {
long companyId = PortalUtil.getCompanyId(request);
User user = getUser(session, companyId);
if (user == null) {
user = _addUser(companyId, profileBean);
}
credentials[0] = String.valueOf(user.getUserId());
credentials[1] = user.getPassword();
credentials[2] = Boolean.FALSE.toString();
}
return credentials;
}
protected User getUser(HttpSession session, long companyId)
throws PortalException, SystemException {
ProfileBean profileBean = (ProfileBean) session
.getAttribute(CasConstants.PROFILE_BEAN);
if (Validator.isNotNull(profileBean.getEmailAddress())) {
session.removeAttribute(CasConstants.PROFILE_BEAN);
return UserLocalServiceUtil.fetchUserByEmailAddress(companyId,
profileBean.getEmailAddress());
} else {
long facebookId = GetterUtil.getLong(profileBean.getId());
if (facebookId > 0) {
return UserLocalServiceUtil.fetchUserByFacebookId(companyId,
facebookId);
}
}
return null;
}
private User _addUser(long companyId, ProfileBean profileBean)
throws PortalException, SystemException {
long creatorUserId = 0;
boolean autoPassword = true;
String password1 = StringPool.BLANK;
String password2 = StringPool.BLANK;
boolean autoScreenName = true;
String screenName = StringPool.BLANK;
String emailAddress = profileBean.getEmailAddress();
long facebookId = GetterUtil.getLong(profileBean.getId());
String openId = StringPool.BLANK;
Locale locale = LocaleUtil.getDefault();
String firstName = profileBean.getFirstName();
String middleName = StringPool.BLANK;
String lastName = profileBean.getLastName();
int prefixId = 0;
int suffixId = 0;
boolean male = Validator.equals(profileBean.getGender(), "male");
int birthdayMonth = Calendar.JANUARY;
int birthdayDay = 1;
int birthdayYear = 1970;
String jobTitle = StringPool.BLANK;
long[] groupIds = null;
long[] organizationIds = null;
long[] roleIds = null;
long[] userGroupIds = null;
boolean sendEmail = true;
ServiceContext serviceContext = new ServiceContext();
User user = UserLocalServiceUtil.addUser(creatorUserId, companyId,
autoPassword, password1, password2, autoScreenName, screenName,
emailAddress, facebookId, openId, locale, firstName,
middleName, lastName, prefixId, suffixId, male, birthdayMonth,
birthdayDay, birthdayYear, jobTitle, groupIds, organizationIds,
roleIds, userGroupIds, sendEmail, serviceContext);
user = UserLocalServiceUtil.updateLastLogin(user.getUserId(),
user.getLoginIP());
user = UserLocalServiceUtil
.updatePasswordReset(user.getUserId(), false);
user = UserLocalServiceUtil.updateEmailAddressVerified(
user.getUserId(), true);
return user;
}
Se tutto è andato a buon fine ci trovermo autenticati con il nostro account social su liferay