/* * 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 . */ 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.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.google.api.client.googleapis.auth.oauth2.GoogleIdToken; import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.jackson2.JacksonFactory; import com.juick.model.AuthResponse; import com.juick.model.ext.facebook.User; import com.juick.model.ext.vk.UsersResponse; import com.juick.service.CrosspostService; import com.juick.service.EmailService; import com.juick.service.UserService; import com.juick.util.HttpBadRequestException; 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.http.HttpStatus; import org.springframework.http.ResponseEntity; 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 javax.annotation.PostConstruct; import javax.inject.Inject; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.Collections; import java.util.Map; 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 CrosspostService crosspostService; @Inject private UserService userService; @Inject private EmailService emailService; @Inject private AppleClientSecretGenerator clientSecretGenerator; @Inject private Users users; private final HttpTransport transport = new NetHttpTransport(); private final JsonFactory jsonFactory = new JacksonFactory(); private GoogleIdTokenVerifier verifier; @PostConstruct public void init() { ServiceBuilder facebookBuilder = new ServiceBuilder(FACEBOOK_APPID); ServiceBuilder twitterBuilder = new ServiceBuilder(twitterConsumerKey); ServiceBuilder vkBuilder = new ServiceBuilder(VK_APPID); verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory) .setAudience(Collections.singletonList(googleClientId)) .build(); 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(); crosspostService.addFacebookState(fbstate, state); return "redirect:" + facebookAuthService.getAuthorizationUrl(fbstate); } String redirectUrl = crosspostService.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/v3.2/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.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(); } UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl); uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid)); uriComponentsBuilder.queryParam("retpath", redirectUrl); return "redirect:" + uriComponentsBuilder.build().toUriString(); } else { if (!crosspostService.createFacebookUser(fbID, state, token.getAccessToken(), fb.getName())) { if (StringUtils.isNotEmpty(fb.getEmail())) { logger.info("found {} for facebook user {}", fb.getEmail(), fb.getName()); 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("/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(); crosspostService.addVKState(vkstate, state); return "redirect:" + vkAuthService.getAuthorizationUrl(vkstate); } String redirectUrl = crosspostService.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).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) { 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 (!crosspostService.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 ResponseEntity googleSignIn(@RequestParam(name = "idToken") String idTokenString) throws GeneralSecurityException, IOException { logger.info("Token: {}", idTokenString); logger.info("Client: {}", googleClientId); GoogleIdToken idToken = verifier.verify(idTokenString); if (idToken != null) { String email = idToken.getPayload().getEmail(); com.juick.model.User visitor = userService.getUserByEmail(email); if (visitor.isAnonymous()) { String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); emailService.addVerificationCode(null, email, verificationCode); return ResponseEntity.ok(new AuthResponse(null, email, verificationCode)); } else { return ResponseEntity.ok(new AuthResponse(users.getMe(visitor), null, null)); } } return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null); } @ResponseBody @PostMapping("/api/signup") public ResponseEntity 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 ResponseEntity.ok(newUser); } else { return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null); } } @GetMapping("/api/_applelogin") public String doAppleLogin(@RequestParam(required = false) String code, @RequestParam String state) { if (StringUtils.isBlank(code)) { String astate = UUID.randomUUID().toString(); crosspostService.addVKState(astate, state); return "redirect:" + appleSignInService.getAuthorizationUrl(astate); } throw new HttpBadRequestException(); } @PostMapping("/api/_applelogin") public String doVerifyAppleResponse(@RequestParam Map 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(); 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 = crosspostService.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(); } }