From 7a2f89266c8f6337e4e81a2fd8488e0f80f4f9bd Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Fri, 3 Apr 2020 23:53:23 +0300 Subject: Reorganize layout and code cleanup --- .../java/com/juick/www/api/ApiSocialLogin.java | 322 +++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 src/main/java/com/juick/www/api/ApiSocialLogin.java (limited to 'src/main/java/com/juick/www/api/ApiSocialLogin.java') diff --git a/src/main/java/com/juick/www/api/ApiSocialLogin.java b/src/main/java/com/juick/www/api/ApiSocialLogin.java new file mode 100644 index 00000000..6499b507 --- /dev/null +++ b/src/main/java/com/juick/www/api/ApiSocialLogin.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2008-2019, 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.server.util.HttpBadRequestException; +import com.juick.service.CrosspostService; +import com.juick.service.EmailService; +import com.juick.service.UserService; +import com.juick.model.ext.vk.UsersResponse; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.proc.BadJOSEException; +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.text.ParseException; +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, ParseException, JOSEException, BadJOSEException { + 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(); + } +} -- cgit v1.2.3