Skip to content

Commit

Permalink
Update examples and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
taylorjdawson committed Nov 12, 2024
1 parent 7602b65 commit 338f53a
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 94 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions Sources/TurnkeySDK/TurnkeyClient.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
148 changes: 92 additions & 56 deletions docs/email-auth.md
Original file line number Diff line number Diff line change
@@ -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).

Expand All @@ -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 = "[email protected]"
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)")
}
```
53 changes: 49 additions & 4 deletions docs/proxy-middleware.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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.
Expand All @@ -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

Expand Down
32 changes: 7 additions & 25 deletions example/TurnkeyiOSExample/TurnkeyiOSExample/AccountManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}

Expand Down
4 changes: 0 additions & 4 deletions templates/TurnkeyClient.stencil
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 338f53a

Please sign in to comment.