Blog

Like Google, Apple introduced an authentication system called Sign in With Apple .

This means that users who have Apple ID can sign in or login to a web or an app service using their existing credentials.

This article will help you to understand and implement Sign in With Apple feature into your Symfony application.

Who is this article for?

This article is useful for PHP backend developers. It has been written for Symfony framework but it can be used for other frameworks as well (with some changes).

This article is not for beginners and will not explain the basic setup of Symfony. To know more about Symfony, you can find the Symfony documentation here.

If you would like to follow this tutorial, some previous knowledge is required. More specifically, we are talking about the knowledge of:

1. PHP OOP

2. Symfony

Benefits of the Apple Sign in authentication

Apart from using the existing login service provider UI, users now have an option to "privately" sign in to a service.

What that means is that Apple will hide real user email and generate a new one that will need to be used in a service.

This will also prevent trackers from other platforms to track what kind of apps or services you are using.

If you are using an Apple device, sign in can be achieved via Touch ID or Face ID that is a quicker way of signing in.

Is Sign in with Apple mandatory?

If you're dealing with an API that requires an authentication of iOS devices, then the answer is yes.

At this point, Apple is forcing their new or existing apps that have any kind of a „sign in“ feature to also implement Sign in with Apple.

How Sign In with Apple works:

The server side authentication is very easy to understand as most of the job is on the Client App side. Client App (mobile or frontend app) will connect to an Apple server and will send the user's email and password. In this step, the user will be asked if he would like to hide his email.

Apple will then return the user data to the Client App and that data will be sent to the Server. The most important thing here is the authorization_code.

What the server is actually doing is verifying that the authotization_code is valid and that the user exists.

Since the user can hide his email, we need to be aware that we will not get the user's real data - except in the case when the real data is neccessery and we ask the user that later.

Steps:

  1. App will send us an authorization_code on the backend server
  2. Backend server will make a request to an Apple server
  3. Apple server will return a response

Requirements:

  • The credentials from the Apple Connect: Apple Connect is an Apple service for handling iOS apps. There, we can get all the credentials that are needed for telling Apple that we are authorized to check users authorization_code.
  • Save Apple ID – this is needed to find user for login.
  • patrickbussamann/oauth2-apple - library that will handle most of the logic for us

NOTICE!

You will get the user email only upon registration, later during login only the ID will be shown in response.

Apple Credentials and where to find them

The credentials can be found on Apple Cloud Account but from the backend developers side, usually you can just ask someone from the mobile department to give you those. Otherwise, if you have access to the App Store Connect for the app, you can find the credentials under your app settings.

You will need the following:

  1. Apple Key ID https://developer.apple.com/account/resources/authkeys/list
  2. Apple Team ID https://developer.apple.com/account/#/membership/
  3. Apple Client ID – This is your client ID
  4. Key File Path -> you will need to download file on https://developer.apple.com/account/resources/authkeys/list and set path to this file

It is important that you enable Sign in with Apple under Certificates, Identifiers & Profiles .

We have put the credentials for our app in .env file of our project:

Adding Apple ID to the database is pretty simple in Symphony. If you are using a classic MVC architecture just use maker and add that field to an Entity (usually on User entity but if you have another Entity where you store that kind of data, that’s ok).

Installing oauth2-apple library to app:

This requires only one step:

  1. Type composer require patrickbussamann/oauth2-apple in your terminal

This will install a package that contains everything you need to verify with the user authorization code.

Implementation of the PHP part

We are using Factory method pattern here. To find out more about this design pattern, follow this link.

In Factory, we will create our provider that will handle the request for us. In this part, we will need to pass the Apple Credentials to the constructor of the provider.

class AppleProviderFactory
{
    private const FILE_CREDENTIALS_PATH = 'path/to/key/file;

    /**
     * @param string $clientId
     * @param string $teamId
     * @param string $keyFileId
     * @param string $redirectUrl
     * @return Apple
     * @throws Exception
     */
    public static function createProvider(
        string $clientId,
        string $teamId,
        string $keyFileId,
        string $redirectUrl
    ): Apple {
        return  new Apple([
            'clientId'          => $clientId,
            'teamId'            => $teamId, //https://developer.apple.com/account/#/membership/ (Team ID)
            'keyFileId'         => $keyFileId, //https://developer.apple.com/account/resources/authkeys/list (Key ID)
            'keyFilePath'       => self::FILE_CREDENTIALS_PATH, //__DIR__ . '/AuthKey_template.p8'->Download key above
            'redirectUri'      => $redirectUrl //The destination URI the code was originally sent to.
        ]);
    }
}

Apple Authenticator is our class that will hold the logic of getting users' data.

It is important to wrap logic in „try catch“ method and handle errors ourselves.

If verification fails, you will get an Apple response code that you can google and find the solution to.  

The most problematic part is that the authorization_code has a time frame and it can be used only one time so it is good to have an iOS or a Frontend dev on stand-by when you will be implementing this.

class AppleAuthenticator
{
    private const AUTHORIZATION_CODE_NAME_REFERENCE = 'authorization_code';
    private const CODE_NAME_REFERENCE = 'code';
    /**
     *
@var Apple
    
*/
   
private Apple $provider;

    /**
     * AppleAuthenticator constructor.
     *
@param Apple $provider
     */
   
public function __construct(Apple $provider)
    {
        $this->provider = $provider;
    }


    /**
     *
@param string $authorisationCode
     *
@return ResourceOwnerInterface
    
* @throws AppleServerException
    
*/
   
public function getUserData(string $authorisationCode): ResourceOwnerInterface
    {
        try {
            /** @var AccessToken $token */
           
$token = $this->provider->getAccessToken(self::AUTHORIZATION_CODE_NAME_REFERENCE, [
                self::CODE_NAME_REFERENCE => $authorisationCode
            ]);

            $user = $this->provider->getResourceOwner($token);
        } catch (Exception $exception) {
            throw new AppleServerException($exception);
        }

        return $user;
    }
}
class AppleAuthenticationService
{
    /**
     *
@var string
     */
    private string
$clientId;

    /**
     *
@var string
     */
    private string
$teamId;

    /**
     *
@var string
     */
    private string
$appleKeyId;

    /**
     * AuthenticationService constructor.
     *
@param string $clientId
     *
@param string $teamId
     *
@param string $appleKeyId
     */
    public function
__construct(
        string $clientId,
        string $teamId,
        string $appleKeyId
    ) {
        $this->clientId = $clientId;
        $this->teamId = $teamId;
        $this->appleKeyId = $appleKeyId;
    }

    /**
     *
@param string $authorizationCode
     *
@param string $redirectUrl
     *
@return ResourceOwnerInterface
     *
@throws AppleServerException
     */
    public function
authenticate(string $authorizationCode, string $redirectUrl): ResourceOwnerInterface
    {               //create provider
        $applePrpvider = AppleProviderFactory::createProvider(
            $this->clientId,
            $this->teamId,
            $this->keyFileId,
            $redirectUrl
        );
        // pass provider and authorization_code to Authenticator and get users data

In your handler, with the autowiring, you can access the authentication service like this:

/**
 *
@var AppleAuthenticationService
 
*/
private AppleAuthenticationService $authenticationService;
/**

 * CreateAppleUserHandler constructor.

 * @param AppleAuthenticationService $authenticationService

 */

public function __construct(

    AppleAuthenticationService $authenticationService

) {

    $this->authenticationService = $authenticationService;

} 
$user = $this->authenticationService->authenticate(
    'auth_code'
    'redirect_url'
);

To handle the Sign in and login a bit easier, we choose to return to the user our JWT authentication token on response.  

Conclusion

Sign in With Apple is a secure and a fast way for iOS users to register and use services that require authentication.

The assumption is that, as time passes, it will be normal to see it alongside with the web Google and Facebook login.

It is fairly easy to implement this once you get the idea of how it works.

This article has explained how all this works step-by-step, so everything should work for you, just remember this:

  1. Email can be private
  2. Email will be shown only the first time
  3. Authorization_code has a timeframe of working

Happy coding!