From bc7f66b3c7dfaa3100e2ea2513448b0ec9a4dab7 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Fri, 20 Dec 2019 17:24:52 +0300 Subject: Apple SignIn API endpoint --- .../java/com/juick/server/api/ApiSocialLogin.java | 174 +++++++++++---------- 1 file changed, 92 insertions(+), 82 deletions(-) (limited to 'src/main/java') diff --git a/src/main/java/com/juick/server/api/ApiSocialLogin.java b/src/main/java/com/juick/server/api/ApiSocialLogin.java index a1b22606..0fa78788 100644 --- a/src/main/java/com/juick/server/api/ApiSocialLogin.java +++ b/src/main/java/com/juick/server/api/ApiSocialLogin.java @@ -17,6 +17,8 @@ package com.juick.server.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; @@ -35,9 +37,20 @@ import com.juick.model.facebook.User; import com.juick.server.util.HttpBadRequestException; 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 com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import net.minidev.json.JSONObject; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; @@ -56,8 +69,11 @@ import org.springframework.web.util.UriComponentsBuilder; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.io.IOException; +import java.net.URL; 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; @@ -79,7 +95,7 @@ public class ApiSocialLogin { 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; + private OAuth20Service facebookAuthService, vkAuthService, appleSignInService; @Value("${twitter_consumer_key:appid}") private String twitterConsumerKey; @@ -89,10 +105,12 @@ public class ApiSocialLogin { private String VK_APPID; @Value("${vk_secret:secret}") private String VK_SECRET; - @Value("${telegram_token:secret}") - private String telegramToken; @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; @@ -101,7 +119,7 @@ public class ApiSocialLogin { @Inject private EmailService emailService; @Inject - private TelegramService telegramService; + private AppleClientSecretGenerator clientSecretGenerator; private final HttpTransport transport = new NetHttpTransport(); private final JsonFactory jsonFactory = new JacksonFactory(); @@ -125,6 +143,13 @@ public class ApiSocialLogin { .defaultScope("friends,wall,offline") .callback(VK_REDIRECT) .build(VkontakteApi.instance()); + ServiceBuilder appleSignInBuilder = new ServiceBuilder(appleApplicationId); + UriComponentsBuilder redirectBuilder = UriComponentsBuilder.fromUriString(baseUri); + String appleSignInRedirectUri = redirectBuilder.replacePath("/_apple").build().toUriString(); + appleSignInService = appleSignInBuilder + .callback(appleSignInRedirectUri) + .defaultScope("email") + .build(new AppleSignInApi(clientSecretGenerator)); } @GetMapping("/api/_fblogin") @@ -180,56 +205,7 @@ public class ApiSocialLogin { } 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("http://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.twitter.User twitterUser = jsonMapper.readValue(oAuthService.execute(oAuthRequest).getBody(), - com.juick.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("/api/_vklogin") protected String doVKLogin(@RequestParam(required = false) String code, @RequestParam String state) throws IOException, ExecutionException, InterruptedException { @@ -309,35 +285,69 @@ public class ApiSocialLogin { return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null); } } - /* - @GetMapping("/_tglogin") - public String doDurovLogin(HttpServletRequest request, - @RequestParam Map 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 Utils.getPreviousPageByRequest(request).orElse("redirect:/"); + @GetMapping("/_apple") + 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("/_apple") + 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(); + +// Create a JWT processor for the access tokens + ConfigurableJWTProcessor jwtProcessor = + new DefaultJWTProcessor<>(); + +// The public RSA keys to validate the signatures will be sourced from the +// OAuth 2.0 server's JWK set, published at a well-known URL. The RemoteJWKSet +// object caches the retrieved keys to speed up subsequent look-ups and can +// also handle key-rollover + JWKSource keySource = + new RemoteJWKSet<>(new URL("https://appleid.apple.com/auth/keys")); + +// The expected JWS algorithm of the access tokens (agreed out-of-band) + JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256; + +// Configure the JWT processor with a key selector to feed matching public +// RSA keys sourced from the JWK set URL + JWSKeySelector keySelector = + new JWSVerificationKeySelector<>(expectedJWSAlg, keySource); + + jwtProcessor.setJWSKeySelector(keySelector); + +// Set the required JWT claims for access tokens issued by the Connect2id +// server, may differ with other servers + jwtProcessor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier<>()); + +// Process the token + JSONObject claimsSet = jwtProcessor.process(idToken, null).toJSONObject(); + + var email = claimsSet.getAsString("email"); + var verified = claimsSet.getAsString("email_verified").equals("true"); + + if (verified) { + com.juick.User user = userService.getUserByEmail(email); + 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())); + return "redirect:" + uriComponentsBuilder.build().toUriString(); } 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); + String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); + emailService.addVerificationCode(null, email, verificationCode); + return "redirect:/signup?type=email&hash=" + verificationCode; } - } else { - logger.warn("invalid tg hash {} for {}", resultString, hash); } throw new HttpBadRequestException(); - }*/ + } } -- cgit v1.2.3