Skip to content

Extend passkey login function, sync webAuthnlist to keychain and crash fixes #100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 73 additions & 3 deletions GigyaSwift/Global/PersistenceService.swift
Original file line number Diff line number Diff line change
@@ -11,6 +11,15 @@ import Foundation
public final class PersistenceService {
var isStartSdk: Bool = false
var isInitSdk: Bool = false
let config: GigyaConfig?

private var apiKey: String {
return config?.apiKey ?? "zxyt"
}

init(config: GigyaConfig?) {
self.config = config
}

// MARK: - UserDefault

@@ -64,7 +73,7 @@ public final class PersistenceService {

public var webAuthnlist: [GigyaWebAuthnCredential] {
get {
if let data = UserDefaults.standard.object(forKey: InternalConfig.Storage.webAuthn) as? Data {
if let data = GigyaKeyChainService.read(key: InternalConfig.Storage.webAuthnKey(apiKey: apiKey)) {
do {
return try PropertyListDecoder().decode([GigyaWebAuthnCredential].self, from: data)
} catch {
@@ -102,10 +111,71 @@ public final class PersistenceService {
func addWebAuthnKey(model: GigyaWebAuthnCredential) {
var list = webAuthnlist
list.append(model)
UserDefaults.standard.set(try? PropertyListEncoder().encode(list), forKey: InternalConfig.Storage.webAuthn)
if let data = try? PropertyListEncoder().encode(list) {
GigyaKeyChainService.save(key: InternalConfig.Storage.webAuthnKey(apiKey: apiKey), valueData: data)
}
}

internal func removeAllWebAuthnKeys() {
UserDefaults.standard.removeObject(forKey: InternalConfig.Storage.webAuthn)
GigyaKeyChainService.delete(key: InternalConfig.Storage.webAuthnKey(apiKey: apiKey))
}
}

class GigyaKeyChainService {
static func save(key: String, valueData: Data) {
var query = getQuery()
query[String(kSecAttrAccount)] = key
var attributes = getQuery()
attributes[String(kSecAttrAccount)] = key
attributes[String(kSecValueData)] = valueData
attributes[String(kSecAttrAccessible)] = kSecAttrAccessibleAfterFirstUnlock
let status = SecItemCopyMatching(query as CFDictionary, nil)
if status == noErr {
let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
if updateStatus != noErr {
SecItemDelete(query as CFDictionary)
let addStatus = SecItemAdd(attributes as CFDictionary, nil)
if addStatus != noErr {
GigyaLogger.log(with: self, message: "Failed to store value for key = '\(addStatus)'")
}
}
} else {
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(attributes as CFDictionary, nil)
if status != noErr {
GigyaLogger.log(with: self, message: "Failed to add value for key '\(key)'")
}
}
}

static func read(key: String) -> Data? {
var query = getQuery()
query[String(kSecMatchLimit)] = kSecMatchLimitOne
query[String(kSecReturnData)] = kCFBooleanTrue
query[String(kSecAttrAccount)] = key
var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == noErr, let data = dataTypeRef as? Data {
return data
} else {
return nil
}
}

static func delete(key: String) {
var query = getQuery()
query[String(kSecAttrAccount)] = key
query[String(kSecAttrSynchronizable)] = kSecAttrSynchronizableAny
SecItemDelete(query as CFDictionary)
}

private class func getQuery() -> [String: Any] {
let bundleId = Bundle.main.bundleIdentifier ?? ""
var query: [String : Any] = [
String(kSecClass): String(kSecClassGenericPassword),
String(kSecAttrService): bundleId
]
query[String(kSecAttrSynchronizable)] = kCFBooleanTrue
return query
}
}
5 changes: 3 additions & 2 deletions GigyaSwift/Global/Utils/GigyaIOCContainer.swift
Original file line number Diff line number Diff line change
@@ -161,8 +161,9 @@ final class GigyaIOCContainer<T: GigyaAccountProtocol>: GigyaContainerProtocol {
return AccountService()
}

container.register(service: PersistenceService.self, isSingleton: true) { _ in
return PersistenceService()
container.register(service: PersistenceService.self, isSingleton: true) { resolver in
let config = resolver.resolve(GigyaConfig.self)
return PersistenceService(config: config)
}

container.register(service: InterruptionResolverFactoryProtocol.self) { _ in
33 changes: 31 additions & 2 deletions GigyaSwift/Global/WebAuthn/WebAuthnDeviceIntegration.swift
Original file line number Diff line number Diff line change
@@ -113,8 +113,37 @@ class WebAuthnDeviceIntegration: NSObject {
authController.presentationContextProvider = self
authController.performRequests()
}



@available(iOS 16.0, *)
func loginWithAvailableCredentials(viewController: UIViewController, options: WebAuthnGetOptionsResponseModel, allowedKeys: [GigyaWebAuthnCredential], handler: @escaping WebAuthnIntegrationHandler) {
self.vc = viewController
self.handler = handler

let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: options.options.rpId)

let challenge = options.options.challenge.decodeBase64Url()!

let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(challenge: challenge)

let publicKeys = allowedKeys.filter { $0.type == .platform }
.map {
ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: Data(base64Encoded: $0.key) ?? Data())
}

assertionRequest.allowedCredentials = publicKeys

if let userVerification = options.options.userVerification {
assertionRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.init(rawValue: userVerification)
}

let authorizationRequests: [ASAuthorizationRequest] = [assertionRequest]

let authController = ASAuthorizationController(authorizationRequests: authorizationRequests )
authController.delegate = self
authController.presentationContextProvider = self
authController.performRequests(options: .preferImmediatelyAvailableCredentials)
}

deinit {
GigyaLogger.log(with: self, message: "deinit")
}
75 changes: 74 additions & 1 deletion GigyaSwift/Global/WebAuthn/WebAuthnService.swift
Original file line number Diff line number Diff line change
@@ -206,7 +206,79 @@ public class WebAuthnService<T: GigyaAccountProtocol> {
return .failure(LoginApiError(error: error))
}
}


@available(iOS 16.0.0, *)
public func loginWithAvailableCredentials(viewController: UIViewController, params: [String: Any] = [:]) async -> GigyaLoginResult<T> {
if isActiveContinuation {
return .failure(.init(error: .providerError(data: "cancelled")))
}

let allowedKeys = persistenceService.webAuthnlist
if allowedKeys.isEmpty {
return .failure(.init(error: .providerError(data: "cancelled")))
}

isActiveContinuation.toggle()
oauthService.params = params

let assertionOptions = await getAssertionOptions()

switch assertionOptions {
case .success(let options):
return await withCheckedContinuation() { continuation in
webAuthnDeviceIntegration.loginWithAvailableCredentials(viewController: viewController, options: options, allowedKeys: allowedKeys) { [weak self] result in
guard let self = self else {
return
}
switch result {
case .login(let token):
let attestation: [String: Any] = self.attestationUtils.makeLoginData(object: token)

Task {
let result = await self.verifyAssertion(params: ["authenticatorAssertion": attestation, "token": options.token])
switch result {
case .success(data: let data):
let user: GigyaLoginResult<T> = await self.oauthService.authorize(token: data["idToken"]!.value as! String) // idToken for login
continuation.resume(returning: user)
self.isActiveContinuation.toggle()
case .failure(let error):
continuation.resume(returning: .failure(LoginApiError(error: error)))
self.isActiveContinuation.toggle()
}
}
case .securityLogin(let token):
let attestation: [String: Any] = self.attestationUtils.makeSecurityLoginData(object: token)

Task {
let result = await self.verifyAssertion(params: ["authenticatorAssertion": attestation, "token": options.token])
switch result {
case .success(data: let data):
let user: GigyaLoginResult<T> = await self.oauthService.authorize(token: data["idToken"]!.value as! String) // idToken for login
continuation.resume(returning: user)
self.isActiveContinuation.toggle()
case .failure(let error):
continuation.resume(returning: .failure(LoginApiError(error: error)))
self.isActiveContinuation.toggle()
}
}
case .canceled:
continuation
.resume(returning: .failure(LoginApiError(error: NetworkError.providerError(data: "cancelled"))))
self.isActiveContinuation.toggle()
default:
let error = GigyaResponseModel(statusCode: .unknown, errorCode: 400301, callId: "", errorMessage: "Operation failed", sessionInfo: nil)
continuation
.resume(returning: .failure(LoginApiError(error: NetworkError.gigyaError(data: error))))
self.isActiveContinuation.toggle()
}
}
}
case .failure(let error):
self.isActiveContinuation.toggle()
return .failure(LoginApiError(error: error))
}
}

@available(iOS 16.0.0, *)
private func getAssertionOptions() async -> GigyaApiResult<WebAuthnGetOptionsResponseModel> {
return await withCheckedContinuation({
@@ -284,6 +356,7 @@ public class WebAuthnService<T: GigyaAccountProtocol> {
self.persistenceService.removeAllWebAuthnKeys()
case .failure(_):
continuation.resume(returning: false)
return
}
}

5 changes: 4 additions & 1 deletion GigyaSwift/Models/Config/InternalConfig.swift
Original file line number Diff line number Diff line change
@@ -31,7 +31,10 @@ struct InternalConfig {
internal static let hasRunBefore = "com.gigya.GigyaSDK:hasRunBefore"
internal static let expirationSession = "com.gigya.GigyaSDK:expirationSession"
internal static let pushKey = "com.gigya.GigyaTfa:pushKey"
internal static let webAuthn = "com.gigya.GigyaSDK:webauthn"
internal static func webAuthnKey(apiKey: String) -> String {
let suffix = String(apiKey.suffix(4))
return "com.gigya.GigyaSDK:webauthn:\(suffix)"
}
}

struct Network {