Sign in with Apple Java User Verification

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
}

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")
    }
}