From 8f7b2af21beda60d6123f555a0c21d2eadfc777a Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Fri, 20 Dec 2019 16:28:41 +0300 Subject: Sign In With Apple --- .../configuration/SignInWithAppleConfig.java | 44 +++++++++ .../juick/server/www/controllers/SocialLogin.java | 100 ++++++++++++++++++++- 2 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/juick/server/configuration/SignInWithAppleConfig.java (limited to 'src/main/java/com/juick/server') diff --git a/src/main/java/com/juick/server/configuration/SignInWithAppleConfig.java b/src/main/java/com/juick/server/configuration/SignInWithAppleConfig.java new file mode 100644 index 00000000..f736e673 --- /dev/null +++ b/src/main/java/com/juick/server/configuration/SignInWithAppleConfig.java @@ -0,0 +1,44 @@ +/* + * 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.server.configuration; + +import com.github.scribejava.apis.AppleClientSecretGenerator; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +@Configuration +public class SignInWithAppleConfig { + @Value("${apple_app_id:com.example.app}") + private String appId; + @Value("${apple_team_id:teamid}") + private String teamId; + @Value("${apple_key_id:keyid}") + private String keyId; + @Value("${apple_key_path:AuthKey.p8}") + private String keyPath; + + @Bean + public AppleClientSecretGenerator clientSecretGenerator() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { + return new AppleClientSecretGenerator(appId, teamId, keyId, keyPath); + } +} diff --git a/src/main/java/com/juick/server/www/controllers/SocialLogin.java b/src/main/java/com/juick/server/www/controllers/SocialLogin.java index 7d019940..92f83c4e 100644 --- a/src/main/java/com/juick/server/www/controllers/SocialLogin.java +++ b/src/main/java/com/juick/server/www/controllers/SocialLogin.java @@ -17,6 +17,8 @@ package com.juick.server.www.controllers; 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.TwitterApi; import com.github.scribejava.apis.VkontakteApi; @@ -33,9 +35,22 @@ import com.juick.service.EmailService; import com.juick.service.TelegramService; import com.juick.service.UserService; import com.juick.service.security.annotation.Visitor; +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.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; @@ -44,6 +59,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.CookieValue; 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.util.UriComponentsBuilder; @@ -53,6 +69,8 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.net.URL; +import java.text.ParseException; import java.util.List; import java.util.Map; import java.util.UUID; @@ -79,7 +97,7 @@ public class SocialLogin { @Inject private ObjectMapper jsonMapper; private ServiceBuilder twitterBuilder; - private OAuth20Service facebookAuthService, vkAuthService; + private OAuth20Service facebookAuthService, vkAuthService, appleSignInService; @Value("${twitter_consumer_key:appid}") private String twitterConsumerKey; @@ -91,7 +109,8 @@ public class SocialLogin { private String VK_SECRET; @Value("${telegram_token:secret}") private String telegramToken; - + @Value("${apple_app_id:appid}") + private String appleApplicationId; @Inject private CrosspostService crosspostService; @Inject @@ -100,14 +119,16 @@ public class SocialLogin { private EmailService emailService; @Inject private TelegramService telegramService; + @Inject + private AppleClientSecretGenerator clientSecretGenerator; @PostConstruct public void init() { ServiceBuilder facebookBuilder = new ServiceBuilder(FACEBOOK_APPID); twitterBuilder = new ServiceBuilder(twitterConsumerKey); ServiceBuilder vkBuilder = new ServiceBuilder(VK_APPID); - UriComponentsBuilder facebookRedirectBuilder = UriComponentsBuilder.fromUriString(baseUri); - String facebookRedirectUri = facebookRedirectBuilder.replacePath("/_fblogin").build().toUriString(); + UriComponentsBuilder redirectBuilder = UriComponentsBuilder.fromUriString(baseUri); + String facebookRedirectUri = redirectBuilder.replacePath("/_fblogin").build().toUriString(); facebookAuthService = facebookBuilder .apiSecret(FACEBOOK_SECRET) .callback(facebookRedirectUri) @@ -118,6 +139,12 @@ public class SocialLogin { .defaultScope("friends,wall,offline") .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)); } @GetMapping("/_fblogin") @@ -315,4 +342,69 @@ public class SocialLogin { } throw new HttpBadRequestException(); } + + @GetMapping("/_apple") + public String doAppleLogin(HttpServletRequest request, + @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) 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()) { + Cookie c = new Cookie("hash", userService.getHashByUID(user.getUid())); + c.setMaxAge(50 * 24 * 60 * 60); + response.addCookie(c); + return "redirect:/" + Utils.getPreviousPageByRequest(request).orElse(StringUtils.EMPTY); + } else { + String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); + emailService.addVerificationCode(null, email, verificationCode); + return "redirect:/signup?type=email&hash=" + verificationCode; + } + } + throw new HttpBadRequestException(); + } } -- cgit v1.2.3