One of the first things you have to do for virtually any new app is some form of authentication. For many, that means supporting one of the social network authentication systems. This is the “Signin with <pick your provider>”. When I started looking at this with Dart/Flutter, I ran into a few issues. First, most of the client plugins that support social signin only support the mobile platforms (iOS/Android). I wanted to support Desktop/Web as well. Also, many of the client (Flutter) plug-ins expect you to supply a client_scecret. It’s never a good idea to expose your client_secret on the client side of the app.
I decided to dig into the finer details of OAuth2 and see if I could write my own authentication system that would support all flutter clients (mobile, desktop and web). To be secure, I’d do most of the authentication using a homegrown Dart server. This allows me to keep the client_secret where it belongs, but also made support for “Signin with Apple” possible. I did get authentication working for Google, Facebook, Github, and Apple. One code base with only a few differences to handle the quirks of each provider. Apple was by far the most challenging.
Apple’s support for OAuth2 has a number of differences compared to all the other social providers. Most providers (Google, Github, Facebook) generate the client_id and client_secret for you. You just need to keep them in a secure place on the server. In the Apple process, you first download a private key, and then generate a client_secret using that private key. The other different with Apple is that they don’t support using
localhost as a redirect. In the end, I initiate the OAuth2 process on the Flutter client; but all the redirects and the token exchange process happen on the server.
If you want to support “Signin with Apple” and you’re just getting started, I highly recommend you read this article by Aaron Parecki:
What the Heck is Sign In with Apple?
This week at Apple's developer conference WWDC, Apple announced a new feature, "Sign In with Apple" enabling users to…
Aaron does a great job of walking you through the entire process of getting ready to support “Signin with Apple”. Towards the end of the article, you’ll see a section where Aaron generates the client_secret using a Ruby script. Fundamentally, this article is about implementing that Ruby script using Dart.
To generate the client_secret, you’ll need these components:
- The TeamID (also called AppID prefix). This will be in your App ID identifiers
- The KeyID. This will be in the keys section when you download your private key
- The ClientID. In Apple land, this is the Services ID
- The Audience which will always be
When you download your key file (which you can only do once), it will download with .p8 extension. I rename this file to
With all the components in place, let’s create a small Dart app to make all this work.
Create a new Dart project with
dart create apple_client_secret.
In this project folder create a new folder called assets, and under that folder, create one called keys. Put your
apple_private_key.pem file in this folder. Your structure should look like this (I use VSCode). Also notice I added a
test folder which we’ll get to later:
We now need to add a few dependencies to
pubspec.yaml as follows:
- under the
- also under
- under the
With all the setup complete, now let’s write the code to make this all work. The Dart project creation process will have generated the
apple_client_secret.dart file in the
bin folder. This file will be very minimal and contain only the following:
All the real work will be in the
utils.dart file. Go ahead and create that file in the
bin folder. What follows is the content of the
utils.dart file. You can type it in, or copy paste. Then, i’ll explain each section in the text that follows the code.
After the import statements is a simple
extension on the
DateTime only provides
millisecondsSinceEpoch and we need
secondsSinceEpoch. This is just a convenience to make the code below a tiny bit cleaner.
What follows is the main
Utils class with a number of static functions. You’ll need to plug in all the right values you get from the Apple developer site for
KEY_ID, TEAM_ID, and CLIENT_ID.
Let me try to explain why I did it this way. The first static function in the class is
get projectFolder. Normally, you’d like just use
Platform.script to get that location. That will work until you try to do unit testing. In order for this to work at runtime AND during unit testing, you have to use introspection and locate the
Utils class (and from there, the project folder). The unit test we run later will work just fine as a result of getting the
projectFolder this way.
The next static function is
get pemKey. This is just a utility function to locate and read our private key file which is located under
The last function is money function. If you read through Aaron’s article, it’s a pretty close replication of that Ruby script.
appleClientSecret() uses the JOSE plug in to generate the
client_secret that conforms to Apple’s requirements. Here is the sequence:
- Generate a JsonWebKey using the private key PEM file, and the KeyID from Apple.
- Generate the claims required by Apple. The
iat(issued at) is just ‘now’, and I chose 300 seconds (5 minutes) for the expiration. Notice this also where we use the extension on the
DateTimeclass we created earlier.
- Then, we prepare a Json signature builder by combining our claims with the JsonWebKey and we need to specify the algorithm as
ES256which is also specified by Apple.
- Finally, we build the JsonWebToken using compactSerialization.
Save all this work, and go ahead and run the app like this:
dart run bin/apple_client_secret.dart
You should get an output on the console that looks something like this:
The web token in the image above won’t work since I randomly scrambled it. But, if you get an output like this, then all looks good. You should be able to go to
jwt.io and paste in the token and see all of your claims. If you copy/paste your key, you should be able to validate the signature as well.
But, let’s create a couple of tests to make sure our client_secret is generated properly. In the
test folder, create a file called
apple_client_secret_test.dart. What follows is the content of the test file:
There are two tests to verify that the client_secret is properly generated. The first test uses the JOSE plugin to verify that the signature is valid on our token. The second test will simply ensure that the claims in the token match the claims that we put in there.
That’s it. The final test is to try to get an access_token and id_token from apple. I can tell you that this is the code I use to do just that, and I have no issues. If Apple returns an error like
invalid_client, then you need to double-check that all the values for keyId, clientId, teamId and the rest are exactly correct. There is no room for error — trust me. It took a while for me to get all this to work.
Good luck getting Signin with Apple to work. Hopefully this little bit of code will make it easier.