Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Commit

Permalink
Added improved error handling and Open API generated code (#90)
Browse files Browse the repository at this point in the history
* feat(PSG-3976): added openapi generated code

* Refactor auth methods to use OpenAPI generated code (#86)

* refactor(PSG-3976): otp & ml auth methods refactor

* refactor(PSG-3976): refactored passkey methods

* refactor(PSG-3976): remove olde MagicLink struct

* refactor(PSG-3976): removed unused api client code

* Finish replacing old Passage API code with generated code. (#87)

* refactor(PSG-3976): otp & ml auth methods refactor

* refactor(PSG-3976): refactored passkey methods

* refactor(PSG-3976): remove olde MagicLink struct

* refactor(PSG-3976): removed unused api client code

* refactor: removed `PassageAPIClient`

* refactor: cleaned up unused models

* Updated integration tests (#88)

* refactor(PSG-3976): otp & ml auth methods refactor

* refactor(PSG-3976): refactored passkey methods

* refactor(PSG-3976): remove olde MagicLink struct

* refactor(PSG-3976): removed unused api client code

* refactor: removed `PassageAPIClient`

* refactor: cleaned up unused models

* refactor: removed PassageSettings class

* refactor: update app info tests

* refactor: updated get user tests

* refactor: updated otp tests

* refactor: updated magic link tests

* refactor: updated change contact tests

* refactor: updated current user tests

* refactor: update device tests

* refactor: updated session tests

* refactor: updated social auth tests

* refactor: removed outdated unit tests

* refactor: removed unnecessary static test data

* refactor: misc test cleanup

* Update Sources/Passage/PassageAuth.swift

Signed-off-by: Ricky Padilla <[email protected]>

---------

Signed-off-by: Ricky Padilla <[email protected]>

* Added improved error enums (#89)

* chore: removed old unit tests

* feat: added script for fixing generated code

* fix: fixed passkey login issue w user id

* chore: added `CONTRIBUTING.md` and updated code gen fix script.

* chore: misc cleanup

* chore: misc cleanup

* fix: add try safety to identifierExists method

* chore: add error explanation to `identifierExists`

* feat: added more auth error cases

* fix: fixed auth new error cases

* chore: increased mailosaur email check retry count

---------

Signed-off-by: Ricky Padilla <[email protected]>
  • Loading branch information
rickycpadilla authored Jun 27, 2024
1 parent aae2ac1 commit 857245d
Show file tree
Hide file tree
Showing 295 changed files with 13,394 additions and 2,817 deletions.
10 changes: 0 additions & 10 deletions .github/workflows/test-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,6 @@ env:
MAILOSAUR_API_KEY: ${{ secrets.MAILOSAUR_API_KEY }}

jobs:
unit-test-iOS:
name: Build and Unit Test
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v2

- name: Test iOS
run: xcodebuild clean build-for-testing test -scheme Passage -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 15 Pro"

integration-test-iOS:
name: Build and Integration Tests iOS
runs-on: macos-latest
Expand Down
17 changes: 0 additions & 17 deletions .github/workflows/unit-tests.yml

This file was deleted.

11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Code generation

1. Run below command to update the OpenAPI generated code:
```
openapi-generator generate -i https://api.swaggerhub.com/apis/passage/passage-auth-api/1 -g swift5 --additional-properties=responseAs=AsyncAwait -o Sources/Passage/generated
```

2. Run script to fix known generated code issues:
```
python3 Sources/Passage/fix_generated_code.py
```
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"pins" : [
{
"identity" : "anycodable",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Flight-School/AnyCodable",
"state" : {
"revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05",
"version" : "0.6.7"
}
},
{
"identity" : "swiftkeychainwrapper",
"kind" : "remoteSourceControl",
Expand Down
7 changes: 2 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,15 @@ let package = Package(
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/Flight-School/AnyCodable", .upToNextMajor(from: "0.6.1")),
.package(url: "https://github.com/jrendel/SwiftKeychainWrapper", from: "4.0.1")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Passage",
dependencies: ["SwiftKeychainWrapper"], resources: [.copy("Resources/settings.json")]),
.testTarget(
name: "PassageTests",
dependencies: ["Passage"]),
dependencies: ["AnyCodable", "SwiftKeychainWrapper"], resources: [.copy("Resources/settings.json")]),
.testTarget(
name: "Integration",
dependencies: ["Passage"]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import AuthenticationServices
@available(iOS 16.0, *)
protocol LoginAuthorizationControllerProtocol: NSObject, ASAuthorizationControllerDelegate {

func login(from response: WebauthnLoginStartResponse) async throws -> ASAuthorizationPublicKeyCredentialAssertion?
func login(from response: LoginWebAuthnStartResponse) async throws -> ASAuthorizationPublicKeyCredentialAssertion?

func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ class LoginAuthorizationController : NSObject, ASAuthorizationControllerDelegate

static var shared: LoginAuthorizationControllerProtocol = LoginAuthorizationController()

func login(from response: WebauthnLoginStartResponse) async throws -> ASAuthorizationPublicKeyCredentialAssertion? {
func login(from response: LoginWebAuthnStartResponse) async throws -> ASAuthorizationPublicKeyCredentialAssertion? {
PassageAutofillAuthorizationController.shared.cancel()
let rpId = response.handshake.challenge.publicKey.rpId
let challenge = response.handshake.challenge.publicKey.challenge
guard let decodedChallenge = challenge.decodeBase64Url() else {
guard
let rpId = response.handshake.challenge.publicKey.rpId,
let decodedChallenge = challenge.decodeBase64Url()
else {
return nil
}
// Handle platform request
Expand Down Expand Up @@ -80,34 +82,13 @@ class LoginAuthorizationController : NSObject, ASAuthorizationControllerDelegate
credentialAssertionThrowingContinuation?.resume(returning: credentialAssertion)
credentialAssertionThrowingContinuation = nil
default:
credentialAssertionThrowingContinuation?.resume(throwing: PassageASAuthorizationError.unknownAuthorizationType)
credentialAssertionThrowingContinuation?.resume(throwing: ASAuthorizationError.init(.invalidResponse))
}
}


public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {

// TODO: Implement better error handling below
guard let authorizationError = error as? ASAuthorizationError else {
credentialAssertionThrowingContinuation?.resume(throwing: PassageASAuthorizationError.unknown)
credentialAssertionThrowingContinuation = nil
return
}


if authorizationError.code == .canceled {
// Either the system doesn't find any credentials and the request ends silently, or the user cancels the request.
// This is a good time to show a traditional login form, or ask the user to create an account.
credentialAssertionThrowingContinuation?.resume(throwing: PassageASAuthorizationError.canceled)
credentialAssertionThrowingContinuation = nil
} else {
// Another ASAuthorization error.
// Note: The userInfo dictionary contains useful information.
credentialAssertionThrowingContinuation?.resume(throwing: PassageASAuthorizationError.unknownAuthorizationType)
credentialAssertionThrowingContinuation = nil
}
credentialAssertionThrowingContinuation?.resume(throwing: error)
credentialAssertionThrowingContinuation = nil
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class PassageAutofillAuthorizationController : NSObject, ASAuthorizationC
public static let shared = PassageAutofillAuthorizationController()

var authController : ASAuthorizationController?
var startResponse : WebauthnLoginStartResponse?
var startResponse : LoginWebAuthnStartResponse?
var isPerformingModalRequest : Bool = false
var authenticationAnchor: ASPresentationAnchor?

Expand Down Expand Up @@ -47,7 +47,7 @@ public class PassageAutofillAuthorizationController : NSObject, ASAuthorizationC
let startResponse = try await PassageAuth.autoFillStart()
self.startResponse = startResponse

let rpId = startResponse.handshake.challenge.publicKey.rpId
guard let rpId = startResponse.handshake.challenge.publicKey.rpId else { return }
let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId)

let challenge = self.startResponse!.handshake.challenge.publicKey.challenge
Expand All @@ -69,7 +69,7 @@ public class PassageAutofillAuthorizationController : NSObject, ASAuthorizationC
case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
Task {
guard startResponse != nil else {
throw PassageASAuthorizationError.invalidStartResponse
throw ASAuthorizationError.init(.invalidResponse)
}
let loginResult = try await PassageAuth.autoFillFinish(startResponse: startResponse!, credentialAssertion: credentialAssertion)
isPerformingModalRequest = false
Expand All @@ -80,7 +80,7 @@ public class PassageAutofillAuthorizationController : NSObject, ASAuthorizationC
default:
isPerformingModalRequest = false
if let onError = onError {
onError(PassageASAuthorizationError.authorizationTypeUnknown)
onError(ASAuthorizationError.init(.invalidResponse))
}
}
isPerformingModalRequest = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ final internal class PassageSocialAuthController:
let idTokenData = appleIDCredential.identityToken,
let idToken = String(data: idTokenData, encoding: .utf8) else
{
siwaContinuation?.resume(throwing: PassageSocialError.missingAppleCredentials)
siwaContinuation?.resume(throwing: SocialAuthError.missingAppleCredentials)
return
}
siwaContinuation?.resume(returning: (authCode, idToken))
} else {
siwaContinuation?.resume(throwing: PassageSocialError.missingAppleCredentials)
siwaContinuation?.resume(throwing: SocialAuthError.missingAppleCredentials)
}
}

Expand Down Expand Up @@ -161,7 +161,7 @@ final internal class PassageSocialAuthController:
let components = NSURLComponents(url: callbackURL, resolvingAgainstBaseURL: true),
let authCode = components.queryItems?.filter({$0.name == "code"}).first?.value
else {
continuation.resume(throwing: PassageSocialError.missingAuthCode)
continuation.resume(throwing: SocialAuthError.missingAuthCode)
return
}
continuation.resume(returning: authCode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@ class RegistrationAuthorizationController : NSObject, ASAuthorizationControllerD
static var shared: RegistrationAuthorizationControllerProtocol = RegistrationAuthorizationController()

func register(
from response: WebauthnRegisterStartResponse,
from response: RegisterWebAuthnStartResponse,
identifier: String,
includeSecurityKeyOption: Bool = false
) async throws -> ASAuthorizationPublicKeyCredentialRegistration? {
PassageAutofillAuthorizationController.shared.cancel()
let rpId = response.handshake.challenge.publicKey.rp.id
let challenge = response.handshake.challenge.publicKey.challenge
let userId = response.user.id
guard
let publicKey = response.handshake.challenge.publicKey,
let challenge = publicKey.challenge,
let rpId = publicKey.rp?.id,
let userId = response.user?.id
else {
return nil
}
guard let decodedChallenge = challenge.decodeBase64Url() else {
return nil
}
Expand Down Expand Up @@ -77,32 +82,13 @@ class RegistrationAuthorizationController : NSObject, ASAuthorizationControllerD
credentialRegistrationCheckedThrowingContinuation?.resume(returning: credentialRegistration)
credentialRegistrationCheckedThrowingContinuation = nil
default:
credentialRegistrationCheckedThrowingContinuation?.resume(throwing: PassageASAuthorizationError.credentialRegistration)
credentialRegistrationCheckedThrowingContinuation?.resume(throwing: ASAuthorizationError.init(.invalidResponse))
}
}


public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {

// TODO: Implement better error handling below
guard let authorizationError = error as? ASAuthorizationError else {
credentialRegistrationCheckedThrowingContinuation?.resume(throwing: PassageASAuthorizationError.unknown)
credentialRegistrationCheckedThrowingContinuation = nil
return
}


if authorizationError.code == .canceled {
// Either the system doesn't find any credentials and the request ends silently, or the user cancels the request.
// This is a good time to show a traditional login form, or ask the user to create an account.
credentialRegistrationCheckedThrowingContinuation?.resume(throwing: PassageASAuthorizationError.canceled)
credentialRegistrationCheckedThrowingContinuation = nil
} else {
// Another ASAuthorization error.
// Note: The userInfo dictionary contains useful information.
credentialRegistrationCheckedThrowingContinuation?.resume(throwing: PassageASAuthorizationError.unknownAuthorizationType)
credentialRegistrationCheckedThrowingContinuation = nil
}
credentialRegistrationCheckedThrowingContinuation?.resume(throwing: error)
credentialRegistrationCheckedThrowingContinuation = nil
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import AuthenticationServices
protocol RegistrationAuthorizationControllerProtocol: NSObject, ASAuthorizationControllerDelegate {

func register(
from response: WebauthnRegisterStartResponse,
from response: RegisterWebAuthnStartResponse,
identifier: String,
includeSecurityKeyOption: Bool
) async throws -> ASAuthorizationPublicKeyCredentialRegistration?
Expand Down
40 changes: 40 additions & 0 deletions Sources/Passage/Errors/AddDeviceError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import AuthenticationServices

public enum AddDeviceError: PassageError {

case authorizationFailed
case canceled
case credentialChallengeParsingFailed
case inactiveUser
case invalidRequest
case unauthorized
case unspecified

public static func convert(error: Error) -> AddDeviceError {
// Check if error is already proper
if let addDeviceError = error as? AddDeviceError {
return addDeviceError
}
// Handle client error
if let errorResponse = error as? ErrorResponse {
guard let (statusCode, errorData) = PassageErrorData.getData(from: errorResponse) else {
return .unspecified
}
switch errorData.code {
case Model400Code.request.rawValue: return .invalidRequest
case Model403Code.userNotActive.rawValue: return .inactiveUser
default: ()
}
if statusCode == 401 {
return .unauthorized
}
return .unspecified
}
// Handle authorization error
if let authError = error as? ASAuthorizationError {
return authError.code == .canceled ? .canceled : .authorizationFailed
}
return .unspecified
}

}
26 changes: 26 additions & 0 deletions Sources/Passage/Errors/AppInfoError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

public enum AppInfoError: PassageError {

case appNotFound
case unspecified

public static func convert(error: Error) -> AppInfoError {
// Check if error is already proper
if let appInfoError = error as? AppInfoError {
return appInfoError
}
// Handle client error
if let errorResponse = error as? ErrorResponse {
guard let (_, errorData) = PassageErrorData.getData(from: errorResponse) else {
return .unspecified
}
if errorData.code == Model404Code.appNotFound.rawValue {
return .appNotFound
}
return .unspecified
}
return .unspecified
}

}
15 changes: 15 additions & 0 deletions Sources/Passage/Errors/ConfigurationError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

public enum PassageConfigurationError: Error {
case cannotFindPassagePlist
case cannotFindAppId

public var description: String {
switch self {
case .cannotFindAppId:
return "Cannot find required Passage.plist file."
case .cannotFindPassagePlist:
return "Cannot find your Passage app id in your Passage.plist file"
}
}
}
29 changes: 29 additions & 0 deletions Sources/Passage/Errors/GetMagicLinkStatusError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

public enum GetMagicLinkStatusError: PassageError {

case invalidRequest
case magicLinkNotFound
case unspecified

public static func convert(error: Error) -> GetMagicLinkStatusError {
// Check if error is already proper
if let error = error as? GetMagicLinkStatusError {
return error
}
// Handle client error
if let errorResponse = error as? ErrorResponse {
guard let (_, errorData) = PassageErrorData.getData(from: errorResponse) else {
return .unspecified
}
switch errorData.code {
case Model400Code.request.rawValue: return .invalidRequest
case Model404Code.magicLinkNotFound.rawValue: return .magicLinkNotFound
default: ()
}
return .unspecified
}
return .unspecified
}

}
Loading

0 comments on commit 857245d

Please sign in to comment.