Implementing Apple id signup and login for Flutter iOS, Java BE Application

Selvaraj Mani
10 min readJun 3, 2023

Note: This is my first ever article, please forgive any grammatical mistakes :-). I want to share my experience in implementing apple id authentication for my Flutter (Java BE) application. Hope this will help some developers as a reference.

History

Recently we submitted an iOS application for approval on AppConnect, but after 24 hours received a mail saying “App Rejected” with one of the below reasons.

This was a surprise for me. Went back to square-1 to implement the Apple Id authentication for my Flutter application, I already had a bitter experience in implementing login&signup with Facebook id. Initially, I thought Apple documentation will be too clear to understand how to implement (Ex: Stripe is a great example for development pleasure) Apple id sign-in, but the documentation is not clear and there are a lot of conflicting implementations and challenges when searching on the Internet. Finally, with the help of the Internet and ChatGPT, I could able to get the Apple id login working.

The following sections will explain step-by-step instructions we followed to get apple id working for our Flutter iOS application with Java backend.

Workflow

I followed the below workflow to get my apple-id authentication working for my Flutter iOS application.

  1. Creating the Apple Identifiers, Keys, Certificates
  2. Enabling “Signup” and “Login” using Apple in the Flutter application (Frontend)
  3. Enabling backend token verification (Java Backend)

I will clearly mention about each step in the below sections, please feel free to skip a section if you are already done with that step.

Step-1: Creating the Apple Identifiers, Keys, Certificates

In this step, we need the following list of details to start integrating the Apple-id authentication.

Note: Apple allows only the paid developer account to be used in apple id auth verification. So as a pre-requisite, you need to have paid apple developer account.

  1. Apple application identifier (AppId)
  2. Enable Apple sign-in capability
  3. Enable Apple sign-in capability on XCode
  4. Generate the Apple sign in Key

Note: You have an existing iOS application so I'm assuming you already have the Apple developer account and created the unique bundle-id/app-id.

Enable Sign in

Go to the developer.apple.com portal and navigate to “Certificates, Identifiers & Profiles”, and enable sign-in with the Apple checkbox.

Enable on XCode

  • Open Runner.xcproject in Xcode.
  • Select your Runner in Target
  • Go to Signing & Capabilities and add the “Sign in with Apple” capability like below
  • Generate the Key, Download the p8 cert (private cert), Note down the key id (KID)

Create sign-in key

Go to “Certificates, Identifiers & Profiles”, and select the “Keys” tab.

Create a new key, provide some readable name, select the sign-in option, and configure to select the AppId this key is associated with.

Warning: Once you create the key make sure you download and save the key certificate which is a p8 extension file, Once you download you cannot re-download it again, you lost that key permanently.

Once you save the key and downloaded the key certificate (p8 private cert), Note down the “Key ID”. This is the Key Id we refer to as Kid from here on.

Summary

After step-1 we have the following details to be used for verification of the token.

  • Kid (Key Identifier)
  • Client-Id (App Identifier)
  • P8 Private Key Cert (Key certificate downloaded)

Step-2: Enabling frontend to use Apple-id in the Flutter application

I installed the below Flutter package to use the sign-in with Apple.

sign_in_with_apple: ^4.3.0

After installing the flutter package, on my login screen we integrated the two buttons for “Sign up” and “Login” with the Apple.

Signup Workflow

Note: Apple will share the e-mail and name only once during the initial login workflow on your application. Subsequent login will only give you the token and other details but not e-mail and name. So I had to store this information securely in the backend so I can retrieve this information later if needed.

Hint: If you are testing and trying multiple times to get the e-mail and the name from the Apple login workflow, make sure on the simulator/device to sign out from your iCloud (apple-id) and then re-sign in again, that will make to start the workflow fresh and again you will get the e-mail and name for the first sign in again.

SignInButton(
Buttons.AppleDark,
elevation: 3.0,
text: "Sign up using Apple",
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
onPressed: () async {
try {
AuthorizationCredentialAppleID credential =
await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);

await //Send the retrieved email, firstName, familyname to your
//backend to store and fetch it later. Apple will not share
//email and the name everytime you login with Apple-Id.

} catch (ex) {
print(ex);
}
},
)

//At this stage you got email, name and then you can proceed with
//the account creation screens carrying these details and create the
//user account on your backend servers.

Flutter Apple Sign-In


Platform.isIOS
? SignInButton(
Buttons.AppleDark,
elevation: 3.0,
onPressed: () async {
handleAppleAuthLogin();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0)),
)
)
void handleAppleAuthLogin() async {
try {
AuthorizationCredentialAppleID authzProfile =
await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);

// At this stage you get the following parameters from the login

// authzProfile.email
// authzProfile.identityToken
// authzProfile.authorizationCode
// authzProfile.userIdentifier

} catch (ex) {
print(ex);
}
}

After your login with Apple, you get the following params in the authorization profile

- User e-mail
- Identity Token (JWT Token)
- Authorization Code (Used for initial login at the back end to fetch the refresh token)
- User Identifier (Fixed for each user, you will use this to look up your backend mapping table for fetching email for the given user-identifier, mentioned in the previous sign-up step.)

Step-3: Enabling backend token verification

As I mentioned earlier our backend is implemented in Java, we need the below dependencies to be able to verify the identity token and get the refresh token given the authorization code.

Backend implementation is a three-step validation process

  • Validating the received identity token (JWT Token) using the Apple public key
  • Authorizing the user using the authorization code to fetch the refresh token during the initial login
  • Use the refresh token subsequently to verify the validity of the user.

Identity JWT Token Validation

Before we delve into identity token validation, we need to fetch the Apple public key for verifying the ID Token

Apple Public Key

Use this URL to fetch the Apple public keys. The API returns multiple public keys to the application, We need to check the “kid” (please do not confuse it with the kid which we noted in the previous step, that is the private key id, this is the public key id).

The identity token that we got on the application will have the public kid in its header, we need to match that kid with the retrieved key list to get the right public key.

Note: When you decode the JWT token (identity token you got on the app), use jwt.io to see the JWT header with the kid.

https://appleid.apple.com/auth/keys

Returns :
{
"keys": [
{
"kty": "RSA",
"kid": "W6WcOKB",
"use": "sig",
"alg": "RS256",
"n": "<public key>",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "fh6Bs8C",
"use": "sig",
"alg": "RS256",
"n": "<public key>",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "YuyXoY", <<<<<<<<<<<< this is the public key identifier we need
"use": "sig",
"alg": "RS256",
"n": "<public key>",
"e": "AQAB"
}
]
}

Validation

Required fields that are used in the subsequent sections.

Note: Refer to AuthorizationCredentialAppleID which we got on the Flutter app for these parameters, TEAM_ID & KEY_ID you got it in the apple developer portal.

private static final String P8_KEY_FILE_PATH = 
"<PATH WHERE THE P8 CERT FILE STORED \
ex: /tmp/signin.apple.authkey>";
// Replace with the actual path to the P8 key file

private String OAUTH_APPLE_TEAM_ID =
"<DEVELOPER ACCOUNT TEAM ID>";

private String OAUTH_APPLE_KEY_ID =
"<KEY ID WHICH WE CREATED IN PREVIOUS STEP>";

private String ID_TOKEN =
"<JWT TOKEN WE GOT ON THE APP, SENT TO BACKEND FOR VEIRIFICATION>";

private String AUTHZ_CODE =
"<AUTHORIZATION CODE WE GOT EARLIER>";
public PublicKey getAppleIdPublicKey(String kid) {
//Fetch the public key
String applePublicKeys = getUrlContents("https://appleid.apple.com/auth/keys");

JSONObject applePublicKeysObject = toJsonObject(applePublicKeys);
JSONArray keys = (JSONArray) applePublicKeysObject.get("keys");
try {
for(int idx=0; idx < keys.size(); idx++) {
JSONObject keyInstance = (JSONObject) keys.get(idx);
//get the public key id
String keyIdType = keyInstance.get("kid").toString();
//If it matches with the id token kid
if (keyIdType.equals(kid)) {
//Get the key
System.out.println(keyInstance.get("n"));
String modulus = keyInstance.get("n").toString();
String exponent = keyInstance.get("e").toString();

//Create the Java RSA PublicKey Instance with the key retrieved
BigInteger bModulus, bExponent;
KeyFactory factory = KeyFactory.getInstance("RSA");

bModulus = new BigInteger(1, Base64.decodeBase64(modulus));
bExponent = new BigInteger(1, Base64.decodeBase64(exponent));
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(bModulus, bExponent);
return factory.generatePublic(publicKeySpec);
}

}
} catch(NoSuchAlgorithmException | InvalidKeySpecException ex) {

}

return null;
}


private boolean verifyIdentityToken(String idToken, PublicKey publicKey) {
Claims idClaims = Jwts.parserBuilder()
/* This is the public key we got in the previous method*/
.setSigningKey(publicKey)
.build()
.parseClaimsJws(idToken)
.getBody();

Date expDate = idClaims.getExpiration();
Date now = new Date(System.currentTimeMillis());
if (!expDate.before(expDate)) {
System.out.println("Expired Token");
return false;
}

System.out.println("Valid Token");
return true;
}

Authorizing the user

In the previous section we finished validating the received identity token, with that we are sure that the received identity token is not tampered with and is indeed generated by the Apple login authentication.

In this step, we proceed with the user authorization by invoking the API to validate the received authorization code by the Apple authentication servers.

The below-working code summarizes authorizing the user against the Apple auth servers. The following steps are involved in authorizing the user.

  1. Parse the downloaded key certificate (Recall the p8 file we fetched earlier) to create the PrivateKey instance, we need this to sign the client secret JWT we generate in step-2.
  2. Generate the client secret (This is again JWT which we create to send it to Apple servers for validation). It's a way we put all our auth parameters and sign and send it to Apple servers.
  3. Authorize user by invoking API: https://appleid.apple.com/auth/token
  4. Save the refresh_token in your DB against the user login (or) send it to the app to send to our server for validation.
  5. Validate refresh_token using API: https://appleid.apple.com/auth/token
//Main Driver method
private void appleAuth() throws Exception {

//STEP-1: VALIDATE THE IDENTITY TOKEN
//using auth0 library to parse and get the private key id
DecodedJWT decodedJWT = JWT.decode(ID_TOKEN);
String jwtAlgorithm = decodedJWT.getAlgorithm();
String keyId = decodedJWT.getKeyId();


System.out.println("Validating the apple id user identity token");
//Get the public key
PublicKey applePublicKey = getAppleIdPublicKey(keyId);
//Verify the identity token using the retrieved public key
verifyIdentityToken(ID_TOKEN, applePublicKey);

//STEP-2: AUTHORIZE THE USER
//Generate the client secret
String clientSecret = generateClientSecret();
System.out.println("Generated Token: " + token);

//Http Requests Client
AppleTokenResponse tokenValidationResponse = authorizeUser(clientSecret, AUTHZ_CODE, false);
System.out.println(tokenValidationResponse);

}

private String generateClientSecret() throws Exception {
PrivateKey pKey = generatePrivateKey();

return Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, OAUTH_APPLE_KEY_ID)
.setIssuer(OAUTH_APPLE_TEAM_ID)
.setAudience("https://appleid.apple.com")
.setSubject("<Application ID>")
.setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(pKey, SignatureAlgorithm.ES256)
.compact();
}

private PrivateKey generatePrivateKey() throws Exception {
try {
// Read the contents of the P8 certificate file
byte[] keyBytes = Files.readAllBytes(Paths.get(P8_KEY_FILE_PATH));
String privateKeyContent = new String(keyBytes);

// Extract the private key from the P8 certificate file
String privateKeyPEM = privateKeyContent
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");

byte[] decodedKey = Base64.decodeBase64(privateKeyPEM);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
return privateKey;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

AppleTokenResponse authorizeUser(String clientSecretJWT, String authzCode, boolean refreshToken) throws ClientProtocolException, IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("https://appleid.apple.com/auth/token");

httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded");

List<NameValuePair> params = new ArrayList<NameValuePair>();

params.add(new BasicNameValuePair("client_id", "<your app-id>"));
params.add(new BasicNameValuePair("client_secret", clientSecretJWT));


if (refreshToken == true) {
params.add(new BasicNameValuePair("grant_type", "refresh_token"));
params.add(new BasicNameValuePair("refresh_token", authzCode));
} else {
params.add(new BasicNameValuePair("code", authzCode));
params.add(new BasicNameValuePair("grant_type", "authorization_code"));
}

httpPost.setEntity(new UrlEncodedFormEntity(params));

HttpResponse httpResponse = httpClient.execute(httpPost);
int responseCode = httpResponse.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8);

System.out.println("RECEIVED STATUS: " + responseCode);
System.out.println("RESPONSE: " + responseBody);

if (responseCode == HttpStatus.SC_OK) {

AppleTokenResponse tokenResponse = this.toObject(responseBody, AppleTokenResponse.class)
.setSuccess(true);

JSONObject responseBodyJson = toJsonObject(responseBody);
System.out.println("RESPONSE JSON: " + responseBodyJson.toJSONString());
return tokenResponse;

}

AppleTokenResponse tokenResponse = new AppleTokenResponse().setSuccess(false);

return tokenResponse;
}

This summarizes the workflow to integrate the Apple id authentication for the Flutter app with Java back end.

References

https://chat.openai.com/

--

--