DEV Community

Cover image for Adding Apple Sign In To Spring Boot App (JAVA) Backend part
Balvinder Singh
Balvinder Singh

Posted on • Edited on • Originally published at Medium

10

Adding Apple Sign In To Spring Boot App (JAVA) Backend part

So we submitted our app to IOS Store for review and it got rejected because of one silly reason. Because we were using other social logins and not apple login, so they simply asked us to replace other logins with Apple. Does not that sound bad ? Yeah, we also felt same, and asked but they were strict on this. So , we have to add. But there was a very few docs and only some blogs on medium here with some different languages and not java. So struggling for over a week. I was finally able to done this, so i am now sharing it here, so you all do not face same issues.
Some ideas taken from previous blogs, listed below. Go check.

1. Generate following from Developer account

  • Apple Certificate (.p8), that needs to be downloaded
  • Apple Client ID
  • Apple Key ID
  • Apple Team ID
  • Apple URL (https://appleid.apple.com)

These all can be generated by going to keys and identifiers menu in Application Developer account of apple

2. Generating a JWT Token for Apple which will work as Client_Secret for apple authentication

/***Imports ****/
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import java.security.PrivateKey;
import java.security.PublicKey;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.io.File;
import org.springframework.util.ResourceUtils;
/**************/
/***
* To generate JWT token for verification
* @param identifierFromApp the service identifier of web app or kid from mobile app extracted from id token
*
****/
private String generateJWT(String identiferFromApp) throws Exception {
// Generate a private key for token verification from your end with your creds
PrivateKey pKey = generatePrivateKey();
String token = Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, appleKeyId)
.setIssuer(appleTeamId)
.setAudience("https://appleid.apple.com")
.setSubject(identiferFromApp)
.setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(pKey, SignatureAlgorithm.ES256)
.compact();
return token;
}
// Method to generate private key from certificate you created
private PrivateKey generatePrivateKey() throws Exception {
// here i have added cert at resource/apple folder. So if you have added somewhere else, just replace it with your path ofcert
File file = ResourceUtils.getFile("classpath:apple/cert.p8");
final PEMParser pemParser = new PEMParser(new FileReader(file));
final JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
final PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
final PrivateKey pKey = converter.getPrivateKey(object);
pemParser.close();
return pKey;
}
view raw JWTToken.java hosted with ❤ by GitHub

3. Authorize request method to get OAuth token as TokenResponse and ID token as ID Token Payload

Note : Apple only sends user obj first time as a JSON string containing email and name. If user not available, email can be get from idToken

/*************** Imports ***************/
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONObject;
import org.springframework.cloud.cloudfoundry.com.fasterxml.jackson.databind.ObjectMapper;
private static String APPLE_AUTH_URL = "https://appleid.apple.com/auth/token";
/** Social Paramters DTO check here
https://gist.github.com/balvinder294/8a6c8c4754c309a3a7052ed037bc3c3b
Apple Id token payload DTO here
https://gist.github.com/balvinder294/72a437791aab7c9708b5a74dcece41b9
Token Response DTO here
https://gist.github.com/balvinder294/344284456e06c37d5afcdf08b8381092
*****************/
/****************************************/
public void authorizeApple(SocialParametersDTO socialParametersDTO) throws Exception {
log.debug("Get Apple User Profile {}", socialParametersDTO);
String appClientId = null;
if (socialParametersDTO.getIdentifierFromApp() != null) {
// if kid is sent from mobile app
appClientId = socialParametersDTO.getIdentifierFromApp();
} else {
// if doing sign in with web using predefined identifier
appClientId = appleClientId;
}
SocialUserDTO socialUserDTO = new SocialUserDTO();
// generate personal verification token
String token = generateJWT(appClientId);
////////// Get OAuth Token from Apple by exchanging code
// Prepare client, you can use other Rest client library also
OkHttpClient okHttpClient = new OkHttpClient()
.newBuilder()
.connectTimeout(70, TimeUnit.SECONDS)
.writeTimeout(70, TimeUnit.SECONDS)
.readTimeout(70, TimeUnit.SECONDS)
.build();
// Request body for sending parameters as FormUrl Encoded
RequestBody requestBody = new FormBody
.Builder()
.add("client_id", appClientId)
.add("client_secret", token)
.add("grant_type", "authorization_code")
.add("code", socialParametersDTO.getAuthorizationCode())
.build();
// Prepare rest request
Request request = new Request
.Builder()
.url(APPLE_AUTH_URL)
.post(requestBody)
.header("Content-Type", "application/x-www-form-urlencoded")
.build();
// Execute api call and get Response
Response resp = okHttpClient.newCall(request).execute();
String response = resp.body().string();
// Parse response as DTO
ObjectMapper objectMapper = new ObjectMapper();
TokenResponse tokenResponse = objectMapper.readValue(response, TokenResponse.class);
// Parse id token from Token
String idToken = tokenResponse.getId_token();
String payload = idToken.split("\\.")[1];// 0 is header we ignore it for now
String decoded = new String(Decoders.BASE64.decode(payload));
AppleIDTokenPayload idTokenPayload = new Gson().fromJson(decoded, AppleIDTokenPayload.class);
// if we have user obj also from Web or mobile
// we get only at 1st authorization
if (socialParametersDTO.getUserObj() != null ) {
JSONObject user = new JSONObject(userObj);
JSONObject name = user.has("name") ? user.getJSONObject("name") : null;
String firstName = name.getString("firstName);
String lastName = name.getString("lastName);
}
// Add your logic here
}

4. Response classes

public class AppleIDTokenPayload {
private String iss;
private String aud;
private Long exp;
private Long iat;
private String sub;// users unique id
private String at_hash;
private Long auth_time;
private Boolean nonce_supported;
private Boolean email_verified;
private String email;
public String getIss() {
return iss;
}
public void setIss(String iss) {
this.iss = iss;
}
public String getAud() {
return aud;
}
public void setAud(String aud) {
this.aud = aud;
}
public Long getExp() {
return exp;
}
public void setExp(Long exp) {
this.exp = exp;
}
public Long getIat() {
return iat;
}
public void setIat(Long iat) {
this.iat = iat;
}
public String getSub() {
return sub;
}
public void setSub(String sub) {
this.sub = sub;
}
public String getAt_hash() {
return at_hash;
}
public void setAt_hash(String at_hash) {
this.at_hash = at_hash;
}
public Long getAuth_time() {
return auth_time;
}
public void setAuth_time(Long auth_time) {
this.auth_time = auth_time;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((at_hash == null) ? 0 : at_hash.hashCode());
result = prime * result + ((aud == null) ? 0 : aud.hashCode());
result = prime * result + ((auth_time == null) ? 0 : auth_time.hashCode());
result = prime * result + ((exp == null) ? 0 : exp.hashCode());
result = prime * result + ((iat == null) ? 0 : iat.hashCode());
result = prime * result + ((iss == null) ? 0 : iss.hashCode());
result = prime * result + ((sub == null) ? 0 : sub.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AppleIDTokenPayload other = (AppleIDTokenPayload) obj;
if (at_hash == null) {
if (other.at_hash != null)
return false;
} else if (!at_hash.equals(other.at_hash))
return false;
if (aud == null) {
if (other.aud != null)
return false;
} else if (!aud.equals(other.aud))
return false;
if (auth_time == null) {
if (other.auth_time != null)
return false;
} else if (!auth_time.equals(other.auth_time))
return false;
if (exp == null) {
if (other.exp != null)
return false;
} else if (!exp.equals(other.exp))
return false;
if (iat == null) {
if (other.iat != null)
return false;
} else if (!iat.equals(other.iat))
return false;
if (iss == null) {
if (other.iss != null)
return false;
} else if (!iss.equals(other.iss))
return false;
if (sub == null) {
if (other.sub != null)
return false;
} else if (!sub.equals(other.sub))
return false;
return true;
}
@Override
public String toString() {
return "AppleIDTokenPayload [at_hash=" + at_hash + ", aud=" + aud + ", auth_time=" + auth_time + ", email="
+ email + ", email_verified=" + email_verified + ", exp=" + exp + ", iat=" + iat + ", iss=" + iss
+ ", nonce_supported=" + nonce_supported + ", sub=" + sub + "]";
}
public Boolean getNonce_supported() {
return nonce_supported;
}
public void setNonce_supported(Boolean nonce_supported) {
this.nonce_supported = nonce_supported;
}
public Boolean getEmail_verified() {
return email_verified;
}
public void setEmail_verified(Boolean email_verified) {
this.email_verified = email_verified;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
public final class TokenResponse {
private String access_token;
private String token_type;
private Long expires_in;
private String refresh_token;
private String id_token;
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public String getToken_type() {
return token_type;
}
public void setToken_type(String token_type) {
this.token_type = token_type;
}
public Long getExpires_in() {
return expires_in;
}
public void setExpires_in(Long expires_in) {
this.expires_in = expires_in;
}
public String getRefresh_token() {
return refresh_token;
}
public void setRefresh_token(String refresh_token) {
this.refresh_token = refresh_token;
}
public String getId_token() {
return id_token;
}
public void setId_token(String id_token) {
this.id_token = id_token;
}
@Override
public String toString() {
return "TokenResponse [access_token=" + access_token + ", expires_in=" + expires_in + ", id_token=" + id_token
+ ", refresh_token=" + refresh_token + ", token_type=" + token_type + "]";
}
}
package dehaze.mvp.service.dto;
import java.io.Serializable;
/**
* A DTO for the SocialUser.
*/
public class SocialParametersDTO implements Serializable {
private static final long serialVersionUID = 3484800209656475818L;
// code varaible returned from sign in request
private String authorizationCode;
// If Apple sign in authoriation sends user object as string
private String userObj;
// id token from Apple Sign in Authorization if asked
private String idToken;
// kid or key identifier from mobile app authorization
private String identifierFromApp;
@Override
public String toString() {
return "SocialParametersDTO [
authorizationCode=" + authorizationCode + ",
idToken=" + idToken + ",
identifierFromApp=" + identifierFromApp + ",
userObj=" + userObj + ",
"]";
}
public String getUserObj() {
return userObj;
}
public void setUserObj(String userObj) {
this.userObj = userObj;
}
public String getIdToken() {
return idToken;
}
public void setIdToken(String idToken) {
this.idToken = idToken;
}
public String getAuthorizationCode() {
return authorizationCode;
}
public void setAuthorizationCode(String authorizationCode) {
this.authorizationCode = authorizationCode;
}
public String getIdentifierFromApp() {
return identifierFromApp;
}
public void setIdentifierFromApp(String identifierFromApp) {
this.identifierFromApp = identifierFromApp;
}
}

So, i have listed the methods to complete the authorization and get details in backend. I will next add a new post for frontend login also, using some more java code. Let me know if any thing you stuck on.


Bonus code

Let me share here some more code below as a bonus, which will be used with frontend for web authorization flow. I will share frontend code later.
1 Method to generate authorization url with params. Also as we have multiple subdomains in project, adding the state as subDomain for redirecting to subdomain later.

Note: Advantage of this method is that, by using a single domain and redirect url, we can achieve Apple sign(Any other social Sign in also) for multiple domains using popup, to catch authorization params in the url after redirection.
i will add code for popup later in frontend part

import org.json.JSONObject;
/******
*To create authorization url for social sign in
this url will open authentication window for apple sign in
for a subdomain as state param
*****/
public String createAuthorizationUrl(String subDomain) {
// I have added jsob objct for encodin state paramters like for getting the curent domain used as we have multiple subdomains, you can add your state parameters here
JSONObject jsonObject = new JSONObject();
jsonObject.put("subDomain", subDomain);
try {
String url = "https://appleid.apple.com/auth/authorize?client_id=" + appleClientId +
"&redirect_uri=" + redirectUrl +
"&response_type=code%20id_token" + // to request code and id token both
"&scope=" + "name%20email" + // scopes are name email
"&response_mode=" + "form_post" + // when we request id token only form_post will be available
"&state=" + URLEncoder.encode(jsonObject.toString(), "UTF-8");
return url;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "";
}

2 Method for handling redirect from form_post to get redirect with authorization params as query params in popup window url.

/**
* Redirect User to Social verify to complete social media authorization
*
* @param state the state parameter containing BoxDomain for verification
* @param code the authorization code
* @param httpServletResponse the response with redirection for url
*
* @throws IOException
*/
@GetMapping(value = "/social-login/authorize")
public void forwardSocialAuthorization(@RequestParam String state, @RequestParam Optional<String> code, HttpServletResponse httpServletResponse) throws IOException {
String decodedStateParam = URLDecoder.decode(state, "UTF-8");
JSONObject stateParams = new JSONObject(decodedStateParam);
// your state param here, i added as just state
String subDomain = stateParams.getString("subDomain");
if(code.isPresent()){
httpServletResponse.sendRedirect(redirectSocialVerifyUrl(subDomain, code.get()));
}
}
/**
* Url for redirecting user based on subdomain from where authorization is requested
*
* @param subDomain the domain of the box
* @param code the authorization code
* @return the redirect to verfy social
*/
String redirectSocialVerifyUrlApple(String subDomain, String code, Optional<String> idToken, Optional<String> user) {
String redirectUrl = "https://" + boxDomain + "/verify-social" + "?code=" + code;
if (idToken.isPresent()) {
redirectUrl = redirectUrl + "idToken=" + idToken.get();
}
if (user.isPresent()) {
redirectUrl = redirectUrl + "user=" + user.get();
}
return redirectUrl;
}

So, this is it the secret bonus method, many people still looking for.


So this was all for now, i hope it will help you in achieving apple sign in. There are some more issues you may face, that i will address in later posts. You can comment any issue you may face and i will try to help as much as i can. Feel free to like and show love, your support inspires me to write more and share like this. Thanks for reading.

Do visit our another technical Blog Tekraze.com for more posts

Originally published at Medium

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay