Sign In With Apple and Google in Flutter on All the Platforms
Some time ago I decided to add Apple and Google sign in buttons to one of my Flutter apps. The idea was like this: user signs in using one of these OAuth2 providers, the app gets the server auth code, sends it to our server (written in C#, .NET), our server exchanges the code for the token, takes the validated email from the token, and finally issues our own access token for the user with the given email (because we have several sign in methods and add some app-specific claims to the tokens).
My app is available on all the platforms that are supported by Flutter (Android, iOS, macOS, Linux, Windows, and web). So, I started looking for the libs and realized that none of them support all the platforms.
The sign_in_with_apple package works on Android, iOS, macOS, and web. The google_sign_in only supports Android, iOS, and web (but on web you can’t get server auth code to exchange it for a token on server side, so for me it means that web is not supported). And neither of them supports desktop.
There was no quick and simple solution. After few days trying to find the best one, I decided to use the combination of next 3 approaches.
Apple sign in on iOS and macOS, Google sign in on Android
In this case I use the native system sign in dialogs provided by the sign_in_with_apple and google_sign_in packages. It is important, because the iOS and macOS users will more likely sign in with Apple, so they have usual experience. The same is valid for Android devices.
Client-side, Flutter
Server-side, C#, .NET
I use 2 packages on the server-side: AppleAuth.NET and Google.Apis.Auth.
I followed the instructions and didn’t have any issues with the AppleAuth.NET (except the one because I didn’t notice some IIS setting from the docs and had strange “File not found” exception).
It was not easy for me to find the correct way of working with the Google Auth. It took several hours to find some examples and write this code:
Apple sign in on Android, Google sign in on iOS and macOS, and all the desktops
I decided not to rely on the packages and provide my own standardized sign in experience for all the non-native (from point of view of the OAuth2 provider) platforms. The flow is the following.
1. User clicks the sign in with Apple button.
2. The app creates an empty email validation token on the server and gets its ID.
3. The app opens a browser window and navigates user to a small auth service.
The app provides the auth service with the sign in provider name (Apple or Google) and the email validation token ID received on the previous step as URL parameters. The full URL might look like this: https://auth.youapp.com?provider=apple&token={GUID}.
It is important to open the link in the default browser, not in the embedded web view, otherwise it will be very inconvenient and not obvious for a user how to close it.
4. The app starts checking the email validation token state every 3 seconds.
(There should be also a timeout, but it is not important now.)
This code illustrates the first 4 steps:
5. The auth service receives the request, looks at the passed provider name, generates an OAuth2 sign in screen URL for the selected provider (in our case it is Apple), and redirects user there.
It is important, that the email validation token ID (the “token” parameter’s value) set as the “state” parameter’s value while generating the URL. (As you can see, the platform is also sent in the “state” parameter, it will be explained below.)
As it is OAuth2, the URL parameters are identical, so we can get rid of the code duplication. (This also means that the approach will work with the other OAuth2 providers too.)
6. After user signs in, the OAuth2 provider redirects him back to the auth service using the callback URL (for example, https://auth.yourapp.com/callback) with the OAuth2 token and our own email validation token ID from the “state” parameter.
7 . The auth service gets the user’s email from the OAuth2 token and sets it to the email validation token.
8. The auth service redirects user to a page where it is said that the authentication was successful, and the window can be now closed.
(Or in case of web user is redirected back to the app itself, see below.)
This code illustrates the previous 3 steps:
9. The app (which is checking the email validation token’s state all this time) sees that the email validation token now contains the validated email, so it can make one more server request to create an access token using the email validation token’s ID.
(In my case users also can sign in after the email is validated using the one-time code, so in your case you can even simplify the flow a little bit if you only provide OAuth2 sign in.)
Apple and Google sign in on web
This approach is almost the same as the previous one, but with some minor modifications. On web it would be strange if you open a new page to sign in user and then left it open, while user is signed in on the previous page. (Also, you might face with the pop-up blocking features of the browsers.)
So, when user clicks the sign in button on web, he will be redirected to the auth service directly in the same tab instead of opening the new one. In fact, he will leave the app. After he signs in, the auth service will redirect him back to the app instead of showing the successful sign in message. The email validation token ID will be added as the URL parameter, so the app will use it to issue the access token and sign in user without additional actions.
You could already noticed this in the code above. Let’s look only at the Flutter’s part. This is how the web app URL parameter is processed:
The token
URL parameter is provided by our auth service and contains the email validation token ID with the validated email, so you can just continue with getting the access token and signing in the user.
That’s all
Thanks for reading, please let me know if you have questions, I will try to help.