Skip to content

Commit 5f4ba2e

Browse files
committed
Use SimpleKeychain to save local data
This update switches from using the unencrypted SwiftUI `@AppStorage` to using Auth0's `SimpleKeychain` library to save the user account information locally.
1 parent 9c45b63 commit 5f4ba2e

File tree

3 files changed

+138
-40
lines changed

3 files changed

+138
-40
lines changed

swift/example_code/apple/Buckets/Buckets.xcodeproj/project.pbxproj

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
283FA3A72C5D3628003CF6D9 /* AWSSTS in Frameworks */ = {isa = PBXBuildFile; productRef = 283FA3A62C5D3628003CF6D9 /* AWSSTS */; };
1717
283FA3A92C5D3697003CF6D9 /* IDString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 283FA3A82C5D3697003CF6D9 /* IDString.swift */; };
1818
283FA3AB2C5D36BC003CF6D9 /* BucketsAppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 283FA3AA2C5D36BC003CF6D9 /* BucketsAppError.swift */; };
19+
286E157D2CA46392001D1009 /* SimpleKeychain in Frameworks */ = {isa = PBXBuildFile; productRef = 286E157C2CA46392001D1009 /* SimpleKeychain */; };
1920
28FA46BE2C8A135700A2E959 /* Task-extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FA46BD2C8A135700A2E959 /* Task-extension.swift */; };
2021
28FA46C02C8A139900A2E959 /* LocalizedAlertError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FA46BF2C8A139900A2E959 /* LocalizedAlertError.swift */; };
2122
28FA46C22C8A145500A2E959 /* View-extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FA46C12C8A145500A2E959 /* View-extension.swift */; };
@@ -41,6 +42,7 @@
4142
isa = PBXFrameworksBuildPhase;
4243
buildActionMask = 2147483647;
4344
files = (
45+
286E157D2CA46392001D1009 /* SimpleKeychain in Frameworks */,
4446
283FA3A52C5D3628003CF6D9 /* AWSS3 in Frameworks */,
4547
283FA3A72C5D3628003CF6D9 /* AWSSTS in Frameworks */,
4648
);
@@ -53,6 +55,7 @@
5355
isa = PBXGroup;
5456
children = (
5557
283FA3912C5D34FF003CF6D9 /* Buckets */,
58+
286E157B2CA46392001D1009 /* Frameworks */,
5659
283FA3902C5D34FF003CF6D9 /* Products */,
5760
);
5861
sourceTree = "<group>";
@@ -72,10 +75,10 @@
7275
283FA3942C5D34FF003CF6D9 /* ContentView.swift */,
7376
283FA3A12C5D3590003CF6D9 /* ContentView-ViewModel.swift */,
7477
283FA3AA2C5D36BC003CF6D9 /* BucketsAppError.swift */,
78+
28FA46BC2C8A131D00A2E959 /* Support */,
7579
283FA3962C5D3500003CF6D9 /* Assets.xcassets */,
7680
283FA3982C5D3500003CF6D9 /* Buckets.entitlements */,
7781
283FA3992C5D3500003CF6D9 /* Preview Content */,
78-
28FA46BC2C8A131D00A2E959 /* Support */,
7982
);
8083
path = Buckets;
8184
sourceTree = "<group>";
@@ -88,6 +91,13 @@
8891
path = "Preview Content";
8992
sourceTree = "<group>";
9093
};
94+
286E157B2CA46392001D1009 /* Frameworks */ = {
95+
isa = PBXGroup;
96+
children = (
97+
);
98+
name = Frameworks;
99+
sourceTree = "<group>";
100+
};
91101
28FA46BC2C8A131D00A2E959 /* Support */ = {
92102
isa = PBXGroup;
93103
children = (
@@ -118,6 +128,7 @@
118128
packageProductDependencies = (
119129
283FA3A42C5D3628003CF6D9 /* AWSS3 */,
120130
283FA3A62C5D3628003CF6D9 /* AWSSTS */,
131+
286E157C2CA46392001D1009 /* SimpleKeychain */,
121132
);
122133
productName = Buckets;
123134
productReference = 283FA38F2C5D34FF003CF6D9 /* Buckets.app */;
@@ -149,6 +160,7 @@
149160
mainGroup = 283FA3862C5D34FF003CF6D9;
150161
packageReferences = (
151162
283FA3A32C5D3628003CF6D9 /* XCRemoteSwiftPackageReference "aws-sdk-swift" */,
163+
2816F5ED2CA460E800E07574 /* XCRemoteSwiftPackageReference "SimpleKeychain" */,
152164
);
153165
productRefGroup = 283FA3902C5D34FF003CF6D9 /* Products */;
154166
projectDirPath = "";
@@ -414,6 +426,14 @@
414426
/* End XCConfigurationList section */
415427

416428
/* Begin XCRemoteSwiftPackageReference section */
429+
2816F5ED2CA460E800E07574 /* XCRemoteSwiftPackageReference "SimpleKeychain" */ = {
430+
isa = XCRemoteSwiftPackageReference;
431+
repositoryURL = "https://github.com/auth0/SimpleKeychain.git";
432+
requirement = {
433+
kind = upToNextMajorVersion;
434+
minimumVersion = 1.2.0;
435+
};
436+
};
417437
283FA3A32C5D3628003CF6D9 /* XCRemoteSwiftPackageReference "aws-sdk-swift" */ = {
418438
isa = XCRemoteSwiftPackageReference;
419439
repositoryURL = "https://github.com/awslabs/aws-sdk-swift.git";
@@ -435,6 +455,11 @@
435455
package = 283FA3A32C5D3628003CF6D9 /* XCRemoteSwiftPackageReference "aws-sdk-swift" */;
436456
productName = AWSSTS;
437457
};
458+
286E157C2CA46392001D1009 /* SimpleKeychain */ = {
459+
isa = XCSwiftPackageProductDependency;
460+
package = 2816F5ED2CA460E800E07574 /* XCRemoteSwiftPackageReference "SimpleKeychain" */;
461+
productName = SimpleKeychain;
462+
};
438463
/* End XCSwiftPackageProductDependency section */
439464
};
440465
rootObject = 283FA3872C5D34FF003CF6D9 /* Project object */;

swift/example_code/apple/Buckets/Buckets/BucketsApp.swift

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,9 @@
66
//
77

88
import SwiftUI
9-
import ClientRuntime
109

1110
@main
1211
struct BucketsApp: App {
13-
14-
/// Initialize the application.
15-
init() {
16-
// Create a synchronous task that configures logging then
17-
// immediately exits.
18-
Task.synchronous {
19-
SDKDefaultIO.setLogLevel(level: .error)
20-
await SDKLoggingSystem().initialize(logLevel: .error)
21-
}
22-
}
23-
2412
var body: some Scene {
2513
WindowGroup {
2614
ContentView().environmentObject(ViewModel())

swift/example_code/apple/Buckets/Buckets/ContentView-ViewModel.swift

Lines changed: 112 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
// snippet-start:[siwa-viewmodel.swift.imports]
45
import SwiftUI
56
import AuthenticationServices
7+
import SimpleKeychain
68

79
// Import the AWS SDK for Swift modules the app requires.
8-
//
9-
// snippet-start:[siwa-viewmodel.swift.imports]
1010
import AWSS3
1111
import AWSSDKIdentity
1212
// snippet-end:[siwa-viewmodel.swift.imports]
@@ -34,49 +34,53 @@ class ViewModel: ObservableObject {
3434
/// This is only returned by SIWA if the user has just created
3535
/// the app's SIWA account link. Otherwise, it's returned as `nil`
3636
/// by SIWA and must be retrieved from local storage if needed.
37-
@AppStorage("email") var email = ""
37+
var email = ""
3838

3939
/// The user's family (last) name.
4040
///
4141
/// This is only returned by SIWA if the user has just created
4242
/// the app's SIWA account link. Otherwise, it's returned as `nil`
4343
/// by SIWA and must be retrieved from local storage if needed.
44-
@AppStorage("family-name") var familyName = ""
45-
44+
var familyName = ""
45+
4646
/// The user's given (first) name.
4747
///
4848
/// This is only returned by SIWA if the user has just created
4949
/// the app's SIWA account link. Otherwise, it's returned as `nil` by SIWA
5050
/// and must be retrieved from local storage if needed.
51-
@AppStorage("given-name") var givenName = ""
52-
51+
var givenName = ""
52+
5353
/// The AWS account number provided by the user.
54-
@AppStorage("account-number") var awsAccountNumber = ""
54+
var awsAccountNumber = ""
5555

5656
/// The AWS IAM role name given by the user.
57-
@AppStorage("iam-role") var awsIAMRoleName = ""
58-
57+
var awsIAMRoleName = ""
58+
5959
/// The credential identity resolver created by the AWS SDK for
6060
/// Swift. This resolves temporary credentials using
6161
/// `AssumeRoleWithWebIdentity`.
6262
var identityResolver: STSWebIdentityAWSCredentialIdentityResolver? = nil
6363
// snippet-end:[siwa-auth-properties.swift]
64-
64+
6565
//****** The variables below this point aren't involved in Sign in With
6666
//****** Apple or AWS authentication.
67-
67+
6868
/// An array of the user's bucket names.
6969
///
7070
/// This is filled out once the user is signed into AWS.
7171
@Published var bucketList: [IDString] = []
72-
72+
7373
/// An error string to present in an alert panel if needed. If this is
7474
/// `nil`, no error alert is presented.
7575
@Published var error: Swift.Error?
7676

7777
/// The title of the error alert's close button.
7878
@Published var errorButtonTitle: String = "Continue"
79-
79+
80+
init() {
81+
readUserData()
82+
}
83+
8084
// MARK: - Authentication
8185

8286
// snippet-start:[siwa-handle.swift]
@@ -102,13 +106,13 @@ class ViewModel: ObservableObject {
102106
else {
103107
throw BucketsAppError.credentialsIncomplete
104108
}
105-
109+
106110
userID = credential.user
107-
111+
108112
// If the email field has a value, set the user's recorded email
109113
// address. Otherwise, keep the existing one.
110114
email = credential.email ?? self.email
111-
115+
112116
// Similarly, if the name is present in the credentials, use it.
113117
// Otherwise, the last known name is retained.
114118
if let name = credential.fullName {
@@ -140,7 +144,7 @@ class ViewModel: ObservableObject {
140144
}
141145
}
142146
// snippet-end:[siwa-handle.swift]
143-
147+
144148
// snippet-start:[siwa-authenticate.swift]
145149
/// Convert the given JWT identity token string into the temporary
146150
/// AWS credentials needed to allow this application to operate, as
@@ -170,13 +174,12 @@ class ViewModel: ObservableObject {
170174
do {
171175
try tokenString.write(to: tokenFileURL, atomically: true, encoding: .utf8)
172176
} catch {
173-
//print("Error writing token to disk: \(error)")
174177
throw BucketsAppError.tokenFileError()
175178
}
176179

177180
// Create an identity resolver that uses the JWT token received
178181
// from Apple to create AWS credentials.
179-
182+
180183
do {
181184
identityResolver = try STSWebIdentityAWSCredentialIdentityResolver(
182185
region: region,
@@ -187,9 +190,14 @@ class ViewModel: ObservableObject {
187190
} catch {
188191
throw BucketsAppError.assumeRoleFailed
189192
}
193+
194+
// Save the user's data securely to local storage so it's available
195+
// in the future.
196+
197+
saveUserData()
190198
}
191199
// snippet-end:[siwa-authenticate.swift]
192-
200+
193201
// snippet-start:[siwa-sign-out.swift]
194202
/// "Sign out" of the user's account.
195203
///
@@ -218,7 +226,7 @@ class ViewModel: ObservableObject {
218226
return userID != ""
219227
}
220228
// snippet-end:[siwa-check-signed-in.swift]
221-
229+
222230
// MARK: - AWS access
223231
// snippet-start:[siwa-use-credentials.swift]
224232
/// Fetches a list of the user's Amazon S3 buckets.
@@ -230,11 +238,11 @@ class ViewModel: ObservableObject {
230238
// credential identity resolver created from the JWT token
231239
// returned by Sign In With Apple.
232240
let config = try await S3Client.S3ClientConfiguration(
233-
awsCredentialIdentityResolver: identityResolver,
234-
region: "us-east-1"
241+
awsCredentialIdentityResolver: identityResolver,
242+
region: "us-east-1"
235243
)
236244
let s3 = S3Client(config: config)
237-
245+
238246
let output = try await s3.listBuckets(
239247
input: ListBucketsInput()
240248
)
@@ -251,7 +259,7 @@ class ViewModel: ObservableObject {
251259
}
252260
}
253261
// snippet-end:[siwa-use-credentials.swift]
254-
262+
255263
// MARK: - Token file management
256264

257265
// snippet-start:[siwa-create-token-file.swift]
@@ -264,7 +272,7 @@ class ViewModel: ObservableObject {
264272
return tempDirURL.appendingPathComponent("example-siwa-token.jwt")
265273
}
266274
// snippet-end:[siwa-create-token-file.swift]
267-
275+
268276
// snippet-start:[siwa-delete-token-file.swift]
269277
/// Delete the local JWT token file.
270278
func deleteTokenFile() throws {
@@ -275,4 +283,81 @@ class ViewModel: ObservableObject {
275283
}
276284
}
277285
// snippet-end:[siwa-delete-token-file.swift]
286+
287+
288+
// MARK: - Secure storage
289+
290+
let KEY_USER_ID = "buckets-user-id"
291+
let KEY_USER_EMAIL = "buckets-user-email"
292+
let KEY_USER_NAME_GIVEN = "buckets-user-given-name"
293+
let KEY_USER_NAME_FAMILY = "buckets-user-family-name"
294+
let KEY_AWS_ACCOUNT = "buckets-aws-account"
295+
let KEY_IAM_ROLE = "buckets-iam-role"
296+
297+
/// Securely save user and account information to the Keychain.
298+
func saveUserData() {
299+
// Create a `SimpleKeychain` object to use for Keychain access. The
300+
// default service name (the bundle ID) is used.
301+
let simpleKeychain = SimpleKeychain()
302+
303+
do {
304+
//try simpleKeychain.set(userID, forKey: KEY_USER_ID)
305+
try simpleKeychain.set(email, forKey: KEY_USER_EMAIL)
306+
try simpleKeychain.set(givenName, forKey: KEY_USER_NAME_GIVEN)
307+
try simpleKeychain.set(familyName, forKey: KEY_USER_NAME_FAMILY)
308+
try simpleKeychain.set(awsAccountNumber, forKey: KEY_AWS_ACCOUNT)
309+
try simpleKeychain.set(awsIAMRoleName, forKey: KEY_IAM_ROLE)
310+
} catch {
311+
// The way this example is written, if the data doesn't get saved,
312+
// it just means it won't be available later. So silently log
313+
// the problem and continue.
314+
print("Unable to save user data to keychain.")
315+
}
316+
}
317+
318+
/// Read user data from the keychain.
319+
func readUserData() {
320+
// Create a `SimpleKeychain` object to use for Keychain access. The
321+
// default service name (the bundle ID) is used. Any item that isn't
322+
// found is set to an empty string.
323+
let simpleKeychain = SimpleKeychain()
324+
325+
/*
326+
do {
327+
userID = try simpleKeychain.string(forKey: KEY_USER_ID)
328+
} catch {
329+
userID = ""
330+
}
331+
*/
332+
333+
do {
334+
email = try simpleKeychain.string(forKey: KEY_USER_EMAIL)
335+
} catch {
336+
email = ""
337+
}
338+
339+
do {
340+
givenName = try simpleKeychain.string(forKey: KEY_USER_NAME_GIVEN)
341+
} catch {
342+
givenName = ""
343+
}
344+
345+
do {
346+
familyName = try simpleKeychain.string(forKey: KEY_USER_NAME_FAMILY)
347+
} catch {
348+
familyName = ""
349+
}
350+
351+
do {
352+
awsAccountNumber = try simpleKeychain.string(forKey: KEY_AWS_ACCOUNT)
353+
} catch {
354+
awsAccountNumber = ""
355+
}
356+
357+
do {
358+
awsIAMRoleName = try simpleKeychain.string(forKey: KEY_IAM_ROLE)
359+
} catch {
360+
awsIAMRoleName = ""
361+
}
362+
}
278363
}

0 commit comments

Comments
 (0)