/* * Copyright (c) 2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.wso2.carbon.identity.application.authenticator.social.facebook; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.oltu.oauth2.client.request.OAuthClientRequest; import org.apache.oltu.oauth2.client.response.OAuthAuthzResponse; import org.apache.oltu.oauth2.common.exception.OAuthProblemException; import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.apache.oltu.oauth2.common.utils.JSONUtils; import org.wso2.carbon.identity.application.authentication.framework.AbstractApplicationAuthenticator; import org.wso2.carbon.identity.application.authentication.framework.FederatedApplicationAuthenticator; import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext; import org.wso2.carbon.identity.application.authentication.framework.exception.ApplicationAuthenticatorException; import org.wso2.carbon.identity.application.authentication.framework.exception.AuthenticationFailedException; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.model.ClaimMapping; import org.wso2.carbon.identity.application.common.util.IdentityApplicationConstants; import org.wso2.carbon.identity.base.IdentityConstants; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.core.util.IdentityIOStreamUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; import java.util.Arrays; import java.util.HashMap; import java.util.Map; public class FacebookAuthenticator extends AbstractApplicationAuthenticator implements FederatedApplicationAuthenticator { private static final long serialVersionUID = -4844100162196896194L; private static final Log log = LogFactory.getLog(FacebookAuthenticator.class); private String tokenEndpoint; private String oAuthEndpoint; private String userInfoEndpoint; /** * Initiate tokenEndpoint */ private void initTokenEndpoint() { this.tokenEndpoint = getAuthenticatorConfig().getParameterMap().get(FacebookAuthenticatorConstants .FB_TOKEN_URL); if (StringUtils.isBlank(this.tokenEndpoint)) { this.tokenEndpoint = IdentityApplicationConstants.FB_TOKEN_URL; } } /** * Initiate authorization server endpoint */ private void initOAuthEndpoint() { this.oAuthEndpoint = getAuthenticatorConfig().getParameterMap().get(FacebookAuthenticatorConstants .FB_AUTHZ_URL); if (StringUtils.isBlank(this.oAuthEndpoint)) { this.oAuthEndpoint = IdentityApplicationConstants.FB_AUTHZ_URL; } } /** * Initiate userInfoEndpoint */ private void initUserInfoEndPoint() { this.userInfoEndpoint = getAuthenticatorConfig().getParameterMap().get(FacebookAuthenticatorConstants .FB_USER_INFO_URL); if (StringUtils.isBlank(this.userInfoEndpoint)) { this.userInfoEndpoint = IdentityApplicationConstants.FB_USER_INFO_URL; } } /** * Get the tokenEndpoint. * @return tokenEndpoint */ private String getTokenEndpoint() { if (StringUtils.isBlank(this.tokenEndpoint)) { initTokenEndpoint(); } return this.tokenEndpoint; } /** * Get the oAuthEndpoint. * @return oAuthEndpoint */ private String getAuthorizationServerEndpoint() { if (StringUtils.isBlank(this.oAuthEndpoint)) { initOAuthEndpoint(); } return this.oAuthEndpoint; } /** * Get the userInfoEndpoint. * @return userInfoEndpoint */ private String getUserInfoEndpoint() { if (StringUtils.isBlank(this.userInfoEndpoint)) { initUserInfoEndPoint(); } return this.userInfoEndpoint; } @Override public boolean canHandle(HttpServletRequest request) { log.trace("Inside FacebookAuthenticator.canHandle()"); if (request.getParameter(FacebookAuthenticatorConstants.OAUTH2_GRANT_TYPE_CODE) != null && request.getParameter(FacebookAuthenticatorConstants.OAUTH2_PARAM_STATE) != null && FacebookAuthenticatorConstants.FACEBOOK_LOGIN_TYPE.equals(getLoginType(request))) { return true; } return false; } @Override protected void initiateAuthenticationRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException { try { Map<String, String> authenticatorProperties = context.getAuthenticatorProperties(); String clientId = authenticatorProperties.get(FacebookAuthenticatorConstants.CLIENT_ID); String authorizationEP = getAuthorizationServerEndpoint(); String scope = authenticatorProperties.get(FacebookAuthenticatorConstants.SCOPE); if (StringUtils.isEmpty(scope)) { scope = FacebookAuthenticatorConstants.EMAIL; } String callbackUrl = IdentityUtil.getServerURL(FrameworkConstants.COMMONAUTH, true, true); String state = context.getContextIdentifier() + "," + FacebookAuthenticatorConstants.FACEBOOK_LOGIN_TYPE; OAuthClientRequest authzRequest = OAuthClientRequest.authorizationLocation(authorizationEP) .setClientId(clientId) .setRedirectURI(callbackUrl) .setResponseType(FacebookAuthenticatorConstants.OAUTH2_GRANT_TYPE_CODE) .setScope(scope).setState(state) .buildQueryMessage(); response.sendRedirect(authzRequest.getLocationUri()); } catch (IOException e) { log.error("Exception while sending to the login page.", e); throw new AuthenticationFailedException(e.getMessage(), e); } catch (OAuthSystemException e) { log.error("Exception while building authorization code request.", e); throw new AuthenticationFailedException(e.getMessage(), e); } return; } @Override protected void processAuthenticationResponse(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException { log.trace("Inside FacebookAuthenticator.authenticate()"); try { Map<String, String> authenticatorProperties = context.getAuthenticatorProperties(); String clientId = authenticatorProperties.get(FacebookAuthenticatorConstants.CLIENT_ID); String clientSecret = authenticatorProperties.get(FacebookAuthenticatorConstants.CLIENT_SECRET); String userInfoFields = authenticatorProperties.get(FacebookAuthenticatorConstants.USER_INFO_FIELDS); String tokenEndPoint = getTokenEndpoint(); String fbAuthUserInfoUrl = getUserInfoEndpoint(); String callbackUrl = IdentityUtil.getServerURL(FrameworkConstants.COMMONAUTH, true, true); String code = getAuthorizationCode(request); String token = getToken(tokenEndPoint, clientId, clientSecret, callbackUrl, code); if (!StringUtils.isBlank(userInfoFields)) { if (context.getExternalIdP().getIdentityProvider().getClaimConfig() != null && !StringUtils.isBlank (context.getExternalIdP().getIdentityProvider().getClaimConfig().getUserClaimURI())) { String userClaimUri = context.getExternalIdP().getIdentityProvider().getClaimConfig() .getUserClaimURI(); if (!Arrays.asList(userInfoFields.split(",")).contains(userClaimUri)) { userInfoFields += ("," + userClaimUri); } } else { if (!Arrays.asList(userInfoFields.split(",")).contains(FacebookAuthenticatorConstants .DEFAULT_USER_IDENTIFIER)) { userInfoFields += ("," + FacebookAuthenticatorConstants.DEFAULT_USER_IDENTIFIER); } } } Map<String, Object> userInfoJson = getUserInfoJson(fbAuthUserInfoUrl, userInfoFields, token); buildClaims(context, userInfoJson); } catch (ApplicationAuthenticatorException e) { log.error("Failed to process Facebook Connect response.", e); throw new AuthenticationFailedException(e.getMessage(), e); } } private String getAuthorizationCode(HttpServletRequest request) throws ApplicationAuthenticatorException { OAuthAuthzResponse authzResponse; try { authzResponse = OAuthAuthzResponse.oauthCodeAuthzResponse(request); return authzResponse.getCode(); } catch (OAuthProblemException e) { throw new ApplicationAuthenticatorException("Exception while reading authorization code.", e); } } private String getToken(String tokenEndPoint, String clientId, String clientSecret, String callbackurl, String code) throws ApplicationAuthenticatorException { OAuthClientRequest tokenRequest = null; String token = null; try { tokenRequest = buidTokenRequest(tokenEndPoint, clientId, clientSecret, callbackurl, code); token = sendRequest(tokenRequest.getLocationUri()); if (token.startsWith("{")) { throw new ApplicationAuthenticatorException("Received access token is invalid."); } } catch (MalformedURLException e) { if (log.isDebugEnabled()) { log.debug("URL : " + tokenRequest.getLocationUri()); } throw new ApplicationAuthenticatorException( "MalformedURLException while sending access token request.", e); } catch (IOException e) { throw new ApplicationAuthenticatorException("IOException while sending access token request.", e); } return token; } private OAuthClientRequest buidTokenRequest( String tokenEndPoint, String clientId, String clientSecret, String callbackurl, String code) throws ApplicationAuthenticatorException { OAuthClientRequest tokenRequest = null; try { tokenRequest = OAuthClientRequest.tokenLocation(tokenEndPoint).setClientId(clientId) .setClientSecret(clientSecret) .setRedirectURI(callbackurl).setCode(code) .buildQueryMessage(); } catch (OAuthSystemException e) { throw new ApplicationAuthenticatorException("Exception while building access token request.", e); } return tokenRequest; } private String getUserInfoString(String fbAuthUserInfoUrl, String userInfoFields, String token) throws ApplicationAuthenticatorException { String userInfoString; try { if (StringUtils.isBlank(userInfoFields)) { userInfoString = sendRequest(String.format("%s?%s", fbAuthUserInfoUrl, token)); } else { userInfoString = sendRequest(String.format("%s?fields=%s&%s", fbAuthUserInfoUrl, userInfoFields, token)); } } catch (MalformedURLException e) { if (log.isDebugEnabled()) { log.debug("URL : " + fbAuthUserInfoUrl, e); } throw new ApplicationAuthenticatorException( "MalformedURLException while sending user information request.", e); } catch (IOException e) { throw new ApplicationAuthenticatorException( "IOException while sending sending user information request.", e); } return userInfoString; } private void setSubject(AuthenticationContext context, Map<String, Object> jsonObject) throws ApplicationAuthenticatorException { String authenticatedUserId = (String) jsonObject.get(FacebookAuthenticatorConstants.DEFAULT_USER_IDENTIFIER); if (StringUtils.isEmpty(authenticatedUserId)) { throw new ApplicationAuthenticatorException("Authenticated user identifier is empty"); } AuthenticatedUser authenticatedUser = AuthenticatedUser.createFederateAuthenticatedUserFromSubjectIdentifier(authenticatedUserId); context.setSubject(authenticatedUser); } private Map<String, Object> getUserInfoJson(String fbAuthUserInfoUrl, String userInfoFields, String token) throws ApplicationAuthenticatorException { String userInfoString = getUserInfoString(fbAuthUserInfoUrl, userInfoFields, token); if (log.isDebugEnabled() && IdentityUtil.isTokenLoggable(IdentityConstants.IdentityTokens.USER_ID_TOKEN)) { log.debug("UserInfoString : " + userInfoString); } Map<String, Object> jsonObject = JSONUtils.parseJSON(userInfoString); return jsonObject; } public void buildClaims(AuthenticationContext context, Map<String, Object> jsonObject) throws ApplicationAuthenticatorException { if (jsonObject != null) { Map<ClaimMapping, String> claims = new HashMap<ClaimMapping, String>(); for (Map.Entry<String, Object> entry : jsonObject.entrySet()) { claims.put(ClaimMapping.build(entry.getKey(), entry.getKey(), null, false), entry.getValue().toString()); if (log.isDebugEnabled() && IdentityUtil.isTokenLoggable(IdentityConstants.IdentityTokens.USER_CLAIMS)) { log.debug("Adding claim mapping : " + entry.getKey() + " <> " + entry.getKey() + " : " + entry.getValue()); } } if (StringUtils.isBlank(context.getExternalIdP().getIdentityProvider().getClaimConfig().getUserClaimURI())) { context.getExternalIdP().getIdentityProvider().getClaimConfig().setUserClaimURI (FacebookAuthenticatorConstants.EMAIL); } String subjectFromClaims = FrameworkUtils.getFederatedSubjectFromClaims( context.getExternalIdP().getIdentityProvider(), claims); if (subjectFromClaims != null && !subjectFromClaims.isEmpty()) { AuthenticatedUser authenticatedUser = AuthenticatedUser.createFederateAuthenticatedUserFromSubjectIdentifier(subjectFromClaims); context.setSubject(authenticatedUser); } else { setSubject(context, jsonObject); } context.getSubject().setUserAttributes(claims); } else { if (log.isDebugEnabled()) { log.debug("Decoded json object is null"); } throw new ApplicationAuthenticatorException("Decoded json object is null"); } } @Override public String getContextIdentifier(HttpServletRequest request) { log.trace("Inside FacebookAuthenticator.getContextIdentifier()"); String state = request.getParameter(FacebookAuthenticatorConstants.OAUTH2_PARAM_STATE); if (state != null) { return state.split(",")[0]; } else { return null; } } private String sendRequest(String url) throws IOException { BufferedReader in = null; StringBuilder b = new StringBuilder(); try{ URLConnection urlConnection = new URL(url).openConnection(); in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), Charset.forName("utf-8"))); String inputLine = in.readLine(); while (inputLine != null) { b.append(inputLine).append("\n"); inputLine = in.readLine(); } } finally { IdentityIOStreamUtils.closeReader(in); } return b.toString(); } private String getLoginType(HttpServletRequest request) { String state = request.getParameter(FacebookAuthenticatorConstants.OAUTH2_PARAM_STATE); if (state != null) { return state.split(",")[1]; } else { return null; } } @Override public String getFriendlyName() { return "facebook"; } @Override public String getName() { return FacebookAuthenticatorConstants.AUTHENTICATOR_NAME; } }