Skip to content

Commit

Permalink
Add documentation for email auth; minor fixes/refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
taylorjdawson committed May 7, 2024
1 parent 2502df2 commit 396e600
Show file tree
Hide file tree
Showing 16 changed files with 1,411 additions and 600 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Note: Tests will fail if the types generated by the swift-openapi-generator are
The Turnkey SDK project is structured into several key directories:

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

## Makefile Commands
Expand Down
10 changes: 1 addition & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,9 @@ 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
dependencies: [
.package(url: "https://github.com/your-organization/swift-sdk", .upToNextMajor(from: "1.0.0"))
]
.package(url: "https://github.com/tkhq/swift-sdk", from: "1.0.0")
```

Ensure to replace `"https://github.com/your-organization/swift-sdk"` with the actual URL of the Swift SDK repository.

## Usage

Here's a quick guide on how to use the TurnkeySDK in your Swift project:

## Contributing

For guidelines on how to contribute to the Swift SDK, please refer to the [contributing guide](CONTRIBUTING.md).
Expand Down
55 changes: 2 additions & 53 deletions Sources/Shared/PasskeyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,67 +136,16 @@ public class PasskeyManager: NSObject, ASAuthorizationControllerDelegate,
let attestationObject = rawAttestationObject.base64URLEncodedString()
let clientDataJson = credentialRegistration.rawClientDataJSON.base64URLEncodedString()
let credentialId = credentialRegistration.credentialID.base64URLEncodedString()



let attestation = Attestation(
credentialId: credentialId, clientDataJson: clientDataJson, attestationObject: attestationObject)
credentialId: credentialId, clientDataJson: clientDataJson,
attestationObject: attestationObject)

let registrationResult = PasskeyRegistrationResult(
challenge: challenge, attestation: attestation)

notifyRegistrationCompleted(result: registrationResult)
return
// credentialRegistration.rawAttestationObject?.base64URLEncodedString()
//
//
// guard
// let clientDataJSON = try? JSONDecoder().decode(
// ClientDataJSON.self, from: credentialRegistration.rawClientDataJSON)
// else {
// notifyRegistrationFailed(error: PasskeyRegistrationError.invalidClientDataJSON)
// return
// }
//
// guard let rawAttestationData = credentialRegistration.rawAttestationObject else {
// notifyRegistrationFailed(error: PasskeyRegistrationError.invalidAttestation)
// return
// }

// guard
// let attestation = try? JSONDecoder().decode(
// ClientDataJSON.self, from: rawAttestationData)
// else {
// notifyRegistrationFailed(error: PasskeyRegistrationError.invalidClientDataJSON)
// return
// }

// do {
// guard let jsonData = try JSONSerialization.data(withJSONObject: rawAttestationData, options: [])
// else {
// notifyRegistrationFailed(error: PasskeyRegistrationError.invalidAttestation)
// return
// }
//
// guard let jsonString = String(data: jsonData, encoding: .utf8) else {
// notifyRegistrationFailed(error: PasskeyRegistrationError.invalidAttestation)
// return
// }
// }
// catch {
//
// }

// let attestation =
// String(data: attestationData, encoding: .utf8) ?? "Invalid attestation encoding"
//
// let challenge = clientDataJSON.challenge

// let registrationResult = PasskeyRegistrationResult(
// challenge: challenge, attestation: attestation)
//
// notifyRegistrationCompleted(result: registrationResult)

case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
logger.log("A passkey was used to sign in: \(credentialAssertion)")
notifyPasskeyAssertionCompleted(result: credentialAssertion)
Expand Down
11 changes: 5 additions & 6 deletions Tests/TurnkeySDKTests/TurnkeySDKTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,28 +185,27 @@ final class TurnkeySDKTests: XCTestCase {
let email = "[email protected]"
let targetPublicKey =
"04d3f967632eb6a317059a164b7b71704c22fb2b0f20e6f27f62fdadeea14da558318a88bb9bb06c5886397666b4f1a1e3b92337c3ebebb4d570d4c735bc46fe83"
// Data(hexString: apiPrivateKey!)

let apiKeyName = "email-auth-key"
let expirationSeconds = "3600"


let output = try await client.emailAuth(
organizationId: organizationId!,
email: email,
targetPublicKey: targetPublicKey,
apiKeyName: apiKeyName,
expirationSeconds: expirationSeconds,
emailCustomization: Components.Schemas.EmailCustomizationParams()
emailCustomization: nil
)

// Assert the response
switch output {
case .ok(let response):
switch response.body {
case .json(let emailAuthResponse):
// Assert the expected properties in the emailAuthResponse
XCTAssertNotNil(emailAuthResponse.activityId)
// XCTAssertEqual(emailAuthResponse.status, "Success")

// Assert the expected properties in the emailAuthResponse
XCTAssertNotNil(emailAuthResponse.activity.id)
}
case .undocumented(let statusCode, let undocumentedPayload):
// Handle the undocumented response
Expand Down
125 changes: 125 additions & 0 deletions docs/email-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Email Authentication

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.

For a more detailed explanation of the email authentication process, please refer to the [Turnkey API documentation](https://docs.turnkey.com/features/email-auth).

## Prerequisites

- A proxy server set up to handle authentication requests.
- Organization ID and API key name from your Turnkey account.

## Step 1: Initialize the TurnkeyClient

Create an instance of TurnkeyClient using a proxy URL to handle the authentication.
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.
We are using a proxy URL because an authenticated user is required to initiate the email authentication request.

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.

```swift
let proxyURL = "http://localhost:3000/api/email-auth"
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

Use AuthKeyManager to generate a new ephemeral key pair for the email authentication flow.
This key pair is not persisted and is used temporarily during the authentication process.
Note: The 'domain' is used for scoping the key storage specific to an app and is optional for persisting the key.

```swift
let authKeyManager = AuthKeyManager(domain: "your_domain")
let publicKey = try authKeyManager.createKeyPair()
```

## Step 3: 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

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.

```swift
let emailAuthResult = try await client.emailAuth(
organizationId: organizationId,
email: email,
targetPublicKey: targetPublicKey,
apiKeyName: "your_api_key_name",
expirationSeconds: expirationSeconds,
emailCustomization: emailCustomization
)
```

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.

```swift
switch emailAuthResult {
case .ok(let response):
// The user's sub-organizationId:
let organizationId = response.activity.organizationId
// Proceed with user session creation or update
case .undocumented(let statusCode, let undocumentedPayload):
// Handle error, possibly retry or log
}
```

## Step 6: Verify Encrypted Bundle

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.

```swift
let (privateKey, publicKey) = try authManager.decryptBundle(encryptedBundle)
```

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:

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.

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 7: Initializing the TurnkeyClient and Verify the user

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:

```swift
// ...

let apiPublicKey = try publicKey.toString(representation: .compressed)
let apiPrivateKey = try privateKey.toString(representation: .raw)

// Initialize a new TurnkeyClient instance with the provided privateKey and publicKey
let turnkeyClient = TurnkeyClient(apiPrivateKey: apiPrivateKey, apiPublicKey: apiPublicKey)
```

### Verifying User Credentials with getWhoami

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.

```swift
do {
let whoamiResponse = try await turnkeyClient.getWhoami(organizationId: organizationId /* from emailAuthResult */)

switch whoamiResponse {
case .ok(let response):
print("Credential verification successful: \(whoamiResponse)")
case .undocumented(let statusCode, let undocumentedPayload):
print("Error during credential verification: \(error)")
}
} catch {
print("Error during credential verification: \(error)")
}


```
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
1E4FCBD02BCDCAB80042A4B2 /* UserHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FCBCF2BCDCAB80042A4B2 /* UserHomeViewController.swift */; };
1E4FCBD22BCDCAD00042A4B2 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FCBD12BCDCAD00042A4B2 /* SignInViewController.swift */; };
1E81BE842BDC2755006A9A0A /* EmailAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E81BE832BDC2755006A9A0A /* EmailAuthViewController.swift */; };
1EABCB2A2BE468B00037DD52 /* web3swift in Frameworks */ = {isa = PBXBuildFile; productRef = 1EABCB292BE468B00037DD52 /* web3swift */; };
1EABCB2C2BE491790037DD52 /* UserManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB2B2BE491790037DD52 /* UserManager.swift */; };
1EABCB2F2BE498A50037DD52 /* UserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB2E2BE498A50037DD52 /* UserModel.swift */; };
1EABCB6B2BE4B0000037DD52 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB6A2BE4B0000037DD52 /* SessionManager.swift */; };
1EABCB6F2BE56B4F0037DD52 /* SendTransactionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB6E2BE56B4F0037DD52 /* SendTransactionViewController.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -59,6 +64,10 @@
1E4FCBCF2BCDCAB80042A4B2 /* UserHomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserHomeViewController.swift; sourceTree = "<group>"; };
1E4FCBD12BCDCAD00042A4B2 /* SignInViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = "<group>"; };
1E81BE832BDC2755006A9A0A /* EmailAuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailAuthViewController.swift; sourceTree = "<group>"; };
1EABCB2B2BE491790037DD52 /* UserManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserManager.swift; sourceTree = "<group>"; };
1EABCB2E2BE498A50037DD52 /* UserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserModel.swift; sourceTree = "<group>"; };
1EABCB6A2BE4B0000037DD52 /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionManager.swift; path = ../../../../../../../../Documents/SessionManager.swift; sourceTree = "<group>"; };
1EABCB6E2BE56B4F0037DD52 /* SendTransactionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTransactionViewController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -67,6 +76,7 @@
buildActionMask = 2147483647;
files = (
1E4CD78B2BE2CF180097E2CB /* TurnkeySDK in Frameworks */,
1EABCB2A2BE468B00037DD52 /* web3swift in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -110,7 +120,9 @@
1E27801F2BCA349300AE790C /* TurnkeyiOSExample */ = {
isa = PBXGroup;
children = (
1EABCB2D2BE498930037DD52 /* Models */,
1E37D5972BD2F3A400962F0D /* Info.plist */,
1EABCB2E2BE498A50037DD52 /* UserModel.swift */,
1E37D5962BD2EBD200962F0D /* TurnkeyiOSExample.entitlements */,
1E2780242BCA349400AE790C /* Assets.xcassets */,
1E2780262BCA349400AE790C /* Preview Content */,
Expand All @@ -122,6 +134,9 @@
1E37D5992BD2FFDD00962F0D /* LaunchScreen.storyboard */,
1E37D59D2BD3004D00962F0D /* Main.storyboard */,
1E81BE832BDC2755006A9A0A /* EmailAuthViewController.swift */,
1EABCB2B2BE491790037DD52 /* UserManager.swift */,
1EABCB6A2BE4B0000037DD52 /* SessionManager.swift */,
1EABCB6E2BE56B4F0037DD52 /* SendTransactionViewController.swift */,
);
path = TurnkeyiOSExample;
sourceTree = "<group>";
Expand Down Expand Up @@ -151,6 +166,13 @@
path = TurnkeyiOSExampleUITests;
sourceTree = "<group>";
};
1EABCB2D2BE498930037DD52 /* Models */ = {
isa = PBXGroup;
children = (
);
path = Models;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand All @@ -169,6 +191,7 @@
name = TurnkeyiOSExample;
packageProductDependencies = (
1E4CD78A2BE2CF180097E2CB /* TurnkeySDK */,
1EABCB292BE468B00037DD52 /* web3swift */,
);
productName = TurnkeyiOSExample;
productReference = 1E27801D2BCA349300AE790C /* TurnkeyiOSExample.app */;
Expand Down Expand Up @@ -244,6 +267,7 @@
mainGroup = 1E2780142BCA349300AE790C;
packageReferences = (
1E4351F82BCDDBAD00BF67F2 /* XCLocalSwiftPackageReference "../.." */,
1EABCB282BE468B00037DD52 /* XCRemoteSwiftPackageReference "web3swift" */,
);
productRefGroup = 1E27801E2BCA349300AE790C /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -291,8 +315,12 @@
files = (
1E81BE842BDC2755006A9A0A /* EmailAuthViewController.swift in Sources */,
1E4FCBD02BCDCAB80042A4B2 /* UserHomeViewController.swift in Sources */,
1EABCB6B2BE4B0000037DD52 /* SessionManager.swift in Sources */,
1EABCB2F2BE498A50037DD52 /* UserModel.swift in Sources */,
1EABCB2C2BE491790037DD52 /* UserManager.swift in Sources */,
1E37D5932BD2D77C00962F0D /* AccountManager.swift in Sources */,
1E4FCBCA2BCB71820042A4B2 /* AppDelegate.swift in Sources */,
1EABCB6F2BE56B4F0037DD52 /* SendTransactionViewController.swift in Sources */,
1E4FCBD22BCDCAD00042A4B2 /* SignInViewController.swift in Sources */,
1E4FCBCC2BCDCA6C0042A4B2 /* SceneDelegate.swift in Sources */,
);
Expand Down Expand Up @@ -632,11 +660,27 @@
};
/* End XCLocalSwiftPackageReference section */

/* Begin XCRemoteSwiftPackageReference section */
1EABCB282BE468B00037DD52 /* XCRemoteSwiftPackageReference "web3swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/web3swift-team/web3swift.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 3.2.1;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
1E4CD78A2BE2CF180097E2CB /* TurnkeySDK */ = {
isa = XCSwiftPackageProductDependency;
productName = TurnkeySDK;
};
1EABCB292BE468B00037DD52 /* web3swift */ = {
isa = XCSwiftPackageProductDependency;
package = 1EABCB282BE468B00037DD52 /* XCRemoteSwiftPackageReference "web3swift" */;
productName = web3swift;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 1E2780152BCA349300AE790C /* Project object */;
Expand Down
Loading

0 comments on commit 396e600

Please sign in to comment.