Issue
I've implemented the app side of the new apple feature "Sign in with Apple" but i'm unable to verificate with authorizationCode in my backend. My backend is written in java and i'm unable to generate JWT and communicate with Apple servers.
Solution
First go developer.apple.com -> Certificates, Identifiers & Profiles -> Keys. Generate a key for Apple Sign in and download this key. You can not download this key again so keep it in a safe place and don't share with others. Also your Key ID shown here note this, you'll need this later. You'll also need team id. If you don't know it, it's written top right of the page like YOURNAME - XX0XX00XXX.
You will basicly follow these steps.
1.Generate JWT from your key
2.Send auth code with your token
3.Decode response
UPDATE for using both web and mobile
If you would like to use apple login for web there are few more steps you need to follow.
4. Add new Identifier for web
Go developer.apple.com -> Certificates, Identifiers & Profiles -> Identifiers. Register a new identifier with clicking plus button. Select Service IDs and continue. Provide a description and identifier. Identifier must be unique and different from your bundle id. (For example you can use com.your.bundle.id.web). Click continue click register. Then you need to configure this service id. Select Service IDs (It's placed at top right near search icon) your newly created services id listed below click it and enable Sign In with Apple tick box. Then you need to configure your domain. Provide your domain and return url.
Some important points for web you can get invalid_grant error if you forgot to pass a valid redirect_url or try to use same authorization_code more than once.
public class AppleLoginUtil {
private static String APPLE_AUTH_URL = "https://appleid.apple.com/auth/token";
private static String KEY_ID = "**********";
private static String TEAM_ID = "**********";
private static String CLIENT_ID = "com.your.bundle.id";
private static String WEB_CLIENT_ID = "com.your.bundle.id.web";
private static String WEB_REDIRECT_URL = "https://bundle.your.com/";
private static PrivateKey pKey;
private static PrivateKey getPrivateKey() throws Exception {
//read your key
String path = new ClassPathResource("apple/AuthKey.p8").getFile().getAbsolutePath();
final PEMParser pemParser = new PEMParser(new FileReader(path));
final JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
final PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
final PrivateKey pKey = converter.getPrivateKey(object);
return pKey;
}
private static String generateJWT() throws Exception {
if (pKey == null) {
pKey = getPrivateKey();
}
String token = Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, KEY_ID)
.setIssuer(TEAM_ID)
.setAudience("https://appleid.apple.com")
.setSubject(CLIENT_ID)
.setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(pKey, SignatureAlgorithm.ES256)
.compact();
return token;
}
private static String generateWebJWT() throws Exception {
String token = Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, KEY_ID)
.setIssuer(TEAM_ID)
.setAudience("https://appleid.apple.com")
.setSubject(WEB_CLIENT_ID)
.setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(getPrivateKey(), SignatureAlgorithm.ES256)
.compact();
return token;
}
/*
* Returns unique user id from apple
* */
public static String appleAuth(String authorizationCode, boolean forWeb) throws Exception {
HttpResponse<String> response = Unirest.post(APPLE_AUTH_URL)
.header("Content-Type", "application/x-www-form-urlencoded")
.field("client_id", forWeb ? WEB_CLIENT_ID : CLIENT_ID)
.field("client_secret", forWeb ? generateWebJWT() : generateJWT())
.field("grant_type", "authorization_code")
.field("code", authorizationCode)
.field("redirect_uri", forWeb ? WEB_REDIRECT_URL : null)
.asString();
TokenResponse tokenResponse=new Gson().fromJson(response.getBody(),TokenResponse.class);
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));
IdTokenPayload idTokenPayload = new Gson().fromJson(decoded,IdTokenPayload.class);
return idTokenPayload.getSub();
}
}
I've used BouncyCastle jjwt for generating token. And also unirest and gson for rest calls.
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.63</version>
</dependency>
<!--JJWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<!--UNIREST-->
<dependency>
<groupId>com.mashape.unirest</groupId>
<artifactId>unirest-java</artifactId>
<version>1.4.9</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.6</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpasyncclient</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.3.6</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20140107</version>
</dependency>
I've also parsed the responses to these classes if you wanted to know.
public class TokenResponse {
private String access_token;
private String token_type;
private Long expires_in;
private String refresh_token;
private String id_token;
..getters and setters
}
public class IdTokenPayload {
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;
..getters and setters
}
Answered By - Çağdaş Tunca
Answer Checked By - Robin (JavaFixing Admin)