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; | |
} |
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
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 + "]"; | |
} | |
} |
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
2 Method for handling redirect from form_post to get redirect with authorization params as query params in popup window url.
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
Top comments (0)