From 0b3b920cc4f822e0cca96ab4c8da1e7bce0eea3e Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Sun, 28 Jul 2024 16:14:51 -0700 Subject: [PATCH 1/2] Remove erroneous assertion (#34) #31 adjusted the assertion during sending errors back, and reintroduced the data race from canceling the autofill request due to starting a modal one (explicitly returning a failure to the continuation, clearing it out, and canceling the authController - which went back to sendError) This removes the assertion, since it was logically unsound, not to mention unhelpful. I've done another bunch of manual testing in a sample app and can no longer reproduce the issue. I'm not sure how to cover this in automated tests to prevent another regression, but I'll file a ticket in case someone finds a way. --- Sources/SnapAuth/SnapAuth+ASACD.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/SnapAuth/SnapAuth+ASACD.swift b/Sources/SnapAuth/SnapAuth+ASACD.swift index 0b2e8b6..55237a7 100644 --- a/Sources/SnapAuth/SnapAuth+ASACD.swift +++ b/Sources/SnapAuth/SnapAuth+ASACD.swift @@ -40,8 +40,6 @@ extension SnapAuth: ASAuthorizationControllerDelegate { /// Sends the error to the appropriate delegate method and resets the internal state back to idle private func sendError(_ error: SnapAuthError) { - // One or the other should eb set, but not both - assert(continuation != nil) continuation?.resume(returning: .failure(error)) continuation = nil } From 6a8e56122577568f6133699815b01f48bc812d68 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Mon, 29 Jul 2024 11:56:59 -0700 Subject: [PATCH 2/2] Set up documentation generation for Swift Package Index; enhance docs (#37) This adds a bunch of documentation using the `docc` format, and adds a Swift Package Index manifest so that it can be automatically generated, linked, and hosted. Fixes #36 --- .spi.yml | 7 ++ Sources/SnapAuth/Errors.swift | 32 ++++++--- Sources/SnapAuth/SnapAuth.docc/SnapAuth.md | 84 ++++++++++++++++++++++ Sources/SnapAuth/SnapAuth.swift | 6 +- Sources/SnapAuth/SnapAuthDelegate.swift | 13 +++- 5 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 .spi.yml create mode 100644 Sources/SnapAuth/SnapAuth.docc/SnapAuth.md diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..5bc48b5 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,7 @@ +# Swift Package Index +version: 1 +builder: + configs: + - documentation_targets: + - SnapAuth + platform: ios diff --git a/Sources/SnapAuth/Errors.swift b/Sources/SnapAuth/Errors.swift index 584dbe6..3742060 100644 --- a/Sources/SnapAuth/Errors.swift +++ b/Sources/SnapAuth/Errors.swift @@ -1,34 +1,46 @@ import AuthenticationServices +/// SnapAuth error codes +/// +/// Authentication and credential registration can fail or be rejected in +/// numerous ways, and applications should be prepared to handle these +/// scenarios. public enum SnapAuthError: Error { /// The network request was disrupted. This is generally safe to retry. case networkInterruption - /// A new request is starting, so the one in flight was canceled + /// Only a single request can run at a time. A new request is starting, so + /// the current one is being canceled. case newRequestStarting - /// This needs APIs that are not supported on the current platform + /// This needs APIs that are not supported on the current platform. You can + /// use `if #available()` conditionals, or similar, to avoid this. case unsupportedOnPlatform // MARK: Internal errors, which could represent SnapAuth bugs /// The SDK received a response from SnapAuth, but it arrived in an - /// unexpected format. If you encounter this, please reach out to us. + /// unexpected format. + /// + /// If you encounter this, please reach out to us. case malformedResposne - /// The SDK was unable to encode data to send to SnapAuth. If you encounter - /// this, please reach out to us. + /// The SDK was unable to encode data to send to SnapAuth. + /// + /// If you encounter this, please reach out to us. case sdkEncodingError - /// The request was valid and understood, but processing was refused. If you - /// encounter this, please reach out to us. + /// The request was valid and understood, but processing was refused. + /// + /// If you encounter this, please reach out to us. case badRequest // MARK: Weird responses - /// ASAuthorizationServices sent SnapAuth an unexpected type of response - /// which we don't know how to handle. If you encounter this, please reach - /// out to us. + /// `ASAuthorizationServices` sent SnapAuth an unexpected type of response + /// which we don't know how to handle. + /// + /// If you encounter this, please reach out to us. case unexpectedAuthorizationType /// Some of the data SnapAuth requested during credential registration was diff --git a/Sources/SnapAuth/SnapAuth.docc/SnapAuth.md b/Sources/SnapAuth/SnapAuth.docc/SnapAuth.md new file mode 100644 index 0000000..6e282ac --- /dev/null +++ b/Sources/SnapAuth/SnapAuth.docc/SnapAuth.md @@ -0,0 +1,84 @@ +# ``SnapAuth`` + +The official [SnapAuth](https://www.snapauth.app) SDK for Apple platforms. + +## Overview + +SnapAuth allows you to quickly, easily, and reliably add passkeys to your web and native apps. +You can be up and running in minutes on all platforms without ever having to look at `AuthenticationServices`. + +This SDK supports all native Apple platforms that permit passkey use - iOS, iPadOS, macOS, visionOS, and tvOS. +Apple Watch and watchOS do not support passkeys. +If and when it does, we'll add support as well. + +## Getting Started + +To use SnapAuth, you'll first need to register an account: [](https://www.snapauth.app/register). +Registration is free, but SnapAuth is a paid service and you'll be limited to a small number of users during the free trial. + +Once you register, you'll be taken to our [dashboard](https://dashboard.snapauth.app). +In there, you can get access to your `publishable key`, which you'll need to use SnapAuth in your native app. + +## Topics + +### Setup + +To start, import the SnapAuth SDK and provide it your _publishable key_ from the dashboard. + +```swift +import SnapAuth + +let snapAuth = SnapAuth(publishableKey: "pubkey_your_key") +``` + +- ``SnapAuth/SnapAuth`` +- ``SnapAuth/SnapAuth/init(publishableKey:urlBase:)`` + +### Credential Registration + +Create a new passkey (or associate a hardware key) for the user. + +- ``SnapAuth/startRegister(name:displayName:authenticators:)`` + +### Authentication + +Authenticate a user using their previously-registered passkey or hardware key. + +- ``SnapAuth/SnapAuth/startAuth(_:authenticators:)`` +- ``SnapAuth/AuthenticatingUser`` + +### Credential AutoFill + +When a user focuses a `TextField` with `.textContentType(.username)`, the QuickType bar can suggest passkeys. +This allows the user to authenticate without even having to fill in their username. + +AutoFill is only available on iOS and visionOS. + +- ``SnapAuth/SnapAuth/handleAutoFill()`` + +### Controlling Authenticator Types + +For the best user experience and most flexibility, allow all of the platform's supported authenticators. +Passkeys are supported on all platforms. +External hardware authenticators are not supported on tvOS or visionOS. + +- ``SnapAuth/SnapAuth/Authenticator`` + +### Handling Responses + +All of the SDK's core methods will return a `SnapAuthResult`, which is an alias for `Result`. +Inspect the result to decide how to proceed. + +```swift +let result = await snapAuth.startAuth(.id("usr_123456")) +switch result { +case .success(let auth): + // Send auth.token to your backend to sign in the user +case .failure(let error): + // Examine the error and decide how best to proceed +} +``` + +- ``SnapAuth/SnapAuthResult`` +- ``SnapAuth/SnapAuthTokenInfo`` +- ``SnapAuth/SnapAuthError`` diff --git a/Sources/SnapAuth/SnapAuth.swift b/Sources/SnapAuth/SnapAuth.swift index 0a5b9c0..5f43ec0 100644 --- a/Sources/SnapAuth/SnapAuth.swift +++ b/Sources/SnapAuth/SnapAuth.swift @@ -22,11 +22,12 @@ public class SnapAuth: NSObject { // NSObject for ASAuthorizationControllerDeleg internal var continuation: CheckedContinuation? + /// Initialize the SnapAuth SDK /// - Parameters: /// - publishableKey: Your SnapAuth publishable key. This can be obtained /// from the [SnapAuth dashboard](https://dashboard.snapauth.app) - /// - urlBase: A custom URL base for the SnapAuth API. This is generally - /// for internal use. + /// - urlBase: A custom URL base for the SnapAuth API. This should usually + /// be omitted and left as the default value. public init( publishableKey: String, urlBase: URL = URL(string: "https://api.snapauth.app")! @@ -223,6 +224,7 @@ public class SnapAuth: NSObject { // NSObject for ASAuthorizationControllerDeleg } } +/// A representation of the user that is trying to authenticate. public enum AuthenticatingUser { /// Your application's internal identifier for the user (usually a primary key) case id(String) diff --git a/Sources/SnapAuth/SnapAuthDelegate.swift b/Sources/SnapAuth/SnapAuthDelegate.swift index de70764..a2ba49d 100644 --- a/Sources/SnapAuth/SnapAuthDelegate.swift +++ b/Sources/SnapAuth/SnapAuthDelegate.swift @@ -1,5 +1,15 @@ import Foundation +/// The success case of adding or using a passkey. +/// +/// `SnapAuthTokenInfo` is the result of a successful authentication or credential registration. +/// +/// This holds a short-lived token which should be sent to your application's backend for verifcation and use. +/// +/// The token on its own does not authenticate a user; instead, the token must be sent to your application's backend for processing and verification. +/// Tokens are short-lived and one-time-use. +/// +/// See our [server documentation](https://docs.snapauth.app/server.html) for additional info on how to use these tokens. public struct SnapAuthTokenInfo { /// The registration or authentication token. /// @@ -10,9 +20,10 @@ public struct SnapAuthTokenInfo { /// When the paired token will expire. /// - /// If you try to use it after this time (or more than once), the request + /// If you try to use it after this time, or more than once, the request /// will be rejected. public let expiresAt: Date } +/// An alias for the native `Result` type with our success and failure states. public typealias SnapAuthResult = Result