aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/juick
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2019-12-20 16:28:41 +0300
committerGravatar Vitaly Takmazov2019-12-20 16:28:41 +0300
commit8f7b2af21beda60d6123f555a0c21d2eadfc777a (patch)
treecec8a81dc63eaa272a4ab9e818ebb0cbde1e4670 /src/main/java/com/juick
parent3ab17252a7d9f6c5834d85d050a19fa41be0c07d (diff)
Sign In With Apple
Diffstat (limited to 'src/main/java/com/juick')
-rw-r--r--src/main/java/com/juick/server/configuration/SignInWithAppleConfig.java44
-rw-r--r--src/main/java/com/juick/server/www/controllers/SocialLogin.java100
2 files changed, 140 insertions, 4 deletions
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 000000000..f736e673f
--- /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 <http://www.gnu.org/licenses/>.
+ */
+
+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 7d019940e..92f83c4ed 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<String, String> 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<SecurityContext> 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<SecurityContext> 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<SecurityContext> 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();
+ }
}