Generating Apple client_secret with Dart

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:

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 https://appleid.apple.com

When you download your key file (which you can only do once), it will download with .p8 extension. I rename this file to apple_private_key.pem.

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:

Structure for the apple_client_secret app

We now need to add a few dependencies to pubspec.yaml as follows:

  • under the dependencies section, add jose: ^0.3.0-nullsafety.2
  • also under dependencies, add path: ^1.7.0
  • under the dev_dependencies section, add test:

That’s it for the setup. JOSE is the Javascript Object Signing and Encryption plugin which is needed for the client_secret generation. The version is very important as earlier versions had a bug in the underlying X.509 plugin that prevented JOSE from reading PEM key files.

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 class. 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 assets/keys.

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 DateTime class 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 ES256 which 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:

JsonWebToken

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_grant or 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.

Retired geek having fun