/* * Copyright (C) 2008-2020, 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.www.api; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.scribejava.apis.AppleClientSecretGenerator; import com.github.scribejava.apis.AppleSignInApi; import com.github.scribejava.apis.FacebookApi; import com.github.scribejava.apis.GoogleTokenVerifier; import com.github.scribejava.apis.VkontakteApi; import com.github.scribejava.core.builder.ServiceBuilder; import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.model.OAuthRequest; import com.github.scribejava.core.model.Verb; import com.github.scribejava.core.oauth.OAuth20Service; import com.juick.model.AuthResponse; import com.juick.model.ext.facebook.User; import com.juick.model.ext.vk.UsersResponse; import com.juick.service.EmailService; import com.juick.service.UserService; import com.juick.util.HttpBadRequestException; import com.juick.util.HttpForbiddenException; 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.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.util.UriComponentsBuilder; import jakarta.annotation.PostConstruct; import javax.inject.Inject; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; /** * * @author Ugnich Anton */ @Controller public class ApiSocialLogin { private static final Logger logger = LoggerFactory.getLogger(ApiSocialLogin.class); @Value("${facebook_appid:appid}") private String FACEBOOK_APPID; @Value("${facebook_secret:secret}") private String FACEBOOK_SECRET; private static final String FACEBOOK_REDIRECT = "https://api.juick.com/_fblogin"; private static final String VK_REDIRECT = "https://api.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 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("${google_client_id:}") private String googleClientId; @Value("${apple_app_id:appid}") private String appleApplicationId; @Value("${ap_base_uri:http://localhost:8080/}") private String baseUri; @Inject private UserService userService; @Inject private EmailService emailService; @Inject private AppleClientSecretGenerator clientSecretGenerator; @Inject private Users users; @PostConstruct public void init() { ServiceBuilder facebookBuilder = new ServiceBuilder(FACEBOOK_APPID); ServiceBuilder twitterBuilder = new ServiceBuilder(twitterConsumerKey); ServiceBuilder vkBuilder = new ServiceBuilder(VK_APPID); facebookAuthService = facebookBuilder .apiSecret(FACEBOOK_SECRET) .callback(FACEBOOK_REDIRECT) .defaultScope("email") .build(FacebookApi.instance()); vkAuthService = vkBuilder .apiSecret(VK_SECRET) .defaultScope("friends,wall,offline") .callback(VK_REDIRECT) .build(VkontakteApi.instance()); ServiceBuilder appleSignInBuilder = new ServiceBuilder(appleApplicationId); UriComponentsBuilder redirectBuilder = UriComponentsBuilder.fromUriString(baseUri); String appleSignInRedirectUri = redirectBuilder.replacePath("/api/_applelogin").build().toUriString(); appleSignInService = appleSignInBuilder .callback(appleSignInRedirectUri) .defaultScope("email") .build(new AppleSignInApi(clientSecretGenerator)); } @GetMapping("/api/_fblogin") protected String doFacebookLogin(@RequestParam(required = false) String code, @RequestParam(required = false) String state) throws IOException, ExecutionException, InterruptedException { if (StringUtils.isBlank(code)) { String fbstate = UUID.randomUUID().toString(); 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,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<com.juick.model.User> 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()); Integer userId = existingFacebookUser.get().getUid(); if (!emailService.getEmails(userId, false).contains(fb.email())) { emailService.addEmail(userId, fb.email()); } } UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl); uriComponentsBuilder.queryParam("hash", userService.getHashByUID(existingFacebookUser.get().getUid())); uriComponentsBuilder.queryParam("retpath", redirectUrl); return "redirect:" + uriComponentsBuilder.build().toUriString(); } else { if (!userService.createFacebookUser(fbID, state, token.getAccessToken(), fb.name())) { throw new HttpBadRequestException(); } return "redirect:/signup?type=fb&hash=" + state; } } @GetMapping("/api/_vklogin") protected String doVKLogin(@RequestParam(required = false) String code, @RequestParam String state) throws IOException, ExecutionException, InterruptedException { if (StringUtils.isBlank(code)) { String vkstate = UUID.randomUUID().toString(); userService.addVKState(vkstate, state); return "redirect:" + vkAuthService.getAuthorizationUrl(vkstate); } String redirectUrl = userService.verifyVKState(state); if (StringUtils.isBlank(redirectUrl)) { logger.error("state is missing"); throw new HttpBadRequestException(); } OAuth2AccessToken token = vkAuthService.getAccessToken(code); OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.73"); vkAuthService.signRequest(token, meRequest); String graph = vkAuthService.execute(meRequest).getBody(); com.juick.model.ext.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).users().get(0); 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) { UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl); uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid)); uriComponentsBuilder.queryParam("retpath", redirectUrl); return "redirect:" + uriComponentsBuilder.build().toUriString(); } 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; } } @ResponseBody @PostMapping("/api/_google") public AuthResponse googleSignIn(@RequestParam(name = "idToken") String idTokenString) { logger.info("Token: {}", idTokenString); logger.info("Client: {}", googleClientId); Optional<String> verifiedEmail = GoogleTokenVerifier.validateToken(googleClientId, idTokenString); if (verifiedEmail.isPresent()) { String email = verifiedEmail.get(); com.juick.model.User visitor = userService.getUserByEmail(email); if (visitor.isAnonymous()) { String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); emailService.addVerificationCode(null, email, verificationCode); return new AuthResponse(null, email, verificationCode); } else { return new AuthResponse(users.getMe(visitor), null, null); } } throw new HttpForbiddenException(); } @ResponseBody @PostMapping("/api/signup") public com.juick.model.User signupWithEmail(String username, String password, String verificationCode) { if (username.length() < 2 || username.length() > 16 || !username.matches("^[a-zA-Z0-9\\-]+$") || password.length() < 6 || password.length() > 32) { throw new HttpBadRequestException(); } String verifiedEmail = emailService.getEmailByAuthCode(verificationCode); if (StringUtils.isNotEmpty(verifiedEmail)) { com.juick.model.User newUser = userService.createUser(username, password).orElseThrow(HttpBadRequestException::new); emailService.addEmail(newUser.getUid(), verifiedEmail); emailService.deleteAuthCode(verificationCode); return newUser; } else { throw new HttpForbiddenException(); } } @GetMapping("/api/_applelogin") public String doAppleLogin(@RequestParam(required = false) String code, @RequestParam String state) { if (StringUtils.isBlank(code)) { String astate = UUID.randomUUID().toString(); userService.addVKState(astate, state); return "redirect:" + appleSignInService.getAuthorizationUrl(astate); } throw new HttpBadRequestException(); } @PostMapping("/api/_applelogin") public String doVerifyAppleResponse(@RequestParam Map<String, String> body) 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()) { String redirectUrl = userService.verifyVKState(body.get("state")); if (StringUtils.isBlank(redirectUrl)) { logger.error("state is missing"); throw new HttpBadRequestException(); } UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl); uriComponentsBuilder.queryParam("hash", userService.getHashByUID(user.getUid())); uriComponentsBuilder.queryParam("retpath", redirectUrl); return "redirect:" + uriComponentsBuilder.build().toUriString(); } else { String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); emailService.addVerificationCode(null, email.get(), verificationCode); return "redirect:/signup?type=email&hash=" + verificationCode; } } throw new HttpBadRequestException(); } }