AWS SDK for Swift integration on Apple platforms - AWS SDK for Swift

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:

  1. Set up your application in Apple's "Certificates, Identifiers & Profiles" dashboard, or configure Sign In With Apple using Xcode.

  2. Set up the application's security and authentication configuration in the AWS Management Console.

  3. 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 view. The example works on macOS, iOS, and iPadOS and is shown in part during the walkthrough below. The complete example is on GitHub.

Configure the app for Sign in With Apple

If you use Xcode, you can enable Sign In With Apple in the options for your application's main target.

  1. 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.

  2. Click the + Capability button to open a capability picker. Add Sign In With Apple to your target's capabilities.

  3. Click the Automatically manage signing checkbox to enable automatic signing of your application. This also configures your application on the Apple developer portal.

  4. 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 to configure IAM to support authentication using the JSON Web Token (JWT) returned by Sign In With Apple after a sign in request.

  1. 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.

  2. 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):

    1. Click Policies in the IAM Management Console sidebar, then click Create policy.

    2. 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.

    3. 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.

    4. Enter a helpful description for your policy, such as "Application permissions for the Sign In With Apple buckets example".

    5. Click Create policy.

  3. Create a new role for your application:

    1. Click Roles in the IAM Management Console sidebar, then click Create role.

    2. Set the trusted entity type to Web identity.

    3. Under Web identity, select the Apple ID identity provider you created above.

    4. Select your application's bundle ID as the Audience.

    5. Under Permissions policies, choose the policy you created above.

    6. 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."

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