/* * Copyright (c) 2014. * * BaasBox - info-at-baasbox.com * * Licensed 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 com.baasbox.controllers; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.scribe.model.Token; import play.libs.Json; import play.mvc.BodyParser; import play.mvc.Controller; import play.mvc.Http; import play.mvc.Http.Request; import play.mvc.Result; import play.mvc.With; import com.baasbox.configuration.SocialLoginConfiguration; import com.baasbox.controllers.actions.filters.AdminCredentialWrapFilter; import com.baasbox.controllers.actions.filters.ConnectToDBFilter; import com.baasbox.controllers.actions.filters.UserCredentialWrapFilter; import com.baasbox.dao.UserDao; import com.baasbox.dao.exception.InvalidModelException; import com.baasbox.dao.exception.SqlInjectionException; import com.baasbox.db.DbHelper; import com.baasbox.security.SessionKeys; import com.baasbox.security.SessionTokenProvider; import com.baasbox.service.logging.BaasBoxLogger; import com.baasbox.service.sociallogin.BaasBoxSocialException; import com.baasbox.service.sociallogin.BaasBoxSocialTokenValidationException; import com.baasbox.service.sociallogin.SocialLoginService; import com.baasbox.service.sociallogin.UnsupportedSocialNetworkException; import com.baasbox.service.sociallogin.UserInfo; import com.baasbox.service.user.UserService; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.ImmutableMap; import com.orientechnologies.orient.core.record.impl.ODocument; public class Social extends Controller{ private static final String OAUTH_TOKEN="oauth_token"; private static final String OAUTH_SECRET="oauth_secret"; @With ({AdminCredentialWrapFilter.class, ConnectToDBFilter.class}) @BodyParser.Of(BodyParser.Json.class) public static Result authorizationUrl(String socialNetwork){ String keyFormat = socialNetwork.toUpperCase()+"_ENABLED"; Boolean enabled = SocialLoginConfiguration.valueOf(keyFormat).getValueAsBoolean(); if(enabled==null || enabled == false){ return badRequest("Social login for "+socialNetwork+" is not enabled"); }else{ SocialLoginService sc = SocialLoginService.by(socialNetwork,(String)ctx().args.get("appcode")); return ok("{\"url\":\""+sc.getAuthorizationURL(session())+"\"}"); } } /** * This method is a common callback for all oauth * providers.It isn't annotated with a Filter because * social networks callback requests couldn't pass the * auth headers needed by baasbox. * @param socialNetwork * @return */ public static Result callback(String socialNetwork){ try{ SocialLoginService sc = SocialLoginService.by(socialNetwork,(String)ctx().args.get("appcode")); Token t = sc.requestAccessToken(request(),session()); return ok("{\""+OAUTH_TOKEN+"\":\""+t.getToken()+"\",\""+OAUTH_SECRET+"\":\""+t.getSecret()+"\"}"); }catch (UnsupportedSocialNetworkException e){ return badRequest(ExceptionUtils.getMessage(e)); }catch (java.lang.IllegalArgumentException e){ return badRequest(ExceptionUtils.getMessage(e)); } } private static Token extractOAuthTokensFromRequest(Request r){ //issue #217: "oauth_token" parameter should be moved to request body in Social Login APIs Http.RequestBody body = request().body(); JsonNode bodyJson= body.asJson(); if (BaasBoxLogger.isDebugEnabled()) BaasBoxLogger.debug("signUp bodyJson: " + bodyJson); String authToken = null; String authSecret = null; if (bodyJson.has(OAUTH_TOKEN)) authToken = bodyJson.findValuesAsText(OAUTH_TOKEN).get(0); if (bodyJson.has(OAUTH_SECRET)) authSecret = bodyJson.findValuesAsText(OAUTH_SECRET).get(0); //NOTE: to maintain compatibility with previous versions, we leave the option to use QueryStrings if (StringUtils.isEmpty(authToken)) authToken = request().getQueryString(OAUTH_TOKEN); if (StringUtils.isEmpty(authSecret)) authSecret = request().getQueryString(OAUTH_SECRET); if(StringUtils.isEmpty(authToken) || StringUtils.isEmpty(authSecret)){ return null; } return new Token(authToken,authSecret); } /** * Login the user through socialnetwork specified * * An oauth_token and oauth_secret provided by oauth steps * are mandatory * @param socialNetwork the social network name (facebook,google) * @return 200 status code with the X-BB-SESSION token for further calls */ @With ({AdminCredentialWrapFilter.class, ConnectToDBFilter.class}) public static Result loginWith(String socialNetwork){ String appcode = (String)ctx().args.get("appcode"); //after this call, db connection is lost! SocialLoginService sc = SocialLoginService.by(socialNetwork,appcode); Token t =extractOAuthTokensFromRequest(request()); if(t==null){ return badRequest(String.format("Both %s and %s should be specified as query parameters or in the json body",OAUTH_TOKEN,OAUTH_SECRET)); } UserInfo result=null; try { if(sc.validationRequest(t.getToken())){ result = sc.getUserInfo(t); }else{ return badRequest("Provided token is not valid"); } } catch (BaasBoxSocialException e1) { return badRequest(e1.getError()); }catch (BaasBoxSocialTokenValidationException e2) { return badRequest("Unable to validate provided token"); } if (BaasBoxLogger.isDebugEnabled()) BaasBoxLogger.debug("UserInfo received: " + result.toString()); result.setFrom(socialNetwork); result.setToken(t.getToken()); //Setting token as secret for one-token only social networks result.setSecret(t.getSecret()!=null && StringUtils.isNotEmpty(t.getSecret())?t.getSecret():t.getToken()); UserDao userDao = UserDao.getInstance(); ODocument existingUser = null; try{ existingUser = userDao.getBySocialUserId(result); }catch(SqlInjectionException sie){ return internalServerError(ExceptionUtils.getMessage(sie)); } if(existingUser!=null){ String username = null; try { username = UserService.getUsernameByProfile(existingUser); if(username==null){ throw new InvalidModelException("username for profile is null"); } } catch (InvalidModelException e) { internalServerError("unable to login with "+socialNetwork+" : "+ExceptionUtils.getMessage(e)); } String password = UserService.generateFakeUserPassword(username, (Date)existingUser.field(UserDao.USER_SIGNUP_DATE)); ImmutableMap<SessionKeys, ? extends Object> sessionObject = SessionTokenProvider.getSessionTokenProvider().setSession(appcode,username, password); response().setHeader(SessionKeys.TOKEN.toString(), (String) sessionObject.get(SessionKeys.TOKEN)); ObjectNode on = Json.newObject(); if(existingUser!=null){ on = (ObjectNode)Json.parse( User.prepareResponseToJson(existingUser)); } on.put(SessionKeys.TOKEN.toString(), (String) sessionObject.get(SessionKeys.TOKEN)); return ok(on); }else{ if (BaasBoxLogger.isDebugEnabled()) BaasBoxLogger.debug("User does not exists with tokens...trying to create"); String username = UUID.randomUUID().toString(); Date signupDate = new Date(); try{ String password = UserService.generateFakeUserPassword(username, signupDate); JsonNode privateData = null; if(result.getAdditionalData()!=null && !result.getAdditionalData().isEmpty()){ privateData = Json.toJson(result.getAdditionalData()); } UserService.signUp(username, password, signupDate, null, privateData, null, null,true); ODocument profile=UserService.getUserProfilebyUsername(username); UserService.addSocialLoginTokens(profile,result); ImmutableMap<SessionKeys, ? extends Object> sessionObject = SessionTokenProvider.getSessionTokenProvider().setSession(appcode, username, password); response().setHeader(SessionKeys.TOKEN.toString(), (String) sessionObject.get(SessionKeys.TOKEN)); ObjectNode on = Json.newObject(); if(profile!=null){ on = (ObjectNode)Json.parse( User.prepareResponseToJson(profile)); } on.put(SessionKeys.TOKEN.toString(), (String) sessionObject.get(SessionKeys.TOKEN)); return ok(on); }catch(Exception uaee){ return internalServerError(ExceptionUtils.getMessage(uaee)); } } } /** * Returns for the current user the linked accounts to external * social providers. * * * @return a json representation of the list of connected social networks * 404 if no social networks are connected */ @With({UserCredentialWrapFilter.class, ConnectToDBFilter.class}) public static Result socialLogins(){ try { ODocument user = UserService.getCurrentUser(); Map<String,ODocument> logins = user.field(UserDao.ATTRIBUTES_SYSTEM+"."+UserDao.SOCIAL_LOGIN_INFO); if(logins==null || logins.isEmpty()){ return notFound(); }else{ List<UserInfo> result = new ArrayList<UserInfo>(); for (ODocument d : logins.values()) { UserInfo i = UserInfo.fromJson(d.toJSON()); result.add(i); } return ok(Json.toJson(result)); } }catch(Exception e){ return internalServerError(ExceptionUtils.getMessage(e)); } } /** * Unlink given social network from current user. * In case that the user was generated by any social network and * at the moment of the unlink the user has only one social network connected * the controller will throw an Exception with a clear message. * Otherwise a 200 code will be returned * @param socialNetwork * @return * @throws SqlInjectionException */ @With ({UserCredentialWrapFilter.class, ConnectToDBFilter.class}) public static Result unlink(String socialNetwork) throws SqlInjectionException{ ODocument user = null; try{ user = UserService.getCurrentUser(); }catch(Exception e){ internalServerError(ExceptionUtils.getMessage(e)); } Map<String,ODocument> logins = user.field(UserDao.ATTRIBUTES_SYSTEM+"."+UserDao.SOCIAL_LOGIN_INFO); if(logins==null || logins.isEmpty() || !logins.containsKey(socialNetwork) || logins.get(socialNetwork)==null){ return notFound("User's account is not linked with "+ StringUtils.capitalize(socialNetwork)); }else{ boolean generated = UserService.isSocialAccount(DbHelper.getCurrentUserNameFromConnection()); if(logins.size()==1 && generated){ return internalServerError("User's account can't be unlinked."); }else{ try{ UserService.removeSocialLoginTokens(user,socialNetwork); return ok(); }catch(Exception e){ return internalServerError(ExceptionUtils.getMessage(e)); } } } } /** * links current user with specified social network param * * In case the token obtained by the service is already existing in the database * for another user an exception is raised * @param socialNetwork the social network to be linked to * @return a 200 code if the link is correctly generated * * */ @With ({UserCredentialWrapFilter.class, ConnectToDBFilter.class}) public static Result linkWith(String socialNetwork){ //issue #217: "oauth_token" parameter should be moved to request body in Social Login APIs Token t = extractOAuthTokensFromRequest(request()); if(t==null){ return badRequest("Both '"+OAUTH_TOKEN+"' and '"+OAUTH_SECRET+"' should be specified."); } String appcode = (String)ctx().args.get("appcode"); SocialLoginService sc = SocialLoginService.by(socialNetwork,appcode); UserInfo result=null; try { if(sc.validationRequest(t.getToken())){ result = sc.getUserInfo(t); }else{ return badRequest("Provided token is not valid."); } } catch (BaasBoxSocialException e1) { return badRequest(e1.getError()); } catch (BaasBoxSocialTokenValidationException e2) { return badRequest("Unable to validate provided token."); } result.setFrom(socialNetwork); result.setToken(t.getToken()); //Setting token as secret for one-token only social networks result.setSecret(t.getSecret()!=null && StringUtils.isNotEmpty(t.getSecret())?t.getSecret():t.getToken()); ODocument user; try { user = UserService.getCurrentUser(); ODocument other = UserDao.getInstance().getBySocialUserId(result); boolean sameUser = other!=null && other.getIdentity().equals(user.getIdentity()); if(other==null || !sameUser){ UserService.addSocialLoginTokens(user, result); }else{ internalServerError("A user with this token already exists and it's not the current user."); } return ok(); } catch (SqlInjectionException e) { return internalServerError(ExceptionUtils.getMessage(e)); } } }