/* * Copyright (C) 2008-2017, Juick * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.juick.server.www.controllers; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.scribejava.apis.FacebookApi; import com.github.scribejava.apis.TwitterApi; import com.github.scribejava.apis.VkontakteApi; import com.github.scribejava.core.builder.ServiceBuilder; import com.github.scribejava.core.model.*; import com.github.scribejava.core.oauth.OAuth10aService; import com.github.scribejava.core.oauth.OAuth20Service; import com.juick.model.facebook.User; import com.juick.server.Utils; import com.juick.server.util.HttpBadRequestException; import com.juick.server.util.UserUtils; import com.juick.service.CrosspostService; import com.juick.service.EmailService; import com.juick.service.TelegramService; import com.juick.service.UserService; import com.juick.model.vk.UsersResponse; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.HmacAlgorithms; import org.apache.commons.codec.digest.HmacUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.util.UriComponentsBuilder; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; /** * * @author Ugnich Anton */ @Controller public class SocialLogin { private static final Logger logger = LoggerFactory.getLogger(SocialLogin.class); @Value("${facebook_appid:appid}") private String FACEBOOK_APPID; @Value("${facebook_secret:secret}") private String FACEBOOK_SECRET; @Value("${ap_base_uri:http://localhost:8080/}") private String baseUri; private String facebookRedirectUri; private static final String VK_REDIRECT = "http://juick.com/_vklogin"; private static final String TWITTER_VERIFY_URL = "https://api.twitter.com/1.1/account/verify_credentials.json"; @Inject private ObjectMapper jsonMapper; private ServiceBuilder facebookBuilder, twitterBuilder, vkBuilder; @Value("${twitter_consumer_key:appid}") private String twitterConsumerKey; @Value("${twitter_consumer_secret:secret}") private String twitterConsumerSecret; @Value("${vk_appid:appid}") private String VK_APPID; @Value("${vk_secret:secret}") private String VK_SECRET; @Value("${telegram_token:secret}") private String telegramToken; @Inject private CrosspostService crosspostService; @Inject private UserService userService; @Inject private EmailService emailService; @Inject private TelegramService telegramService; @PostConstruct public void init() { facebookBuilder = new ServiceBuilder(FACEBOOK_APPID); twitterBuilder = new ServiceBuilder(twitterConsumerKey); vkBuilder = new ServiceBuilder(VK_APPID); UriComponentsBuilder facebookRedirectBuilder = UriComponentsBuilder.fromUriString(baseUri); facebookRedirectUri = facebookRedirectBuilder.replacePath("/_fblogin").build().toUriString(); } @GetMapping("/_fblogin") protected String doFacebookLogin(HttpServletRequest request, @RequestParam(required = false) String code, @RequestParam(required = false) String state, HttpServletResponse response) throws IOException, ExecutionException, InterruptedException { if (StringUtils.isBlank(code)) { String fbstate = UUID.randomUUID().toString(); if (StringUtils.isBlank(state)) { state = Utils.getPreviousPageByRequest(request).orElse("https://juick.com/"); } crosspostService.addFacebookState(fbstate, state); OAuth20Service facebookAuthService = facebookBuilder .apiSecret(FACEBOOK_SECRET) .callback(facebookRedirectUri) .scope("email") .state(fbstate) .build(FacebookApi.instance()); return "redirect:" + facebookAuthService.getAuthorizationUrl(); } String redirectUrl = crosspostService.verifyFacebookState(state); if (StringUtils.isEmpty(redirectUrl)) { logger.error("state is missing"); throw new HttpBadRequestException(); } OAuth20Service facebookService = facebookBuilder .apiKey(FACEBOOK_APPID) .apiSecret(FACEBOOK_SECRET) .callback(facebookRedirectUri) .scope("email") .state(state) .build(FacebookApi.instance()); OAuth2AccessToken token = facebookService.getAccessToken(code); final OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://graph.facebook.com/v3.2/me?fields=id,name,link,verified,email"); facebookService.signRequest(token, meRequest); String graph = facebookService.execute(meRequest).getBody(); if (StringUtils.isBlank(graph)) { logger.error("FACEBOOK GRAPH ERROR"); throw new HttpBadRequestException(); } User fb = jsonMapper.readValue(graph, User.class); long fbID = NumberUtils.toLong(fb.getId(), 0); if (fbID == 0 || StringUtils.isBlank(fb.getName())) { logger.error("Missing required fields, id: {}, name: {}", fbID, fb.getName()); throw new HttpBadRequestException(); } int uid = crosspostService.getUIDbyFBID(fbID); if (uid > 0) { if (!crosspostService.updateFacebookUser(fbID, token.getAccessToken(), fb.getName())) { logger.error("error updating facebook user, id: {}, token: {}", fbID, token.getAccessToken()); throw new HttpBadRequestException(); } Cookie c = new Cookie("hash", userService.getHashByUID(uid)); c.setMaxAge(50 * 24 * 60 * 60); response.addCookie(c); return "redirect:" + redirectUrl; } else { if (!crosspostService.createFacebookUser(fbID, state, token.getAccessToken(), fb.getName())) { if (StringUtils.isNotEmpty(fb.getEmail())) { logger.info("found {} for facebook user {}", fb.getEmail()); Integer userId = crosspostService.getUIDbyFBID(fbID); if (!emailService.getEmails(userId, false).contains(fb.getEmail())) { emailService.addEmail(userId, fb.getEmail()); } } logger.info("email not found for facebook user {}", fb.getName()); throw new HttpBadRequestException(); } return "redirect:/signup?type=fb&hash=" + state; } } @GetMapping("/_twitter") protected void doTwitterLogin(HttpServletRequest request, HttpServletResponse response) throws IOException, ExecutionException, InterruptedException { String hash = StringUtils.EMPTY, request_token = StringUtils.EMPTY, request_token_secret = StringUtils.EMPTY; String verifier = request.getParameter("oauth_verifier"); Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if (cookie.getName().equals("hash")) { hash = cookie.getValue(); } if (cookie.getName().equals("request_token")) { request_token = cookie.getValue(); } if (cookie.getName().equals("request_token_secret")) { request_token_secret = cookie.getValue(); } } com.juick.User user = UserUtils.getCurrentUser(); OAuth10aService oAuthService = twitterBuilder .apiSecret(twitterConsumerSecret) .callback("https://juick.com/_twitter") .build(TwitterApi.instance()); if (request_token.isEmpty() && request_token_secret.isEmpty() && (verifier == null || verifier.isEmpty())) { OAuth1RequestToken requestToken = oAuthService.getRequestToken(); String authUrl = oAuthService.getAuthorizationUrl(requestToken); response.addCookie(new Cookie("request_token", requestToken.getToken())); response.addCookie(new Cookie("request_token_secret", requestToken.getTokenSecret())); response.setStatus(HttpServletResponse.SC_FOUND); response.setHeader("Location", authUrl); } else { if (verifier != null && verifier.length() > 0) { OAuth1RequestToken requestToken = new OAuth1RequestToken(request_token, request_token_secret); OAuth1AccessToken accessToken = oAuthService.getAccessToken(requestToken, verifier); OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, TWITTER_VERIFY_URL); oAuthService.signRequest(accessToken, oAuthRequest); com.juick.model.twitter.User twitterUser = jsonMapper.readValue(oAuthService.execute(oAuthRequest).getBody(), com.juick.model.twitter.User.class); if (userService.linkTwitterAccount(user, accessToken.getToken(), accessToken.getTokenSecret(), twitterUser.getScreenName())) { response.setStatus(HttpServletResponse.SC_FOUND); response.setHeader("Location", "http://juick.com/settings"); } else { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } } } @GetMapping("/_vklogin") protected String doVKLogin(HttpServletRequest request, @RequestParam(required = false) String code, @RequestParam(required = false) String state, @CookieValue(required = false) String vkstate, HttpServletResponse response) throws IOException, ExecutionException, InterruptedException { if (StringUtils.isBlank(code)) { vkstate = UUID.randomUUID().toString(); Cookie c = new Cookie("vkstate", vkstate); response.addCookie(c); OAuth20Service vkAuthService = vkBuilder .apiSecret(VK_SECRET) .scope("friends,wall,offline") .state(vkstate) .callback(VK_REDIRECT) .build(VkontakteApi.instance()); return "redirect:" + vkAuthService.getAuthorizationUrl(); } if (StringUtils.isBlank(vkstate) || !vkstate.equals(state)) { throw new HttpBadRequestException(); } else { Cookie c = new Cookie("vkstate", "-"); c.setMaxAge(0); response.addCookie(c); } OAuth20Service vkService = vkBuilder .apiKey(VK_APPID) .apiSecret(VK_SECRET) .build(VkontakteApi.instance()); OAuth2AccessToken token = vkService.getAccessToken(code); OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.73"); vkService.signRequest(token, meRequest); String graph = vkService.execute(meRequest).getBody(); com.juick.model.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0); String vkName = jsonUser.getFirstName() + " " + jsonUser.getLastName(); String vkLink = jsonUser.getScreenName(); if (vkName.length() == 1 || StringUtils.isBlank(vkLink)) { logger.error("vk user error"); throw new HttpBadRequestException(); } Long vkID = NumberUtils.toLong(jsonUser.getId(), 0); int uid = crosspostService.getUIDbyVKID(vkID); if (uid > 0) { Cookie c = new Cookie("hash", userService.getHashByUID(uid)); c.setMaxAge(50 * 24 * 60 * 60); response.addCookie(c); return "redirect:/" + Utils.getPreviousPageByRequest(request).orElse(StringUtils.EMPTY); } else { String loginhash = UUID.randomUUID().toString(); if (!crosspostService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) { logger.error("create vk user error"); throw new HttpBadRequestException(); } return "redirect:/signup?type=vk&hash=" + loginhash; } } @GetMapping("/_tglogin") public String doDurovLogin(HttpServletRequest request, @RequestParam Map<String, String> params, HttpServletResponse response) { String dataCheckString = params.entrySet().stream() .filter(p -> !p.getKey().equals("hash")) .sorted(Map.Entry.comparingByKey()) .map(p -> p.getKey() + "=" + p.getValue()) .collect(Collectors.joining("\n")); String hash = params.get("hash"); byte[] secretKey = DigestUtils.sha256(telegramToken); String resultString = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secretKey).hmacHex(dataCheckString); if (hash.equals(resultString)) { Long tgUser = Long.valueOf(params.get("id")); int uid = telegramService.getUser(tgUser); if (uid > 0) { Cookie c = new Cookie("hash", userService.getHashByUID(uid)); c.setMaxAge(50 * 24 * 60 * 60); response.addCookie(c); return "redirect:/" + Utils.getPreviousPageByRequest(request).orElse(StringUtils.EMPTY); } else { String username = StringUtils.defaultString(params.get("username"), params.get("first_name")); telegramService.createTelegramUser(tgUser, username); return "redirect:/signup?type=durov&hash=" + userService.getSignUpHashByTelegramID(tgUser, username); } } else { logger.warn("invalid tg hash {} for {}", resultString, hash); } throw new HttpBadRequestException(); } }