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.
问题:
回答1:
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
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 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;
}
/*
* Returns unique user id from apple
* */
public static String appleAuth(String authorizationCode) throws Exception {
String token = generateJWT();
HttpResponse<String> response = Unirest.post(APPLE_AUTH_URL)
.header("Content-Type", "application/x-www-form-urlencoded")
.field("client_id", CLIENT_ID)
.field("client_secret", token)
.field("grant_type", "authorization_code")
.field("code", authorizationCode)
.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;
..getters and setters}
回答2:
I had an error too but after some tweaks, its working, find my tweak below, note, its in kotlin
private suspend fun getPrivateKey(): Status {
return awaitBlocking {
val authKeyFile = appleConfig.getString("auth_private_key_file", "")
val authTokenFilePath = getDataDir()!!.resolve(authKeyFile).absolutePath
val pemParser = PEMParser(FileReader(authTokenFilePath))
val converter = JcaPEMKeyConverter()
val obj = pemParser.readObject() as PrivateKeyInfo
val privateKey = converter.getPrivateKey(obj)
successStatus(data = privateKey)
}
}
/**
* generateSecretKey
*/
suspend fun generateSecretKey() : Status{
val getAuthPrivateKey = getPrivateKey()
if(getAuthPrivateKey.isError()){
logger.fatal(getAuthPrivateKey.message)
return errorStatus("system_busy")
}
val privateKeyData = getAuthPrivateKey.getData<PrivateKey>()
val clientId = "com.company.app"
//team id found in apple developer portal
val teamId = appleConfig.getString("team_id","")
//apple sign in key ID found in app developer portal
val authKeyId = appleConfig.getString("auth_key_id","")
val header = mutableMapOf<String,Any>(
"alg" to "E256",
"kid" to authKeyId
)
val now = Instant.now().epochSecond
val claims = mutableMapOf<String,Any>(
"iss" to teamId,
"iat" to now,
"exp" to now + 86400*180,
"aud" to "https://appleid.apple.com",
"sub" to clientId
)
println("header - $header")
println("claims - $claims")
val token = Jwts.builder()
.setHeader(header)
.setClaims(claims)
.signWith(privateKeyData,SignatureAlgorithm.ES256)
.compact();
return successStatus(data = token)
} //end fun
/**
* fetchApplePublicKeys
*/
private suspend fun fetchAccessToken(authInfo: JsonObject): Status {
return try{
val authCode = authInfo.getString("auth_code")
val clientIdToken = authInfo.getString("id_token")
val accessTokenEndpoint =
appleConfig.getString("access_token_endpoint")
val secretKeyTokenStatus = generateSecretKey()
if(secretKeyTokenStatus.isError()){
logger.fatal(secretKeyTokenStatus.message)
return errorStatus("system_busy")
}
val clientSecret = secretKeyTokenStatus.getData<String>()
val redirectUrl = ""
val clientId = appleConfig.getString("client_id")
val formData = MultiMap.caseInsensitiveMultiMap()
formData.add("client_secret",clientSecret)
.add("client_id",clientId)
.add("redirect_uri",redirectUrl)
.add("grant_type","authorization_code")
.add("code",authCode)
println("accessTokenEndpoint - $accessTokenEndpoint")
println("formData - $formData")
val responseData = httpClient(this::class)
.postAbs(accessTokenEndpoint)
.putHeader("Content-Type","application/x-www-form-urlencoded")
.sendFormAwait(formData)
.bodyAsJsonObject()
println("responseData - ${responseData}")
if(responseData.containsKey("error")){
logger.fatal(responseData.getString("error"))
return errorStatus("social_auth_failed")
}
//val responseIdToken = responseData.getString("id_token","")
return successStatus(data = responseData)
} catch (e: Exception){
logger.fatal(e.message,e)
errorStatus("system_busy")
}
}