Skip to content

Instantly share code, notes, and snippets.

@RZetko
Last active December 30, 2022 13:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RZetko/985c5cac9511f6115cf5103f2d49c3e4 to your computer and use it in GitHub Desktop.
Save RZetko/985c5cac9511f6115cf5103f2d49c3e4 to your computer and use it in GitHub Desktop.
Apple Sign In Java Micronaut implementation
Works with Micronaut 3.5.3.
Don't forget to:
- change package names according to your project
- replace path to your AuthKey.p8 file in AppleSignInUtil.java and set up your resources accordingly
- implement your own AuthExpection or use generic Exception
package com.example.yourpackagename.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class AppleSignInCallbackDetails {
String code;
@JsonProperty("id_token")
String idToken;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getIdToken() {
return idToken;
}
public void setIdToken(String idToken) {
this.idToken = idToken;
}
}
package com.example.yourpackagename.model;
public class AppleSignInDetails {
String authorizationCode;
String email;
String familyName;
String givenName;
String identityToken;
String state;
String userIdentifier;
Boolean isWeb = false;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFamilyName() {
return familyName;
}
public void setFamilyName(String familyName) {
this.familyName = familyName;
}
public String getGivenName() {
return givenName;
}
public void setGivenName(String givenName) {
this.givenName = givenName;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getUserIdentifier() {
return userIdentifier;
}
public void setUserIdentifier(String userIdentifier) {
this.userIdentifier = userIdentifier;
}
public String getAuthorizationCode() {
return authorizationCode;
}
public void setAuthorizationCode(String code) {
this.authorizationCode = code;
}
public String getIdentityToken() {
return identityToken;
}
public void setIdentityToken(String idToken) {
this.identityToken = idToken;
}
public Boolean getIsWeb() {
return isWeb;
}
public void getIsWeb(Boolean isWeb) {
this.isWeb = isWeb;
}
}
package com.example.yourpackagename.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class AppleSignInIdTokenPayload {
private String iss;
private String sub;
private String aud;
private Long iat;
private Long exp;
private String nonce;
@JsonProperty("nonce_supported")
private Boolean nonceSupported;
private String email;
@JsonProperty("email_verified")
private Boolean emailVerified;
@JsonProperty("real_user_status")
private Integer realUserStatus;
@JsonProperty("is_private_email")
private String isPrivateEmail;
@JsonProperty("transfer_sub")
private String transferSub;
@JsonProperty("at_hash")
private String atHash;
@JsonProperty("auth_time")
private Long authTime;
public String getIss() {
return iss;
}
public void setIss(String iss) {
this.iss = iss;
}
public String getSub() {
return sub;
}
public void setSub(String sub) {
this.sub = sub;
}
public String getAud() {
return aud;
}
public void setAud(String aud) {
this.aud = aud;
}
public Long getIat() {
return iat;
}
public void setIat(Long iat) {
this.iat = iat;
}
public Long getExp() {
return exp;
}
public void setExp(Long exp) {
this.exp = exp;
}
public String getNonce() {
return nonce;
}
public void setNonce(String nonce) {
this.nonce = nonce;
}
public Boolean getNonceSupported() {
return nonceSupported;
}
public void setNonceSupported(Boolean nonceSupported) {
this.nonceSupported = nonceSupported;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Boolean getEmailVerified() {
return emailVerified;
}
public void setEmailVerified(Boolean emailVerified) {
this.emailVerified = emailVerified;
}
public Integer getRealUserStatus() {
return realUserStatus;
}
public void setRealUserStatus(Integer realUserStatus) {
this.realUserStatus = realUserStatus;
}
public String getIsPrivateEmail() {
return isPrivateEmail;
}
public void setIsPrivateEmail(String isPrivateEmail) {
this.isPrivateEmail = isPrivateEmail;
}
public String getTransferSub() {
return transferSub;
}
public void setTransferSub(String transferSub) {
this.transferSub = transferSub;
}
public String getAtHash() {
return atHash;
}
public void setAtHash(String atHash) {
this.atHash = atHash;
}
public Long getAuthTime() {
return authTime;
}
public void setAuthTime(Long authTime) {
this.authTime = authTime;
}
}
package com.example.yourpackagename.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class AppleSignInTokenResponse {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("token_type")
private String tokenType;
@JsonProperty("expires_in")
private Long expiresIn;
@JsonProperty("refresh_token")
private String refreshToken;
@JsonProperty("id_token")
private String idToken;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public Long getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(Long expiresIn) {
this.expiresIn = expiresIn;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getIdToken() {
return idToken;
}
public void setIdToken(String idToken) {
this.idToken = idToken;
}
}
package com.example.yourpackagename.utils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import com.example.yourpackagename.model.AppleSignInIdTokenPayload;
import com.example.yourpackagename.model.AppleSignInTokenResponse;
import com.example.yourpackagename.user.AuthException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.micronaut.core.io.ResourceResolver;
import io.micronaut.core.io.scan.ClassPathResourceLoader;
public class AppleSignInUtil {
private String baseUrl;
private String keyId;
private String teamId;
private String clientId;
private String webClientId;
private String webRedirectUrl;
private PrivateKey pKey;
private HttpClient httpClient;
public AppleSignInUtil(
String baseUrl,
String keyId,
String teamId,
String clientId,
String webClientId,
String webRedirectUrl) {
this.baseUrl = baseUrl;
this.keyId = keyId;
this.teamId = teamId;
this.clientId = clientId;
this.webClientId = webClientId;
this.webRedirectUrl = webRedirectUrl;
this.httpClient = HttpClient.newBuilder()
.version(Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(15))
.build();
}
private static PrivateKey getPrivateKey() throws IOException {
Optional<ClassPathResourceLoader> optionalLoader = new ResourceResolver()
.getLoader(ClassPathResourceLoader.class);
if (optionalLoader.isPresent()) {
ClassPathResourceLoader loader = optionalLoader.get();
Optional<InputStream> optionalInputStream = loader
.getResourceAsStream("classpath:apple/AuthKey_XXXXXXXXXX.p8");
if (optionalInputStream.isPresent()) {
InputStream appleAuthKeyInputStream = optionalInputStream.get();
final PEMParser pemParser = new PEMParser(new InputStreamReader(appleAuthKeyInputStream));
final JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
final PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
return converter.getPrivateKey(object);
}
}
return null;
}
private String generateJWT() throws IOException {
if (pKey == null) {
pKey = getPrivateKey();
}
return Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, keyId)
.setIssuer(teamId)
.setAudience(baseUrl)
.setSubject(clientId)
.setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(pKey, SignatureAlgorithm.ES256)
.compact();
}
private String generateWebJWT() throws IOException {
return Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, keyId)
.setIssuer(teamId)
.setAudience(baseUrl)
.setSubject(webClientId)
.setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(getPrivateKey(), SignatureAlgorithm.ES256)
.compact();
}
public AppleSignInIdTokenPayload appleAuth(String authorizationCode, boolean isWeb)
throws IOException, URISyntaxException, InterruptedException, AuthException {
Map<String, String> requestBody = new HashMap<>();
requestBody.put("client_id", isWeb ? webClientId : clientId);
requestBody.put("client_secret", isWeb ? generateWebJWT() : generateJWT());
requestBody.put("grant_type", "authorization_code");
requestBody.put("code", authorizationCode);
if (isWeb) {
requestBody.put("redirect_uri", webRedirectUrl);
}
String formBody = requestBody.entrySet()
.stream()
.map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
HttpRequest req = HttpRequest.newBuilder()
.uri(new URI(baseUrl + "/auth/token"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(formBody))
.build();
HttpResponse<String> response = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new AuthException(response.body());
}
ObjectMapper mapper = new ObjectMapper();
AppleSignInTokenResponse tokenResponse = mapper.readValue(response.body(), AppleSignInTokenResponse.class);
String idToken = tokenResponse.getIdToken();
String payload = idToken.split("\\.")[1];// 0 is header we ignore it for now
String decoded = new String(Decoders.BASE64.decode(payload));
return mapper.readValue(decoded, AppleSignInIdTokenPayload.class);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment