AWS SDK for Swift integration on Apple platforms
Software running on any of Apple's platforms (macOS, iOS, iPadOS,
tvOS, visionOS, or watchOS) may wish to integrate into the Apple
ecosystem. This may include, for example, letting the user authenticate
to access AWS services using Sign In With
Apple
Authenticate using Sign In With Apple
One convenient way for users to sign into AWS while using your application by adding support for Sign In With Apple. This allows your users to access AWS services using their Apple ID and either TouchID or FaceID.
Adding Sign In With Apple support to your app requires planning and configuration of services:
-
Set up your application in Apple's "Certificates, Identifiers & Profiles
" dashboard, or configure Sign In With Apple using Xcode . -
Set up the application's security and authentication configuration in the AWS Management Console
. -
Add the Sign In With Apple authentication method to your application.
Note
When setting up Sign In With Apple as a way to authenticate to
use AWS services, the JWT audience should always be the same
as your application's bundle ID as configured in Xcode (or the
Info.plist
file) and the Apple developer
portal.
The rest of this section discusses the process of setting up and
using Sign In With Apple for AWS authentication. The example
authenticates to AWS using Sign In With Apple, then lists the
user's Amazon Simple Storage Service (Amazon S3) buckets in a SwiftUI
Configure the app for Sign in With Apple
If you use Xcode, you can enable Sign In With Apple
-
Verify that your team name and bundle identifier are correct for the application. Be sure to use Apple's standard reverse-URI notation, such as
com.example.buckets
. -
Click the + Capability button to open a capability picker. Add Sign In With Apple to your target's capabilities.
-
Click the Automatically manage signing checkbox to enable automatic signing of your application. This also configures your application on the Apple developer portal.
-
Under App Sandbox, enable Outgoing Connections (Client) to have your signed application request network access permission.
If you don't use Xcode, or you prefer to configure the app by
hand, you can do so by following the instructions on Apple's developer website
Configure AWS services for your application
Next, open the IAM
Management Console
-
In the sidebar, click Identity providers, then look to see if there's already a provider for
appleid.apple.com
. If there is, click on it to add your new application's bundle ID as a new audience. Otherwise, click Add provider to create a new identity provider with the following configuration:-
Set the provider type to OpenID Connect.
-
Set the Provider URL to
https://appleid.apple.com
. -
Set the audience to match your application's bundle ID, such as
com.example.buckets
.
-
-
If you don't already have a permissions policy set up to grant the permissions needed by your application (and no permissions it doesn't need):
-
Click Policies in the IAM Management Console
sidebar, then click Create policy. -
Add the permissions your application needs, or directly edit the JSON. The JSON for this example application looks like this:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:ListAllMyBuckets", "Resource": ["arn:aws:s3:::*"] } ] }
This policy allows your application to get a list of your Amazon S3 buckets.
-
On the Review and create page, set the Policy name to be a unique name to identify your policy, such as
example-buckets-app-policy
. -
Enter a helpful description for your policy, such as "Application permissions for the Sign In With Apple buckets example".
-
Click Create policy.
-
-
Create a new role for your application:
-
Click Roles in the IAM Management Console
sidebar, then click Create role. -
Set the trusted entity type to Web identity.
-
Under Web identity, select the Apple ID identity provider you created above.
-
Select your application's bundle ID as the Audience.
-
Under Permissions policies, choose the policy you created above.
-
On the Name review, and create page, set a unique name for your new role, such as
example-buckets-app-role
, and enter a description for the role, such as "Permissions for the Buckets example." -
Review the Trust policy generated by the console. The JSON should resemble the following:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "sts:AssumeRoleWithWebIdentity", "Principal": { "Federated": "arn:aws:iam::
111122223333
:oidc-provider/appleid.apple.com" }, "Condition": { "StringEquals": { "appleid.apple.com:aud": [ "com.example.buckets
" ] } } } ] }This indicates that the Apple ID identity provider used to process Sign In With Apple requests should be allowed to use the role if the web token's
aud
property (the audience) matches the application's bundle ID.
-
Add Sign In With Apple support to your application
Once both Apple and AWS know about your application and that Sign In With Apple should be allowed to authenticate the user for the desired services, the next step is to add a Sign In With Apple button and its supporting code to the application.
This section explains how an application can provide a Sign In With Apple option for authentication, using Apple's SwiftUI and Authentication Services libraries.
Add a "Sign In With Apple" button
To include a Sign In With Apple button, the
AuthenticationServices
module needs to be
imported along with SwiftUI:
import SwiftUI import AuthenticationServices
In your
SwiftUI
sign-in view, embed a Sign In With Apple
button.
The SignInWithAppleButton
is used to trigger and
manage the Sign In With Apple process. This button calls a
completion handler with a Result
object, which
indicates whether or not a valid Apple ID was
authenticated:
// Show the "Sign In With Apple" button, using the // `.continue` mode, which allows the user to create // a new ID if they don't already have one. When SIWA // is complete, the view model's `handleSignInResult()` // function is called to turn the JWT token into AWS // credentials. SignInWithAppleButton(.continue) { request in request.requestedScopes = [.email, .fullName ] } onCompletion: { result in Task { do { try await viewModel.handleSignInResult(result) } catch BucketsAppError.signInWithAppleCanceled { // The "error" is actually Sign In With Apple being // canceled by the user, so end the sign in // attempt. return } catch let error as BucketsAppError { // Handle AWS errors. viewModel.error = error return } } }
This example SignInWithAppleButton
uses a
function named handleSignInResult()
as its
completion handler. This function is passed a
Result
that contains an
ASAuthorization
object if Sign In With Apple
succeeded. If sign in failed or was canceled, the
Result
contains an Error
instead.
If the error actually indicates that sign in was canceled by
the user, the sign in attempt is ended. Actual errors are
stored for display by a SwiftUI alert sheet.
Note
By default, the token returned by Sign In With Apple expires one day from its creation time.
Process the JSON Web Token
A number of variables are used by the example application to store information related to the user and their sign-in session:
/// The unique string assigned by Sign In With Apple for this login /// session. This ID is valid across application launches until it /// is signed out from Sign In With Apple. var userID = "" /// The user's email address. /// /// This is only returned by SIWA if the user has just created /// the app's SIWA account link. Otherwise, it's returned as `nil` /// by SIWA and must be retrieved from local storage if needed. var email = "" /// The user's family (last) name. /// /// This is only returned by SIWA if the user has just created /// the app's SIWA account link. Otherwise, it's returned as `nil` /// by SIWA and must be retrieved from local storage if needed. var familyName = "" /// The user's given (first) name. /// /// This is only returned by SIWA if the user has just created /// the app's SIWA account link. Otherwise, it's returned as `nil` by SIWA /// and must be retrieved from local storage if needed. var givenName = "" /// The AWS account number provided by the user. var awsAccountNumber = "" /// The AWS IAM role name given by the user. var awsIAMRoleName = "" /// The credential identity resolver created by the AWS SDK for /// Swift. This resolves temporary credentials using /// `AssumeRoleWithWebIdentity`. var identityResolver: STSWebIdentityAWSCredentialIdentityResolver? = nil
These are used to record the AWS account information
(the AWS account number and the name of the IAM role
to use, as entered by the user), as well as the user's
information returned by Sign In With Apple. The
STSWebIdentityAWSCredentialIdentityResolver
is used to convert the JWT token into valid, temporary
AWS credentials when creating a service client
object.
The handleSignInResult()
function looks
like this:
/// Called by the Sign In With Apple button when a JWT token has /// been returned by the Sign In With Apple service. This function /// in turn handles fetching AWS credentials using that token. /// /// - Parameters: /// - result: The Swift `Result` object passed to the Sign In /// With Apple button's `onCompletion` handler. If the sign /// in request succeeded, this contains an `ASAuthorization` /// object that contains the Apple ID sign in information. func handleSignInResult(_ result: Result<ASAuthorization, Error>) async throws { switch result { case .success(let auth): // Sign In With Apple returned a JWT identity token. Gather // the information it contains and prepare to convert the // token into AWS credentials. guard let credential = auth.credential as? ASAuthorizationAppleIDCredential, let webToken = credential.identityToken, let tokenString = String(data: webToken, encoding: .utf8) else { throw BucketsAppError.credentialsIncomplete } userID = credential.user // If the email field has a value, set the user's recorded email // address. Otherwise, keep the existing one. email = credential.email ?? self.email // Similarly, if the name is present in the credentials, use it. // Otherwise, the last known name is retained. if let name = credential.fullName { self.familyName = name.familyName ?? self.familyName self.givenName = name.givenName ?? self.givenName } // Use the JWT token to request a set of temporary AWS // credentials. Upon successful return, the // `credentialsProvider` can be used when configuring // any AWS service. try await authenticate(withWebIdentity: tokenString) case .failure(let error as ASAuthorizationError): if error.code == .canceled { throw BucketsAppError.signInWithAppleCanceled } else { throw BucketsAppError.signInWithAppleFailed } case .failure: throw BucketsAppError.signInWithAppleFailed } // Successfully signed in. Fetch the bucket list. do { try await self.getBucketList() } catch { throw BucketsAppError.bucketListMissing } }
If sign in is successful, the Result
provides details about the authentication returned by
Sign In With Apple. The authentication credential
contains a
JSON
Web
Token,
as well as a user ID which is unique for the current
session.
If the authentication represents a new link between Sign In With Apple and this application, it may contain the user's email address and full name. If it does, this function retrieves and stores that information. The email address and user name will never be provided again by Sign In With Apple.
Note
Sign In With Apple only includes personally identifiable information (PII) the when the user first associates their Apple ID with your application. For all subsequent connections, Sign In With Apple only provides a unique user ID. If the application needs any of this PII, it's the app's responsibility to securely save it locally and securely. The example application stores the information in the Keychain.
The JWT is converted to a string, which is passed to a
function called authenticate(withWebIdentity:
region:)
to actually create the web identity
resolver.
Create the web identity credential identity resolver
The authenticate(withWebIdentity:
region:)
function performs credential identity
resolution by first writing the token to a local file,
then creating an object of type
STSWebIdentityAWSCredentialIdentityResolver
,
specifying the stored web identity token file when doing
so. This AWS Security Token Service (AWS STS) credential identity resolver
is used when creating service clients.
After authenticating with Sign In With Apple, a
function named
saveUserData()
is called to securely store the user's information in
the Keychain. This lets the sign in screen automatically
fill in the form fields, and lets the application
remember the user's email address and name if
available.
/// Convert the given JWT identity token string into the temporary /// AWS credentials needed to allow this application to operate, as /// specified using the Apple Developer portal and the AWS Identity /// and Access Management (IAM) service. /// /// - Parameters: /// - tokenString: The string version of the JWT identity token /// returned by Sign In With Apple. /// - region: An optional string specifying the AWS Region to /// access. If not specified, "us-east-1" is assumed. func authenticate(withWebIdentity tokenString: String, region: String = "us-east-1") async throws { // If the role is empty, pass `nil` to use the default role for // the user. let roleARN = "arn:aws:iam::\(awsAccountNumber):role/\(awsIAMRoleName)" // Use the AWS Security Token Service (STS) action // `AssumeRoleWithWebIdentity` to convert the JWT token into a // set of temporary AWS credentials. The first step: write the token // to disk so it can be used by the // `STSWebIdentityAWSCredentialIdentityResolver`. let tokenFileURL = createTokenFileURL() let tokenFilePath = tokenFileURL.path do { try tokenString.write(to: tokenFileURL, atomically: true, encoding: .utf8) } catch { throw BucketsAppError.tokenFileError() } // Create an identity resolver that uses the JWT token received // from Apple to create AWS credentials. do { identityResolver = try STSWebIdentityAWSCredentialIdentityResolver( region: region, roleArn: roleARN, roleSessionName: "BucketsExample", tokenFilePath: tokenFilePath ) } catch { throw BucketsAppError.assumeRoleFailed } // Save the user's data securely to local storage so it's available // in the future. // // IMPORTANT: Any potential Personally Identifiable Information _must_ // be saved securely, such as by using the Keychain or an appropriate // encrypting technique. saveUserData() }
Important
The token is written to disk since the SDK expects to load the token from a file. This token file must remain in place until you have finished using the service client. When you're done using the client, delete the token file.
Create a service client using the web identity credential identity resolver
To access an AWS service, create a client
configuration object that includes the
awsCredentialIdentityResolver
property.
This property's value should be the web identity
credential identity resolver created by the
authorize(withWebIdentity: region:)
function:
/// Fetches a list of the user's Amazon S3 buckets. /// /// The bucket names are stored in the view model's `bucketList` /// property. func getBucketList() async throws { // If there's no identity resolver yet, return without doing anything. guard let identityResolver = identityResolver else { return } // Create an Amazon S3 client configuration that uses the // credential identity resolver created from the JWT token // returned by Sign In With Apple. let config = try await S3Client.S3ClientConfiguration( awsCredentialIdentityResolver: identityResolver, region: "us-east-1" ) let s3 = S3Client(config: config) let output = try await s3.listBuckets( input: ListBucketsInput() ) guard let buckets = output.buckets else { throw BucketsAppError.bucketListMissing } // Add the names of all the buckets to `bucketList`. Each // name is stored as a new `IDString` for use with the SwiftUI // `List`. for bucket in buckets { self.bucketList.append(IDString(bucket.name ?? "<unknown>")) } }
This function creates an S3Client
configured to use our web
identity credential identity resolver. That client is
then used to fetch a list of Amazon S3 buckets. The buckets'
names are then added to the bucket list. The SwiftUI
List
view automatically refreshes to
display the newly-added bucket names.