Skip to content

Commit 396e600

Browse files
committed
Add documentation for email auth; minor fixes/refactor
1 parent 2502df2 commit 396e600

16 files changed

+1411
-600
lines changed

CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Note: Tests will fail if the types generated by the swift-openapi-generator are
6767
The Turnkey SDK project is structured into several key directories:
6868

6969
- **Sources/TurnkeySDK**: Contains the source files for the SDK.
70-
- **Sources/TurnkeySDK/Generated**: This directory is used to store auto-generated Swift files. It is populated by running the `swift-openapi-generator`.
70+
- **Sources/TurnkeySDK/Generated**: This directory is used to store auto-generated Swift files. It is populated by running the `make turnkey_client_types` command.
7171
- **templates**: Holds the Stencil templates used by Sourcery for code generation. The main template is `TurnkeyClient.stencil`.
7272

7373
## Makefile Commands

README.md

+1-9
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,9 @@ The TurnkeySDK is built to support macOS, iOS, tvOS, watchOS, and visionOS, maki
1111
To integrate the TurnkeySDK into your Swift project, you need to add it as a dependency in your Package.swift file:
1212

1313
```swift
14-
dependencies: [
15-
.package(url: "https://github.com/your-organization/swift-sdk", .upToNextMajor(from: "1.0.0"))
16-
]
14+
.package(url: "https://github.com/tkhq/swift-sdk", from: "1.0.0")
1715
```
1816

19-
Ensure to replace `"https://github.com/your-organization/swift-sdk"` with the actual URL of the Swift SDK repository.
20-
21-
## Usage
22-
23-
Here's a quick guide on how to use the TurnkeySDK in your Swift project:
24-
2517
## Contributing
2618

2719
For guidelines on how to contribute to the Swift SDK, please refer to the [contributing guide](CONTRIBUTING.md).

Sources/Shared/PasskeyManager.swift

+2-53
Original file line numberDiff line numberDiff line change
@@ -136,67 +136,16 @@ public class PasskeyManager: NSObject, ASAuthorizationControllerDelegate,
136136
let attestationObject = rawAttestationObject.base64URLEncodedString()
137137
let clientDataJson = credentialRegistration.rawClientDataJSON.base64URLEncodedString()
138138
let credentialId = credentialRegistration.credentialID.base64URLEncodedString()
139-
140-
141139

142140
let attestation = Attestation(
143-
credentialId: credentialId, clientDataJson: clientDataJson, attestationObject: attestationObject)
141+
credentialId: credentialId, clientDataJson: clientDataJson,
142+
attestationObject: attestationObject)
144143

145144
let registrationResult = PasskeyRegistrationResult(
146145
challenge: challenge, attestation: attestation)
147146

148147
notifyRegistrationCompleted(result: registrationResult)
149148
return
150-
// credentialRegistration.rawAttestationObject?.base64URLEncodedString()
151-
//
152-
//
153-
// guard
154-
// let clientDataJSON = try? JSONDecoder().decode(
155-
// ClientDataJSON.self, from: credentialRegistration.rawClientDataJSON)
156-
// else {
157-
// notifyRegistrationFailed(error: PasskeyRegistrationError.invalidClientDataJSON)
158-
// return
159-
// }
160-
//
161-
// guard let rawAttestationData = credentialRegistration.rawAttestationObject else {
162-
// notifyRegistrationFailed(error: PasskeyRegistrationError.invalidAttestation)
163-
// return
164-
// }
165-
166-
// guard
167-
// let attestation = try? JSONDecoder().decode(
168-
// ClientDataJSON.self, from: rawAttestationData)
169-
// else {
170-
// notifyRegistrationFailed(error: PasskeyRegistrationError.invalidClientDataJSON)
171-
// return
172-
// }
173-
174-
// do {
175-
// guard let jsonData = try JSONSerialization.data(withJSONObject: rawAttestationData, options: [])
176-
// else {
177-
// notifyRegistrationFailed(error: PasskeyRegistrationError.invalidAttestation)
178-
// return
179-
// }
180-
//
181-
// guard let jsonString = String(data: jsonData, encoding: .utf8) else {
182-
// notifyRegistrationFailed(error: PasskeyRegistrationError.invalidAttestation)
183-
// return
184-
// }
185-
// }
186-
// catch {
187-
//
188-
// }
189-
190-
// let attestation =
191-
// String(data: attestationData, encoding: .utf8) ?? "Invalid attestation encoding"
192-
//
193-
// let challenge = clientDataJSON.challenge
194-
195-
// let registrationResult = PasskeyRegistrationResult(
196-
// challenge: challenge, attestation: attestation)
197-
//
198-
// notifyRegistrationCompleted(result: registrationResult)
199-
200149
case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
201150
logger.log("A passkey was used to sign in: \(credentialAssertion)")
202151
notifyPasskeyAssertionCompleted(result: credentialAssertion)

Tests/TurnkeySDKTests/TurnkeySDKTests.swift

+5-6
Original file line numberDiff line numberDiff line change
@@ -185,28 +185,27 @@ final class TurnkeySDKTests: XCTestCase {
185185
let email = "[email protected]"
186186
let targetPublicKey =
187187
"04d3f967632eb6a317059a164b7b71704c22fb2b0f20e6f27f62fdadeea14da558318a88bb9bb06c5886397666b4f1a1e3b92337c3ebebb4d570d4c735bc46fe83"
188-
// Data(hexString: apiPrivateKey!)
188+
189189
let apiKeyName = "email-auth-key"
190190
let expirationSeconds = "3600"
191191

192-
193192
let output = try await client.emailAuth(
194193
organizationId: organizationId!,
195194
email: email,
196195
targetPublicKey: targetPublicKey,
197196
apiKeyName: apiKeyName,
198197
expirationSeconds: expirationSeconds,
199-
emailCustomization: Components.Schemas.EmailCustomizationParams()
198+
emailCustomization: nil
200199
)
201200

202201
// Assert the response
203202
switch output {
204203
case .ok(let response):
205204
switch response.body {
206205
case .json(let emailAuthResponse):
207-
// Assert the expected properties in the emailAuthResponse
208-
XCTAssertNotNil(emailAuthResponse.activityId)
209-
// XCTAssertEqual(emailAuthResponse.status, "Success")
206+
207+
// Assert the expected properties in the emailAuthResponse
208+
XCTAssertNotNil(emailAuthResponse.activity.id)
210209
}
211210
case .undocumented(let statusCode, let undocumentedPayload):
212211
// Handle the undocumented response

docs/email-auth.md

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Email Authentication
2+
3+
This guide provides a walkthrough for implementing email authentication in a Swift application using the [TurnkeyClient](../Sources/TurnkeySDK/TurnkeyClient.generated.swift) and [AuthKeyManager](../Sources/Shared/AuthKeyManager.swift). This process involves generating key pairs, handling encrypted bundles, and verifying user identity.
4+
5+
For a more detailed explanation of the email authentication process, please refer to the [Turnkey API documentation](https://docs.turnkey.com/features/email-auth).
6+
7+
## Prerequisites
8+
9+
- A proxy server set up to handle authentication requests.
10+
- Organization ID and API key name from your Turnkey account.
11+
12+
## Step 1: Initialize the TurnkeyClient
13+
14+
Create an instance of TurnkeyClient using a proxy URL to handle the authentication.
15+
As a convenience, we've provided a [ProxyMiddleware](../Sources/Shared/ProxyMiddleware.swift) class that can be used to set up a proxy server to handle the authentication request.
16+
We are using a proxy URL because an authenticated user is required to initiate the email authentication request.
17+
18+
Note: The proxy server must be set up to handle the authentication request and return the exact payload received from the Turnkey API. If the response doesn't match exactly you'll see an undocumented response error in the logs.
19+
20+
```swift
21+
let proxyURL = "http://localhost:3000/api/email-auth"
22+
let client = TurnkeyClient(proxyURL: proxyURL)
23+
```
24+
25+
You may also forgo the use of the provided proxy middleware and make the request yourself.
26+
27+
## Step 2: Generate Ephemeral Key Pair
28+
29+
Use AuthKeyManager to generate a new ephemeral key pair for the email authentication flow.
30+
This key pair is not persisted and is used temporarily during the authentication process.
31+
Note: The 'domain' is used for scoping the key storage specific to an app and is optional for persisting the key.
32+
33+
```swift
34+
let authKeyManager = AuthKeyManager(domain: "your_domain")
35+
let publicKey = try authKeyManager.createKeyPair()
36+
```
37+
38+
## Step 3: Define Authentication Parameters
39+
40+
```swift
41+
let organizationId = "your_organization_id"
42+
let email = "[email protected]"
43+
let targetPublicKey = publicKey.toString(representation: .raw)
44+
let expirationSeconds = "3600"
45+
let emailCustomization = Components.Schemas.EmailCustomizationParams() // Customize as needed
46+
```
47+
48+
## Step 4: Send Email Authentication Request
49+
50+
With the TurnkeyClient initialized and the ephemeral key pair generated, you can now send an email authentication request. This involves using the emailAuth method of the TurnkeyClient, passing the necessary parameters.
51+
52+
```swift
53+
let emailAuthResult = try await client.emailAuth(
54+
organizationId: organizationId,
55+
email: email,
56+
targetPublicKey: targetPublicKey,
57+
apiKeyName: "your_api_key_name",
58+
expirationSeconds: expirationSeconds,
59+
emailCustomization: emailCustomization
60+
)
61+
```
62+
63+
After sending the email authentication request, it's important to handle the response appropriately.If the authentication is successful, you should save the user's sub-organizationId from the response for future use. You'll need this organizationId later to verify the user's keys.
64+
65+
```swift
66+
switch emailAuthResult {
67+
case .ok(let response):
68+
// The user's sub-organizationId:
69+
let organizationId = response.activity.organizationId
70+
// Proceed with user session creation or update
71+
case .undocumented(let statusCode, let undocumentedPayload):
72+
// Handle error, possibly retry or log
73+
}
74+
```
75+
76+
## Step 6: Verify Encrypted Bundle
77+
78+
After your user receives the encrypted bundle from Turnkey, via email, you need to decrypt this bundle to retrieve the necessary keys for further authentication steps. Use the [`decryptBundle`](../Sources/Shared/AuthKeyManager.swift?plain=1#L160) method from the `AuthKeyManager` to handle this.
79+
80+
```swift
81+
let (privateKey, publicKey) = try authManager.decryptBundle(encryptedBundle)
82+
```
83+
84+
This method will decrypt the encrypted bundle and provide you with the private and public keys needed for the session.
85+
At this point in the authentication process, you have two options:
86+
87+
1. Prompt the user for passkey authentication (using the `PasskeyManager`) and add a passkey as an authenticator.
88+
2. Save the API private key in the keychain and use that for subsequent authentication requests.
89+
90+
Note: Since the decrypted API key is similar to a session key, it should be handled with the same level of security as authentication tokens.
91+
92+
## Step 7: Initializing the TurnkeyClient and Verify the user
93+
94+
After successfully decrypting the encrypted bundle and retrieving the private and public API keys, you can initialize a TurnkeyClient instance using these keys for further authenticated requests:
95+
96+
```swift
97+
// ...
98+
99+
let apiPublicKey = try publicKey.toString(representation: .compressed)
100+
let apiPrivateKey = try privateKey.toString(representation: .raw)
101+
102+
// Initialize a new TurnkeyClient instance with the provided privateKey and publicKey
103+
let turnkeyClient = TurnkeyClient(apiPrivateKey: apiPrivateKey, apiPublicKey: apiPublicKey)
104+
```
105+
106+
### Verifying User Credentials with getWhoami
107+
108+
After initializing the TurnkeyClient with the decrypted API keys, it is recommended to verify the validity of these credentials. This can be done using the `getWhoami` method, which checks the active status of the credentials against the Turnkey API.
109+
110+
```swift
111+
do {
112+
let whoamiResponse = try await turnkeyClient.getWhoami(organizationId: organizationId /* from emailAuthResult */)
113+
114+
switch whoamiResponse {
115+
case .ok(let response):
116+
print("Credential verification successful: \(whoamiResponse)")
117+
case .undocumented(let statusCode, let undocumentedPayload):
118+
print("Error during credential verification: \(error)")
119+
}
120+
} catch {
121+
print("Error during credential verification: \(error)")
122+
}
123+
124+
125+
```

example/TurnkeyiOSExample/TurnkeyiOSExample.xcodeproj/project.pbxproj

+44
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
1E4FCBD02BCDCAB80042A4B2 /* UserHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FCBCF2BCDCAB80042A4B2 /* UserHomeViewController.swift */; };
2222
1E4FCBD22BCDCAD00042A4B2 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FCBD12BCDCAD00042A4B2 /* SignInViewController.swift */; };
2323
1E81BE842BDC2755006A9A0A /* EmailAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E81BE832BDC2755006A9A0A /* EmailAuthViewController.swift */; };
24+
1EABCB2A2BE468B00037DD52 /* web3swift in Frameworks */ = {isa = PBXBuildFile; productRef = 1EABCB292BE468B00037DD52 /* web3swift */; };
25+
1EABCB2C2BE491790037DD52 /* UserManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB2B2BE491790037DD52 /* UserManager.swift */; };
26+
1EABCB2F2BE498A50037DD52 /* UserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB2E2BE498A50037DD52 /* UserModel.swift */; };
27+
1EABCB6B2BE4B0000037DD52 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB6A2BE4B0000037DD52 /* SessionManager.swift */; };
28+
1EABCB6F2BE56B4F0037DD52 /* SendTransactionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB6E2BE56B4F0037DD52 /* SendTransactionViewController.swift */; };
2429
/* End PBXBuildFile section */
2530

2631
/* Begin PBXContainerItemProxy section */
@@ -59,6 +64,10 @@
5964
1E4FCBCF2BCDCAB80042A4B2 /* UserHomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserHomeViewController.swift; sourceTree = "<group>"; };
6065
1E4FCBD12BCDCAD00042A4B2 /* SignInViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = "<group>"; };
6166
1E81BE832BDC2755006A9A0A /* EmailAuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailAuthViewController.swift; sourceTree = "<group>"; };
67+
1EABCB2B2BE491790037DD52 /* UserManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserManager.swift; sourceTree = "<group>"; };
68+
1EABCB2E2BE498A50037DD52 /* UserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserModel.swift; sourceTree = "<group>"; };
69+
1EABCB6A2BE4B0000037DD52 /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionManager.swift; path = ../../../../../../../../Documents/SessionManager.swift; sourceTree = "<group>"; };
70+
1EABCB6E2BE56B4F0037DD52 /* SendTransactionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTransactionViewController.swift; sourceTree = "<group>"; };
6271
/* End PBXFileReference section */
6372

6473
/* Begin PBXFrameworksBuildPhase section */
@@ -67,6 +76,7 @@
6776
buildActionMask = 2147483647;
6877
files = (
6978
1E4CD78B2BE2CF180097E2CB /* TurnkeySDK in Frameworks */,
79+
1EABCB2A2BE468B00037DD52 /* web3swift in Frameworks */,
7080
);
7181
runOnlyForDeploymentPostprocessing = 0;
7282
};
@@ -110,7 +120,9 @@
110120
1E27801F2BCA349300AE790C /* TurnkeyiOSExample */ = {
111121
isa = PBXGroup;
112122
children = (
123+
1EABCB2D2BE498930037DD52 /* Models */,
113124
1E37D5972BD2F3A400962F0D /* Info.plist */,
125+
1EABCB2E2BE498A50037DD52 /* UserModel.swift */,
114126
1E37D5962BD2EBD200962F0D /* TurnkeyiOSExample.entitlements */,
115127
1E2780242BCA349400AE790C /* Assets.xcassets */,
116128
1E2780262BCA349400AE790C /* Preview Content */,
@@ -122,6 +134,9 @@
122134
1E37D5992BD2FFDD00962F0D /* LaunchScreen.storyboard */,
123135
1E37D59D2BD3004D00962F0D /* Main.storyboard */,
124136
1E81BE832BDC2755006A9A0A /* EmailAuthViewController.swift */,
137+
1EABCB2B2BE491790037DD52 /* UserManager.swift */,
138+
1EABCB6A2BE4B0000037DD52 /* SessionManager.swift */,
139+
1EABCB6E2BE56B4F0037DD52 /* SendTransactionViewController.swift */,
125140
);
126141
path = TurnkeyiOSExample;
127142
sourceTree = "<group>";
@@ -151,6 +166,13 @@
151166
path = TurnkeyiOSExampleUITests;
152167
sourceTree = "<group>";
153168
};
169+
1EABCB2D2BE498930037DD52 /* Models */ = {
170+
isa = PBXGroup;
171+
children = (
172+
);
173+
path = Models;
174+
sourceTree = "<group>";
175+
};
154176
/* End PBXGroup section */
155177

156178
/* Begin PBXNativeTarget section */
@@ -169,6 +191,7 @@
169191
name = TurnkeyiOSExample;
170192
packageProductDependencies = (
171193
1E4CD78A2BE2CF180097E2CB /* TurnkeySDK */,
194+
1EABCB292BE468B00037DD52 /* web3swift */,
172195
);
173196
productName = TurnkeyiOSExample;
174197
productReference = 1E27801D2BCA349300AE790C /* TurnkeyiOSExample.app */;
@@ -244,6 +267,7 @@
244267
mainGroup = 1E2780142BCA349300AE790C;
245268
packageReferences = (
246269
1E4351F82BCDDBAD00BF67F2 /* XCLocalSwiftPackageReference "../.." */,
270+
1EABCB282BE468B00037DD52 /* XCRemoteSwiftPackageReference "web3swift" */,
247271
);
248272
productRefGroup = 1E27801E2BCA349300AE790C /* Products */;
249273
projectDirPath = "";
@@ -291,8 +315,12 @@
291315
files = (
292316
1E81BE842BDC2755006A9A0A /* EmailAuthViewController.swift in Sources */,
293317
1E4FCBD02BCDCAB80042A4B2 /* UserHomeViewController.swift in Sources */,
318+
1EABCB6B2BE4B0000037DD52 /* SessionManager.swift in Sources */,
319+
1EABCB2F2BE498A50037DD52 /* UserModel.swift in Sources */,
320+
1EABCB2C2BE491790037DD52 /* UserManager.swift in Sources */,
294321
1E37D5932BD2D77C00962F0D /* AccountManager.swift in Sources */,
295322
1E4FCBCA2BCB71820042A4B2 /* AppDelegate.swift in Sources */,
323+
1EABCB6F2BE56B4F0037DD52 /* SendTransactionViewController.swift in Sources */,
296324
1E4FCBD22BCDCAD00042A4B2 /* SignInViewController.swift in Sources */,
297325
1E4FCBCC2BCDCA6C0042A4B2 /* SceneDelegate.swift in Sources */,
298326
);
@@ -632,11 +660,27 @@
632660
};
633661
/* End XCLocalSwiftPackageReference section */
634662

663+
/* Begin XCRemoteSwiftPackageReference section */
664+
1EABCB282BE468B00037DD52 /* XCRemoteSwiftPackageReference "web3swift" */ = {
665+
isa = XCRemoteSwiftPackageReference;
666+
repositoryURL = "https://github.com/web3swift-team/web3swift.git";
667+
requirement = {
668+
kind = upToNextMajorVersion;
669+
minimumVersion = 3.2.1;
670+
};
671+
};
672+
/* End XCRemoteSwiftPackageReference section */
673+
635674
/* Begin XCSwiftPackageProductDependency section */
636675
1E4CD78A2BE2CF180097E2CB /* TurnkeySDK */ = {
637676
isa = XCSwiftPackageProductDependency;
638677
productName = TurnkeySDK;
639678
};
679+
1EABCB292BE468B00037DD52 /* web3swift */ = {
680+
isa = XCSwiftPackageProductDependency;
681+
package = 1EABCB282BE468B00037DD52 /* XCRemoteSwiftPackageReference "web3swift" */;
682+
productName = web3swift;
683+
};
640684
/* End XCSwiftPackageProductDependency section */
641685
};
642686
rootObject = 1E2780152BCA349300AE790C /* Project object */;

0 commit comments

Comments
 (0)