Share authentication state across your apps, App Clips and Widgets (iOS)

Seamlessly stay logged inn and share your authentication state across your apps, App Clips and Widgets.

Thomas Asheim Smedmann
10 min readJan 6, 2022
Shared authentication state between apps, App Clips and Widgets

A modern and well equipped app often has some sort of sign in, and might provide both App Clips and Widgets to enrich the user experience. Such apps might want to persist authentication state in a shared way, in order to provide the best user experience possible. For instance — you do not want your users to sign in again after beeing onboarded by your App Clip. Or have a Widget fail to update since it can’t read the user’s authentication state. And sometimes it might even be useful to extend this across apps, and have completely different apps share the user’s authentication state.

OAuth 2.0 is the industry-standard protocol for authorization, and is often used with OpenID Connect on top to authenticate and authorise end-users. There is a lot of services that provide authentication and authorisation (often called AaaS — Authentication as a service), with these technologies at their core. One such service is Auth0.

A lot of these services provide their own client libraries, some of them do not. So if you are using a paid OAuth 2.0 service in you app, or have chosen to make your own authorisation server, you might end up needing a client library to use when interacting with that service. One such client library is AppAuth.

In this article we will use Auth0 as the authorisation server, and AppAuth as our OAuth 2.0 client library. But this is an arbitrary choice, and doesn’t really impact what this article is mainly about: sharing of authentication state across apps, App Clips and Widgets.

Key takeaways

If you want to get your hands on the complete example code right away, it can be found here: https://github.com/thomsmed/ios-examples/tree/main/SharedAppAuthState

The example app

In our example we’ll use AppAuth as our OAuth 2.0 client library. This library will do the heavy lifting when interacting with Auth0. Auth0 will be our OpenID Connect/OAuth 2.0 authorisation server, and will act as the entity that provides our app with Access Tokens to use when accessing resources over the network.

Our Xcode project will contain 3 targets and one extension:

  • The main app
  • An App Clip associated with the main app
  • A Widget associated with the main app
  • And a secondary app that will share authentication state with the main app
Xcode project containing 3 targets and one extension
Main and secondary app, App Clip and Widget

The base

As mentioned, we’ll create four targets and one extension under our Xcode project. The main app will have minimal UI-code, the same goes for the secondary app, App Clip and Widget. It doesn’t really matter. The meat of this example lies in the persistence and sharing of the user’s authentication state. But for those interested in following along, links to the minmalistic UI-code is listed below (for the UIKit-code I’m using TinyContraints to define Auto Layout Constraints).

We’ll come back to the Dependencies.swift files referenced in the UI-code.

Also, as mentioned, the complete source code for the project can be found here: https://github.com/thomsmed/ios-examples/tree/main/SharedAppAuthState

AuthStateRepository

At the absolute core of our example we’ll define a protocol that abstract away the resposiblity of persisting and managing the user’s authentication state. Since we are using AppAuth as our OAuth 2.0 client library, the user’s authentication state is represented as an instance of OIDAuthState. Our protocol will act as a repository for this type of object.

The protocol exposes the stored OIDAuthState object as a get-only property, as well as methods for persisting and clearing the state.

KeychainAuthStateRepository

The good way to store a shared authentication state is in a shared Keychain. iOS apps have access to a single Keychain through the Keychain Services API. By default, apps only have access to Keychain Items under the app’s default access group (“<teamId>.<bundleIdentifier>”, e.g.: “12345ABCDEF.com.mydomain.myapp”).

In order for Keychain Items to be shared across apps/targets, we will have to define a shared Keychain Access Group. Keychain Items under this access group can be accessed by all apps/targets that is part of this access group.

We can define a Keychain Access Group in Xcode by selecting our main app target under the project, then add a new capability called “Keychain Sharing” under “Signing and Capabilities”. This will add a new “Keychain Access Groups”-entitlement for the app, telling the system (iOS) that this app wants to be part of this Keychain Access Group.

Shared Keychain Access Group: com.mydomain.shared

Do this for both the main and secondary app targets, as well as for the Widget extension target. App Clips does not have access to shared items in the Keychain (more on this below).

Next; let’s create a KeychainAuthStateRepository, and have it adapt our AuthStateRepository protocol. That way we can store the OIDAuthState object representing the user’s authentication state securely in the Keychain.

Notice that KeychainAuthStateRepository accepts a second AuthStateRepository called “migrationRepository”, and will check this repository for any stored state before checking the Keychain. If the “migrationRepository” contains a state, that state will be moved (or “migrated”) over to the Keychain. We’ll come back to this more in details later.

SharedUserDefaultsAuthStateRepository

App Clips do have access to the Keychain Services API, but Keychain Items created by App Clips can not be accessed when the associated app is installed and the App Clip deleted (check out this post at the Developer Forums for more insight). Because of this, it is not possible to share Keychain Items between app and App Clip. But, as described in this Apple Developer article, we can use a shared UserDefaults database as an alternative.

To store and share the user’s authentication state between app and App Clip, using a shared UserDefaults database, we’ll have to start with creating an App Group. App Groups lets us share data, and communicate between our apps (IPC — Inter-process Communication, and more).

To register and create a new App Group through Xcode, select the main app target under the project, then add a new capability called “App Groups” under “Signing and Capabilities”. This will add a new “App Groups”-entitlement for the app, telling the system (iOS) that this app wants to be part of this App Group. Xcode will also make sure to register this App Group under your Apple Developer Team. So be sure to check out your new App Group under your team’s Certificate, Identifiers & Profiles.

New App Group called “group.com.mydomain.shared”

Make sure to do this for the main app and the App Clip, since we only need the App Clip to share its authentication state with its associated app.

Next; let's create a SharedUserDefaultsAuthStateRepository, and have it adapt our AuthStateRepository protocol. That way we can store the OIDAuthState object representing the user’s authentication state in a UserDefaults database under our new App Group.

This repository will be provided as the main authentication state store for the App Clip, and as the migration store for the main app. That way the primary Keychain store can migrate any state from the shared UserDefaults into the Keychain, and make it available for the other apps/targets.

AuthService

To provide login and logout functionality we’ll create a protocol called AuthService. This will wrap all details about using AppAuth to sign users in/out, and AuthStateRepository to manage the user’s authentication state.

The protocol exposes methods for signing in/out, as well as a simple boolean to tell if the user is already signed in.

Auth0AuthService

Since we are using Auth0 as our authentication server, or AaaS (Authentication as a Service) as it’s often called, we’ll create a Auth0AuthService class that implements our AuthService protocol. This entity will take care of all the work regarding authentication of the user, as well as making sure to persist the user’s authentication state.

I will not go in great detail about this class, what is important is that this class uses our AuthStateRepository to fetch and persist the user’s authentication state. Feel free to check out the documentation of AppAuth and Auth0 for more details on how to configure and setup your client code.

Note: Auth0 actually has its own iOS client library, but since it’s all OpenId Connect and OAuth 2.0, we can use whatever OAuth 2.0 client library we want!

AuthTokenProvider

As a clean way to provide the app’s API-services (or whatever) with fresh Access Tokens from the user’s authentication state, we’ll create a protocol as an abstraction layer. We’ll call it AuthTokenProvider and it will expose one method, performWithFreshToken(_ action:), that will take a closure whom will be called with a fresh Access Token as input (or an Error if no token is available).

Auth0AuthService+AuthTokenProvider

Since our Auth0AuthService already has a reference to the user’s authentication state through an instance of AuthStateRepository, we’ll just add an extension that adheres to AuthTokenProvider. And for our convince, the stored OIDAuthState object already has a method for getting a fresh Access Token.

We could also had our KeychainAuthStateRepository implement the AuthTokenProvider protocol. It’s just a matter of personal taste.

WidgetAuthTokenProvider

Since it does not make any sense to include a AuthService in our Widget (users can’t sign in through our Widget), we’ll provide a custom AuthTokenProvider instead. With it our Widget’s API-services (or whatever) can get fresh Access Tokens when updating it’s data.

The WidgetAuthTokenProvider accepts a AuthStateRepository as input to it’s initialiser, and uses it to fetch the persisted OIDAuthState and to get fresh Access Tokens.

The final step

As a final step, we need to provide our AuthService and AuthTokenProvider to the rest of the app (or Widget/App Clip). We’ll expose these as static properties on a struct named Dependencies.

Dependencies.swift for main app

Our main app will use the SharedUserDefaultsAuthStateRepository as a migration repository for KeychainAuthStateRepository. That way any existing authentication state stored by the App Clip will be migrated to the Keychain, and the user will stay signed in after downloading the associated app.

Dependencies.swift for the App Clip

Since App Clips can’t share Keychain Items with its associated app (as mentioned earlier), the App Clip will provide the AuthService an instance of SharedUserDefaultsAuthStateRepository as the AuthStateRepository.

Dependencies.swift for the Widget

The Widget only need a AuthTokenProvider to keep its data up to date, as well as an instance of KeychainAuthStateRepository to provide as a AuthStateRepository to the WidgetAuthTokenProvider.

Dependencies.swift for secondary app

The secondary app will have an instance of KeychainAuthStateRepository configured to access Keychain Items under the same Keychain Access Group as the main app. That way any existing authentication state stored by the main app, will already be available to the secondary app when the user downloads it. Saving the user form the hassle of logging in when opening the app for the first time!

With this, our example is complete! A user can now sign in when first using an App Clip, stay signed in when installing the App Clip’s associated app, have Widgets stay up to date with access to a shared authentication state, and automatically be signed in after downloading a second app with access to the shared authentication state in the Keychain.

Some last words about App Groups and Keychain Access Groups

iOS apps are sandboxed and has only access to resources under its own directory (“/var/mobile/Containers/Data/Application/<applicationId>”). When defining one or more App Groups your app is part of, the app get access to resources under those groups as well (located at “/private/var/mobile/Containers/Shared/Shared/AppGroup/<appGroupId>”). The URL to these folders can be obtained by calling FileManager.containerURL(_:) for each App Group.

By default every iOS app has access to Keychain Items under the app’s default Keychain Access Group (“<teamId>.<bundleIdentifier>” e.g.: “12345ABCDEF.com.mydomain.myapp”). But apps can get access to shared Keychain Items by enabeling the Keychain Sharing capability in Xcode, and add a string/identifier for the shared Keychain Access Group.

Actually, when you are using App Groups the app automatically get access to a Keychain Access Group with the same name as the App Group (minus the Team Id), e.g.: “group.com.mydomain.shared”. Which means if you are using App Groups in your app, you don’t need to specify an additional Keychain Access Group to be able to share Keychain Items!

You can read more about all of this in the Apple Documentation.

Summary

In this article we have seen how to persist and share authentication state across apps, App Clips and Widgets. With Auth0 as our authorisation server and AppAuth as our client OAuth 2.0 library. We let the user sign in with our App Clip, continue to be signed inn after installing the full app, and have the app and Widget stay up to date with a shared authentication state. The user is even already signed in after installing a second app that shares the user’s authenticated state with the main app.

Though this example looks at how to share authentication state, the same example code can be used to persist all sorts of data one would like to share across apps, App Clips and Widgets. And the fact that this example uses AppAuth and Auth0 is an arbitrary choice, and doesn’t really matter. The meat lies in how to persist and share the authentication state.

As mentioned at the start of this article, the complete source code can be found here: https://github.com/thomsmed/ios-examples/tree/main/SharedAppAuthState

Happy coding!

--

--