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
---
.../scribejava/apis/AppleClientAuthentication.java | 33 +++++++
.../apis/AppleClientSecretGenerator.java | 85 ++++++++++++++++++
.../com/github/scribejava/apis/AppleSignInApi.java | 45 ++++++++++
.../configuration/SignInWithAppleConfig.java | 44 +++++++++
.../juick/server/www/controllers/SocialLogin.java | 100 ++++++++++++++++++++-
5 files changed, 303 insertions(+), 4 deletions(-)
create mode 100644 src/main/java/com/github/scribejava/apis/AppleClientAuthentication.java
create mode 100644 src/main/java/com/github/scribejava/apis/AppleClientSecretGenerator.java
create mode 100644 src/main/java/com/github/scribejava/apis/AppleSignInApi.java
create mode 100644 src/main/java/com/juick/server/configuration/SignInWithAppleConfig.java
(limited to 'src/main/java')
diff --git a/src/main/java/com/github/scribejava/apis/AppleClientAuthentication.java b/src/main/java/com/github/scribejava/apis/AppleClientAuthentication.java
new file mode 100644
index 00000000..6bb919a3
--- /dev/null
+++ b/src/main/java/com/github/scribejava/apis/AppleClientAuthentication.java
@@ -0,0 +1,33 @@
+/*
+ * 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.github.scribejava.apis;
+
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication;
+
+public class AppleClientAuthentication implements ClientAuthentication {
+ private final AppleClientSecretGenerator generator;
+ public AppleClientAuthentication(AppleClientSecretGenerator generator) {
+ this.generator = generator;
+ }
+ @Override
+ public void addClientAuthentication(OAuthRequest request, String apiKey, String apiSecret) {
+ request.addBodyParameter("client_id", generator.getApplicationId());
+ request.addBodyParameter("client_secret", generator.getClientSecret());
+ }
+}
diff --git a/src/main/java/com/github/scribejava/apis/AppleClientSecretGenerator.java b/src/main/java/com/github/scribejava/apis/AppleClientSecretGenerator.java
new file mode 100644
index 00000000..3af6bc7a
--- /dev/null
+++ b/src/main/java/com/github/scribejava/apis/AppleClientSecretGenerator.java
@@ -0,0 +1,85 @@
+/*
+ * 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.github.scribejava.apis;
+
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.apache.commons.io.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.EncodedKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.time.*;
+import java.util.Base64;
+import java.util.Date;
+
+public class AppleClientSecretGenerator {
+ private final String subject;
+ private final String teamId;
+ private final String keyId;
+
+ private final Key signingKey;
+
+ public AppleClientSecretGenerator(final String subject, final String teamId, final String keyId, final String keyPath)
+ throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
+ this.subject = subject;
+ this.keyId = keyId;
+ this.teamId = teamId;
+
+ String pemData = FileUtils.readFileToString(new File(keyPath), StandardCharsets.UTF_8);
+ String p8encodedData = pemData
+ .replace(
+ "-----BEGIN PRIVATE KEY-----\n", "")
+ .replace("\n", "")
+ .replace("-----END PRIVATE KEY-----", "");
+ KeyFactory kf = KeyFactory.getInstance("EC");
+ EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(p8encodedData));
+ signingKey = kf.generatePrivate(keySpec);
+ }
+
+ public String getClientSecret() {
+ Instant now = Instant.now();
+ return Jwts.builder()
+ .setHeaderParam("kid", keyId)
+ .setIssuer(teamId)
+ .setAudience("https://appleid.apple.com")
+ .setIssuedAt(Date.from(now))
+ .setSubject(subject)
+ .setExpiration(Date.from(ZonedDateTime.ofInstant(now, ZoneId.of("UTC")).plusMonths(1).toInstant()))
+ .signWith(signingKey, SignatureAlgorithm.ES256)
+ .compact();
+ }
+
+ public String getTeamId() {
+ return teamId;
+ }
+
+ public String getKeyId() {
+ return keyId;
+ }
+
+ public String getApplicationId() {
+ return subject;
+ }
+}
diff --git a/src/main/java/com/github/scribejava/apis/AppleSignInApi.java b/src/main/java/com/github/scribejava/apis/AppleSignInApi.java
new file mode 100644
index 00000000..be14ef16
--- /dev/null
+++ b/src/main/java/com/github/scribejava/apis/AppleSignInApi.java
@@ -0,0 +1,45 @@
+/*
+ * 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.github.scribejava.apis;
+
+import com.github.scribejava.core.builder.api.DefaultApi20;
+import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication;
+
+public class AppleSignInApi extends DefaultApi20 {
+
+ private final AppleClientSecretGenerator clientSecretGenerator;
+
+ public AppleSignInApi(AppleClientSecretGenerator clientSecretGenerator) {
+ this.clientSecretGenerator = clientSecretGenerator;
+ }
+
+ @Override
+ public String getAccessTokenEndpoint() {
+ return "https://appleid.apple.com/auth/token";
+ }
+
+ @Override
+ protected String getAuthorizationBaseUrl() {
+ return "https://appleid.apple.com/auth/authorize?response_mode=form_post";
+ }
+
+ @Override
+ public ClientAuthentication getClientAuthentication() {
+ return new AppleClientAuthentication(clientSecretGenerator);
+ }
+}
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