/* * Copyright (C) 2008-2023, 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 . */ package com.juick.www.controllers; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.scribejava.apis.*; 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.ext.facebook.User; import com.juick.model.ext.vk.UsersResponse; import com.juick.service.EmailService; import com.juick.service.TelegramService; import com.juick.service.UserService; import com.juick.service.security.entities.JuickUser; import com.juick.util.HttpBadRequestException; import jakarta.annotation.PostConstruct; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; 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.RandomStringUtils; 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.scheduling.annotation.Scheduled; import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.springframework.web.util.UriComponentsBuilder; import javax.inject.Inject; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Optional; 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); public static final String AUTH_ERROR = "SocialLogin.AuthenticationError"; @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 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 twitterBuilder; private OAuth20Service facebookAuthService, vkAuthService, appleSignInService; @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; @Value("${apple_app_id:appid}") private String appleApplicationId; @Inject private UserService userService; @Inject private EmailService emailService; @Inject private TelegramService telegramService; @Inject private AppleClientSecretGenerator clientSecretGenerator; @Inject private RememberMeServices rememberMeServices; @PostConstruct public void init() { ServiceBuilder facebookBuilder = new ServiceBuilder(FACEBOOK_APPID); twitterBuilder = new ServiceBuilder(twitterConsumerKey); ServiceBuilder vkBuilder = new ServiceBuilder(VK_APPID); UriComponentsBuilder redirectBuilder = UriComponentsBuilder.fromUriString(baseUri); String facebookRedirectUri = redirectBuilder.replacePath("/_fblogin").build().toUriString(); facebookAuthService = facebookBuilder.apiSecret(FACEBOOK_SECRET).callback(facebookRedirectUri) .defaultScope("email").build(FacebookApi.instance()); vkAuthService = vkBuilder.apiSecret(VK_SECRET).defaultScope("friends,wall,offline,groups").callback(VK_REDIRECT) .build(VkontakteApi.instance()); ServiceBuilder appleSignInBuilder = new ServiceBuilder(appleApplicationId); String appleSignInRedirectUri = redirectBuilder.replacePath("/_apple").build().toUriString(); appleSignInService = appleSignInBuilder.callback(appleSignInRedirectUri).defaultScope("email") .build(new AppleSignInApi(clientSecretGenerator, appleApplicationId)); } @GetMapping("/_fblogin") protected String doFacebookLogin(HttpServletRequest request, @RequestParam(required = false) String code, @RequestParam(required = false) String state, @RequestHeader(value = "referer", required = false) String referer, HttpServletResponse response, HttpSession session) throws IOException, ExecutionException, InterruptedException { if (StringUtils.isBlank(code)) { String fbstate = UUID.randomUUID().toString(); if (StringUtils.isBlank(state)) { state = Optional.ofNullable(referer).orElse("https://juick.com/"); } userService.addFacebookState(fbstate, state); return "redirect:" + facebookAuthService.getAuthorizationUrl(fbstate); } String redirectUrl = userService.verifyFacebookState(state); if (StringUtils.isEmpty(redirectUrl)) { logger.error("state is missing"); throw new HttpBadRequestException(); } OAuth2AccessToken token = facebookAuthService.getAccessToken(code); final OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://graph.facebook.com/me?fields=id,name,link,verified,email"); facebookAuthService.signRequest(token, meRequest); String graph = facebookAuthService.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.id(), 0); if (fbID == 0 || StringUtils.isBlank(fb.name())) { logger.error("Missing required fields, id: {}, name: {}", fbID, fb.name()); throw new HttpBadRequestException(); } Optional existingFacebookUser = userService.getUserByFacebookId(fbID); if (existingFacebookUser.isPresent()) { if (!userService.updateFacebookUser(fbID, token.getAccessToken(), fb.name())) { logger.error("error updating facebook user, id: {}, token: {}", fbID, token.getAccessToken()); throw new HttpBadRequestException(); } if (StringUtils.isNotEmpty(fb.email())) { logger.info("found {} for facebook user {}", fb.email(), fb.name()); Optional newFacebookUser = userService.getUserByFacebookId(fbID); if (!emailService.getEmails(newFacebookUser.get().getUid(), false).contains(fb.email())) { emailService.addEmail(newFacebookUser.get().getUid(), fb.email()); } } if (!existingFacebookUser.get().isBanned()) { Cookie c = new Cookie("hash", userService.getHashByUID(existingFacebookUser.get().getUid())); c.setMaxAge(50 * 24 * 60 * 60); response.addCookie(c); return "redirect:" + redirectUrl; } else { session.setAttribute(SocialLogin.AUTH_ERROR, "User is disabled"); return "redirect:/login"; } } else { if (!userService.createFacebookUser(fbID, state, token.getAccessToken(), fb.name())) { throw new HttpBadRequestException(); } return "redirect:/signup?type=fb&hash=" + state; } } @GetMapping("/_twitter") protected void doTwitterLogin(com.juick.model.User user, 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(); } } 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.ext.twitter.User twitterUser = jsonMapper.readValue( oAuthService.execute(oAuthRequest).getBody(), com.juick.model.ext.twitter.User.class); if (userService.linkTwitterAccount(user, accessToken.getToken(), accessToken.getTokenSecret(), twitterUser.screenName())) { 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(@RequestParam(required = false) String code, @RequestParam(required = false) String state, @RequestHeader(value = "referer", required = false) String referer, @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); return "redirect:" + vkAuthService.getAuthorizationUrl(vkstate); } if (StringUtils.isBlank(vkstate) || !vkstate.equals(state)) { throw new HttpBadRequestException(); } else { Cookie c = new Cookie("vkstate", "-"); c.setMaxAge(0); response.addCookie(c); } OAuth2AccessToken token = vkAuthService.getAccessToken(code); OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.131"); vkAuthService.signRequest(token, meRequest); Response vkResponse = vkAuthService.execute(meRequest); if (vkResponse.isSuccessful()) { String graph = vkResponse.getBody(); com.juick.model.ext.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).users().stream() .findFirst().orElseThrow(HttpBadRequestException::new); String vkName = jsonUser.firstName() + " " + jsonUser.lastName(); String vkLink = jsonUser.screenName(); if (vkName.length() == 1 || StringUtils.isBlank(vkLink)) { logger.error("vk user error"); throw new HttpBadRequestException(); } long vkID = NumberUtils.toLong(jsonUser.id(), 0); int uid = userService.getUIDbyVKID(vkID); if (uid > 0) { userService.updateVkUser(vkID, token.getAccessToken(), vkName, vkLink); Cookie c = new Cookie("hash", userService.getHashByUID(uid)); c.setMaxAge(50 * 24 * 60 * 60); response.addCookie(c); return "redirect:/" + Optional.ofNullable(referer).orElse(StringUtils.EMPTY); } else { String loginhash = UUID.randomUUID().toString(); if (!userService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) { logger.error("create vk user error"); throw new HttpBadRequestException(); } return "redirect:/signup?type=vk&hash=" + loginhash; } } else { logger.error("vk error {}: {}", vkResponse.getCode(), vkResponse.getBody()); throw new HttpBadRequestException(); } } @GetMapping("/_tglogin") public String doDurovLogin(@RequestParam Map params, @RequestParam String hash, @RequestHeader(value = "referer", required = false) String referer, HttpServletRequest request, 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")); byte[] secretKey = DigestUtils.sha256(telegramToken); String resultString = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secretKey).hmacHex(dataCheckString); if (hash.equals(resultString)) { long tgUser = Long.parseLong(params.get("id")); var user = userService.getUserByTelegramId(tgUser); if (user.isPresent()) { var authentication = new RememberMeAuthenticationToken( ((AbstractRememberMeServices) rememberMeServices).getKey(), new JuickUser(user.get()), JuickUser.USER_AUTHORITY); SecurityContextHolder.getContext().setAuthentication(authentication); rememberMeServices.loginSuccess(request, response, authentication); return "redirect:" + Optional.ofNullable(referer).orElse(StringUtils.EMPTY); } else { String username = StringUtils.defaultString(params.get("username"), params.get("first_name")); List chats = telegramService.getAnonymous(); if (!chats.contains(tgUser)) { logger.info("added chat with {}", username); 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(); } @GetMapping("/_apple") public String doAppleLogin(@RequestParam(required = false) String code, HttpServletResponse response) { if (StringUtils.isBlank(code)) { String state = UUID.randomUUID().toString(); Cookie c = new Cookie("astate", state); response.addCookie(c); return "redirect:" + appleSignInService.getAuthorizationUrl(state); } throw new HttpBadRequestException(); } @PostMapping("/_apple") public String doVerifyAppleResponse(HttpServletRequest request, HttpServletResponse response, @RequestParam Map body, HttpSession session) throws InterruptedException, ExecutionException, IOException { OAuth2AccessToken token = appleSignInService.getAccessToken(body.get("code")); var jsonNode = jsonMapper.readTree(token.getRawResponse()); var idToken = jsonNode.get("id_token").textValue(); logger.info("Token: {}", idToken); AppleSignInApi api = (AppleSignInApi) appleSignInService.getApi(); var email = api.validateToken(idToken); if (email.isPresent()) { com.juick.model.User user = userService.getUserByEmail(email.get()); if (!user.isAnonymous()) { if (!user.isBanned()) { Cookie c = new Cookie("hash", userService.getHashByUID(user.getUid())); c.setMaxAge(50 * 24 * 60 * 60); response.addCookie(c); return "redirect:/"; } else { session.setAttribute(SocialLogin.AUTH_ERROR, "User is disabled"); return "redirect:/login"; } } else { String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); emailService.addVerificationCode(null, email.get(), verificationCode); return "redirect:/signup?type=email&hash=" + verificationCode; } } throw new HttpBadRequestException(); } @Scheduled(fixedRate = 3600000) public void updatePremium() { userService.getVkTokens(List.of()) .forEach(vkUser -> { var userId = userService.getUIDbyVKID(Long.parseLong(vkUser.getLeft())); if (userId > 0) { OAuth2AccessToken token = new OAuth2AccessToken(vkUser.getRight()); OAuthRequest donRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/donut.isDon?owner_id=-67669480&v=5.131"); vkAuthService.signRequest(token, donRequest); try { Response vkResponse = vkAuthService.execute(donRequest); if (vkResponse.isSuccessful()) { logger.info(vkResponse.getBody()); var response = jsonMapper.readTree(vkResponse.getBody()); if (response.has("response")) { var isDon = response.get("response").intValue() > 0; logger.info("{} is Don: {}", vkUser.getLeft(), isDon); userService.setPremium(userId, isDon); } else { // token is expired or does not have "groups" permissions userService.updateVkToken(userId, ""); } } } catch (Exception e) { logger.error("Don request error", e); } } }); } }