From 338f53a5267163756f1522ccadfe3ea8d6a02926 Mon Sep 17 00:00:00 2001 From: Taylor Dawson Date: Tue, 12 Nov 2024 14:38:59 -0800 Subject: [PATCH] Update examples and docs --- README.md | 2 +- .../TurnkeySDK/TurnkeyClient.generated.swift | 4 - docs/email-auth.md | 148 +++++++++++------- docs/proxy-middleware.md | 53 ++++++- .../TurnkeyiOSExample/AccountManager.swift | 32 +--- templates/TurnkeyClient.stencil | 4 - 6 files changed, 149 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index de1fa84..2c6274b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The TurnkeySDK is built to support macOS, iOS, tvOS, watchOS, and visionOS, maki To integrate the TurnkeySDK into your Swift project, you need to add it as a dependency in your Package.swift file: ```swift -.package(url: "https://github.com/tkhq/swift-sdk", from: "1.0.0") +.package(url: "https://github.com/tkhq/swift-sdk", from: "1.1.0") ``` ## Usage diff --git a/Sources/TurnkeySDK/TurnkeyClient.generated.swift b/Sources/TurnkeySDK/TurnkeyClient.generated.swift index 363b0dd..75d50bd 100644 --- a/Sources/TurnkeySDK/TurnkeyClient.generated.swift +++ b/Sources/TurnkeySDK/TurnkeyClient.generated.swift @@ -48,10 +48,6 @@ public struct TurnkeyClient { /// - apiPublicKey: The public key obtained from Turnkey, used to identify the client. /// - baseUrl: The base URL of the Turnkey API. Defaults to "https://api.turnkey.com". /// - /// - Note: For client-side usage where all authenticated requests need secure key management, - /// it is recommended to use the `AuthKeyManager` for creating, storing, and securely using key pairs. - /// For more details, refer to the [AuthKeyManager](#AuthKeyManager). - /// /// - Example: /// ``` /// let client = TurnkeyClient(apiPrivateKey: "your_api_private_key", apiPublicKey: "your_api_public_key") diff --git a/docs/email-auth.md b/docs/email-auth.md index b26d3c5..d1eb01e 100644 --- a/docs/email-auth.md +++ b/docs/email-auth.md @@ -1,6 +1,6 @@ # Email Authentication -This guide provides a walkthrough for implementing email authentication in a Swift application using the [TurnkeyClient](../Sources/TurnkeySDK/TurnkeyClient.generated.swift). This process involves generating key pairs, handling encrypted bundles, and verifying user identity. +This guide provides a walkthrough for implementing email authentication in a Swift application using the [TurnkeyClient](../Sources/TurnkeySDK/TurnkeyClient.generated.swift). This process involves handling encrypted bundles and verifying user identity. For a more detailed explanation of the email authentication process, please refer to the [Turnkey API documentation](https://docs.turnkey.com/features/email-auth). @@ -24,104 +24,140 @@ let client = TurnkeyClient(proxyURL: proxyURL) You may also forgo the use of the provided proxy middleware and make the request yourself. -## Step 2: Generate Ephemeral Key Pair - -Next we'll generate an ephemeral key pair, which is will be used to decrypt the encrypted bundle sent -that the user will receive in their email. - -```swift -// Create a new ephemeral private key using P-256 curve for key agreement. -let ephemeralPrivateKey = P256.KeyAgreement.PrivateKey() - -// Extract the public key from the private key and convert it to a string using the x963 representation. -let targetPublicKey = try ephemeralPrivateKey.publicKey.toString(representation: .x963) -``` - -## Step 3: Define Authentication Parameters +## Step 2: Define Authentication Parameters ```swift let organizationId = "your_organization_id" let email = "user@example.com" -let targetPublicKey = publicKey.toString(representation: .raw) let expirationSeconds = "3600" let emailCustomization = Components.Schemas.EmailCustomizationParams() // Customize as needed ``` -## Step 4: Send Email Authentication Request +## Step 3: Send Email Authentication Request + +With the TurnkeyClient initialized, you can now send an email authentication request. This involves using the `emailAuth` method of the TurnkeyClient, passing in the necessary parameters. + +### Detailed Explanation -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 in the necessary parameters. +- **Ephemeral Key Generation**: The `emailAuth` method generates an ephemeral private key, which is used to create a public key for the authentication process. This ephemeral key is stored in memory and is used to decrypt the encrypted bundle sent to the user's email. + +- **Tuple Response**: The `emailAuth` method returns a tuple containing two elements: + 1. `Operations.EmailAuth.Output`: This is the output of the email authentication operation, which includes the response from the Turnkey API. + 2. `verify`: A closure function that takes an encrypted bundle as input and returns an `AuthResult`. This closure uses the ephemeral private key to decrypt the bundle and verify the authentication. ```swift -let emailAuthResult = try await client.emailAuth( +let (output, verify) = try await client.emailAuth( organizationId: organizationId, email: email, - targetPublicKey: targetPublicKey, apiKeyName: "your_api_key_name", expirationSeconds: expirationSeconds, emailCustomization: emailCustomization ) + +// Assert the response +switch output { +case let .ok(response): + switch response.body { + case let .json(emailAuthResponse): + print(emailAuthResponse.activity.organizationId) + // We successfully initiated the email authentication request + // We'll use the verify function to verify the encrypted bundle in the next step + } +case let .undocumented(statusCode, undocumentedPayload): + // Handle the undocumented response + if let body = undocumentedPayload.body { + let bodyString = try await String(collecting: body, upTo: .max) + print("Undocumented response body: \(bodyString)") + } + print("Undocumented response: \(statusCode)") +} ``` -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. +## Step 4: Verify Encrypted Bundle + +After your user receives the encrypted bundle from Turnkey, via email, you need to verify this bundle to retrieve the necessary keys for further authentication steps. We'll use the `verify` function returned from the previous step. + +### Detailed Explanation + +- **AuthResult**: The `verify` function returns an `AuthResult` object, which contains: + + - `whoamiResponse`: The result of calling `getWhoami`, which verifies the authentication and retrieves user details. + - `apiPublicKey` and `apiPrivateKey`: The keys obtained from the decrypted bundle, used for further authenticated requests. + +- **getWhoami Call**: The `verify` function internally calls the `getWhoami` method to ensure the credentials are valid and to fetch user details from the Turnkey API. ```swift -switch emailAuthResult { -case .ok(let response): - // The user's sub-organizationId: - let organizationId = response.activity.organizationId - // Proceed with user session creation -case .undocumented(let statusCode, let undocumentedPayload): - // Handle error, possibly retry or log +do { + let authResult = try await verify(bundle) + print("Verification successful: \(authResult)") +} catch { + print("Error occurred during verification: \(error)") } ``` -## Step 6: Verify Encrypted Bundle +This method will verify the encrypted bundle and provide you with the necessary authentication result. -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/AuthManager.swift) method from the `AuthManager` to handle this. +## Step 5: Initialize the TurnkeyClient with API Keys -```swift -let (privateKey, publicKey) = try AuthManager.decryptBundle(encryptedBundle) -``` +After successfully verifying the encrypted bundle and retrieving the private and public API keys, you can initialize a TurnkeyClient instance using these keys for further authenticated requests: -This method will decrypt the encrypted bundle and provide you with the private and public keys needed for the session. -At this point in the authentication process, you have two options: +```swift +// Use the apiPublicKey and apiPrivateKey from the authResult +let apiPublicKey = authResult.apiPublicKey +let apiPrivateKey = authResult.apiPrivateKey -1. Prompt the user for passkey authentication (using the `PasskeyManager`) and add a passkey as an authenticator. -2. Save the API private key in the keychain and use that for subsequent authentication requests. +// Initialize a new TurnkeyClient instance with the provided privateKey and publicKey +let turnkeyClient = TurnkeyClient(apiPrivateKey: apiPrivateKey, apiPublicKey: apiPublicKey) +``` -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. +## Step 6: Create Read Only Session -## Step 7: Initialize the TurnkeyClient and Verify the user +### Extract API Keys and Sub-Organization ID -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: +First, get the `apiPublicKey` and `apiPrivateKey` from the `authResult`, and retrieve the `organizationId` from the `whoamiResponse`. Then, instantiate the `TurnkeyClient`. ```swift -// ... - -let apiPublicKey = try publicKey.toString(representation: .compressed) -let apiPrivateKey = try privateKey.toString(representation: .raw) +// Use the apiPublicKey and apiPrivateKey from the authResult +let apiPublicKey = authResult.apiPublicKey +let apiPrivateKey = authResult.apiPrivateKey + +// Get the organizationId from the whoamiResponse +let whoamiResponse = authResult.whoamiResponse +var subOrganizationId: String? + +switch whoamiResponse { +case let .ok(response): + switch response.body { + case let .json(whoamiResponse): + subOrganizationId = whoamiResponse.organizationId + print("Sub-Organization ID: \(subOrganizationId ?? "N/A")") + } +case let .undocumented(statusCode, undocumentedPayload): + if let body = undocumentedPayload.body { + let bodyString = try await String(collecting: body, upTo: .max) + print("Undocumented response body: \(bodyString)") + } + print("Undocumented response: \(statusCode)") +} // Initialize a new TurnkeyClient instance with the provided privateKey and publicKey let turnkeyClient = TurnkeyClient(apiPrivateKey: apiPrivateKey, apiPublicKey: apiPublicKey) ``` -### Verifying User Credentials with getWhoami +### Create Read Only Session -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. - -Note: We're using the `organizationId` from the email authentication result as the `organizationId` for the `getWhoami` request. +Next, use the `subOrganizationId` to call the `createReadOnlySession` method on the `TurnkeyClient`. ```swift do { - let whoamiResponse = try await turnkeyClient.getWhoami(organizationId: organizationId) - - switch whoamiResponse { - case .ok(let response): - print("Credential verification successful: \(whoamiResponse)") - case .undocumented(let statusCode, let undocumentedPayload): - print("Error during credential verification: \(error)") + // Use the user's sub-organization ID to create a read-only session + if let orgId = subOrganizationId { + let readOnlySessionOutput = try await turnkeyClient.createReadOnlySession(organizationId: orgId) + print("Read-only session created successfully: \(readOnlySessionOutput)") + } else { + print("Failed to extract organization ID.") } } catch { - print("Error during credential verification: \(error)") + print("Error occurred while creating read-only session: \(error)") } ``` diff --git a/docs/proxy-middleware.md b/docs/proxy-middleware.md index 9867a5c..afea20a 100644 --- a/docs/proxy-middleware.md +++ b/docs/proxy-middleware.md @@ -1,4 +1,4 @@ -# Prox yMiddleware +# Proxy Middleware The [`ProxyMiddleware`](/Sources/Middleware/ProxyMiddleware.swift) is integrated into the TurnkeyClient through its initializer that accepts a proxy server URL. This setup is particularly useful for handling scenarios where direct authenticated requests are not feasible, such as during onboarding flows or when additional server-side processing is required before reaching Turnkey's backend. @@ -8,7 +8,7 @@ Here's how you can initialize the TurnkeyClient with a proxy server URL: import TurnkeySDK // Initialize the TurnkeyClient with a proxy server URL -let turnkeyClient = TurnkeyClient(proxyURL: "https://your-proxy-server.com") +let turnkeyClient = TurnkeyClient(proxyURL: "https://your-proxy-server.com/api/turnkey-proxy") ``` This initializer configures the TurnkeyClient to route all requests through the specified proxy server. The proxy server is then responsible for forwarding these requests to a backend capable of authenticating them using an API private key. After authentication, the proxy server forwards the requests to Turnkey's backend and relays the response back to the client. @@ -21,8 +21,53 @@ This setup is especially useful for operations like: ## Important Notes -- **Response Matching**: It is crucial that the response from the developer's backend matches exactly with what would be expected from Turnkey's backend. Any discrepancy in the response format or data can cause the request to fail. -- **Security**: Ensure that the proxy server is secure and only accessible to authorized entities to prevent unauthorized access and data breaches. +#### X-Forwarded-For Header + +The middleware adds an `X-Forwarded-For` header to each request, which contains the original request URL. This is used to forward the request to Turnkey's backend. + +Example implementation of a proxy server: + +```javascript +const express = require('express'); +const app = express(); + +app.use(express.json()); + +app.post('/api/turnkey-proxy', async (req, res) => { + // The original request URL e.g. https://api.turnkey.com/public/v1/submit/email_auth + const turnkeyApiRequestURL = req.headers['x-forwarded-for']; + + // Remove the 'x-forwarded-for' header + delete req.headers['x-forwarded-for']; + + try { + // Forward the request to the original URL + const response = await fetch(turnkeyApiRequestURL, { + method: 'POST', + headers: req.headers, + body: JSON.stringify(req.body), + }); + + // Get the response data + const data = await response.json(); + + // Send the response back to the client + res.status(response.status).json(data); + } catch (error) { + console.error('Error forwarding request:', error); + res.status(500).send('Internal Server Error'); + } +}); + +// Start the server +app.listen(3000, () => { + console.log('Server is running on port 3000'); +}); +``` + +#### Response Matching + +It is crucial that the response from the developer's backend matches exactly with what would be expected from Turnkey's backend. Any discrepancy in the response format or data can cause the request to fail. ## Conclusion diff --git a/example/TurnkeyiOSExample/TurnkeyiOSExample/AccountManager.swift b/example/TurnkeyiOSExample/TurnkeyiOSExample/AccountManager.swift index c5ae285..579b56d 100644 --- a/example/TurnkeyiOSExample/TurnkeyiOSExample/AccountManager.swift +++ b/example/TurnkeyiOSExample/TurnkeyiOSExample/AccountManager.swift @@ -153,33 +153,15 @@ class AccountManager: NSObject, ASAuthorizationControllerPresentationContextProv func verifyEncryptedBundle(bundle: String) async { do { - let (privateKey, publicKey) = try authKeyManager.decryptBundle(bundle) - - let apiPublicKey = try publicKey.toString(representation: .compressed) - let apiPrivateKey = try privateKey.toString(representation: .raw) - - print("apiPrivateKey: \(apiPrivateKey) - apiPublicKey:\(apiPublicKey)") - // Initialize a new TurnkeyClient instance with the provided privateKey and publicKey - let turnkeyClient = TurnkeyClient(apiPrivateKey: apiPrivateKey, apiPublicKey: apiPublicKey) - let response = try await turnkeyClient.getWhoami(organizationId: parentOrgId) - - // Assert the response - switch response { - case let .ok(response): - switch response.body { - case let .json(emailAuthResponse): - print(emailAuthResponse) - } - case let .undocumented(statusCode, undocumentedPayload): - // Handle the undocumented response - if let body = undocumentedPayload.body { - let bodyString = try await String(collecting: body, upTo: .max) - print("Undocumented response body: \(bodyString)") - } - print("Undocumented response: \(statusCode)") + // Use the stored verify closure + if let verify = verifyClosure { + let authResult = try await verify(bundle) + print("Verification successful: \(authResult)") + } else { + print("Verify closure is not set.") } } catch { - print("Error occurred: \(error)") + print("Error occurred during verification: \(error)") } } diff --git a/templates/TurnkeyClient.stencil b/templates/TurnkeyClient.stencil index f618b1a..bc65f79 100644 --- a/templates/TurnkeyClient.stencil +++ b/templates/TurnkeyClient.stencil @@ -54,10 +54,6 @@ public struct TurnkeyClient { /// - apiPublicKey: The public key obtained from Turnkey, used to identify the client. /// - baseUrl: The base URL of the Turnkey API. Defaults to "https://api.turnkey.com". /// - /// - Note: For client-side usage where all authenticated requests need secure key management, - /// it is recommended to use the `AuthKeyManager` for creating, storing, and securely using key pairs. - /// For more details, refer to the [AuthKeyManager](#AuthKeyManager). - /// /// - Example: /// ``` /// let client = TurnkeyClient(apiPrivateKey: "your_api_private_key", apiPublicKey: "your_api_public_key")