Adding Apple Sign-In to an Angular SPA
Note: There are several administrative steps that are required to integrate with Sign In with Apple that you need to configure in the App Connect Console. My guide doesn’t go into any detail about that but see this guide for how to set all that up.
Let's set the stage. You’ve got an Angular web app and a native counterpart. You’re ready to upload the native apps to the app stores. The Android App has gone through the Google Play Store review process without a hitch. Now all you need to do is get it through the Apple app store review. Simple, right? Wrong!
You've added Google sign in to reduce friction on your sign up flow, and now Apple comes along and wants a piece of that third-party-sign-in pie so is holding your app hostage until and unless you integrate with their sign in system. You're still cool and collected. You added Google sign in to your system without too much trouble. Surely adding one more third party auth provider can't be that much more work, right? Wrong!
Obviously, if you offer Apple sign in on your iOS app, you’ll need to allow Sign In with Apple everywhere. In this post, we’ll only cover the process of adding Sign In with Apple to an Angular web app. Let’s dive into the anatomy of the Apple Sign In flow and see why it makes things tricky.
Anatomy of Sign In with Apple
Basically, this is the flow of doing sign in with Apple:
User clicks the “Sign In with Apple” button.
Your app navigates to Apple’s Auth page with some metadata about what information your app needs and where it should redirect the user to when it’s done.
User completes the Apple sign in.
Apple sends a FORM_POST request to the redirect url specified.
The user completes the sign up flow for your site.
This flow runs counter to the way SPAs work. With Apple sign in, there’s the implicit assumption that you'll navigate away from your page to Apple's auth page. Then, once the user is done authenticating, Apple redirects back to your site via a POST request with the data you're looking for and a state variable that will allow you to pick up where you left off. If you're anything like me, this is the part where your brain breaks.
Leaving aside the arguably bad user experience of having to leave your app entirely, this flow doesn’t make any sense in an SPA world. The Angular App can’t handle POST requests since it’s just serving static files. So how do we handle the POST request? Also, can we avoid having to navigate away from our app?
Handling the POST Request
As mentioned above, our Angular App can’t handle POST requests so we have no option but to send the request to our API server. One option we have would be to accept the POST request with our API and redirect back to the front end with the data in the URL. This would resolve the immediate problem of getting the data back from Apple to Angular but it comes with its own downsides. Namely, we still have to navigate away from our app and we also need to come up with a system to restore the state of the sign-in flow from URL parameters.
There is another way, though, using a browser API called postMessage. It will allow us to get the data back from Apple, while keeping our app window open and without needing to allow restoring the state of the auth flow from URL parameters. Read on to see how we go about that.
Adding the Sign in with Apple Button
Before getting started with the implementation, I should mention that Apple does provide a drop-in button that you can just add to your page. Unfortunately, it defaults to using the same tab and, as far as I’m aware, there’s no way to make it open in a new tab/window, so we're going custom with it. When the user clicks sign in with Apple, we call window.open() with the Apple authorize url.
We need a few things to construct this url (for the full details, see Apple’s docs):
Client ID:
This is the name of the apple “Service” that you will have created while setting up apple sign in in the app connect console.
Redirect url:
This is the API endpoint that will handle the POST request from Apple. You will also should have configured this while going through the initial setup of the
The scopes you are requesting
you can request one, both or neither of “email” and “name”
Response Type:
This allows you to ask for specific forms information from Apple.
Here’s a vanilla HTML example of a button that will open the apple auth url in a new tab:
<html> <head> <script> function openAppleAuthWindow() { window.open( 'https://appleid.apple.com/auth/authorize?' + `client_id=${YOUR_CLIENT_ID}&` + `redirect_uri=${encodeURIComponent(YOUR_REDIRECT_ID)}&` + 'response_type=code id_token&' + 'scope=name email&' + 'response_mode=form_post', '_blank', ); } </script> </head> <body> <button onclick="openAppleAuthWindow()">Sign In with Apple</button> </body> </html>
The redirect endpoint
Now we need to actually do something with the POST request that our API gets from Apple. This is the part where things get weird. The response to this post request is going to be returned to the user’s browser as though it's a web page. As opposed to all the other endpoints in the API which serve JSON, this will send back html. More specifically, we construct a script tag which executes just enough JavaScript to get the data back to your Angular app and close the window.
Luckily, there’s a handy API called postMessage which allows you to send a message to another tab assuming you have a handle to it. Conveniently, in our case, our current window was opened by the window we want to send a message to.
There is an opener field on window which will have a reference to the tab that opened it (if such a tab exists).
This is what the Express request handler looks like:
api.post(APPLE_SIGNIN_REDIRECT, (req, res) => { res.send(`<script> const MC_APPLE_SIGNIN_AUTH_VALUES = '${JSON.stringify(req.body)}' if (window.opener) { window.opener.postMessage(MC_APPLE_SIGNIN_AUTH_VALUES, '${env.domain}'); window.close(); } </script>`); });
Listening for messages
The last piece of the puzzle is to listen for the message coming back from the opened window. So here we’ll just add an event listener to the window for “message” and that should complete the cycle.
<html> <head> <script> function openAppleAuthWindow() { window.open( 'https://appleid.apple.com/auth/authorize?' + `client_id=${YOUR_CLIENT_ID}&` + `redirect_uri=${encodeURIComponent(YOUR_REDIRECT_ID)}&` + 'response_type=code id_token&' + 'scope=name email&' + 'response_mode=form_post', '_blank', ); // Here is our new message event listener window.addEventListener('message', event => { console.log('Got a message: ' + JSON.stringify(event.data)); }); } </script> </head> <body> <button onclick="openAppleAuthWindow()">Sign In with Apple</button> </body> </html>
Now the data we have in the message event object should be whatever came back from Apple’s redirect and you can do with it what you please.
Final Thoughts
So there it is. Now you can sail through the Apple review process with flying colors, right? Well, who knows the machinations of the mercurial Apple gods? Certainly not me. But at least now you’ll have one more of their demands off your plate.