Skip to content
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

[DO NOT MERGE] SDKS-2474_WebAuthn_Keys_With_Passkeys #242

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
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
215 changes: 131 additions & 84 deletions FRAuth/FRAuth/Callbacks/WebAuthnAuthenticationCallback.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,30 @@ open class WebAuthnAuthenticationCallback: WebAuthnCallback {
/// - onError: Error callback to notify any error thrown while generating WebAuthn assertion
public func authenticate(node: Node? = nil, window: UIWindow? = UIApplication.shared.windows.first, preferImmediatelyAvailableCredentials: Bool = false, usePasskeysIfAvailable: Bool = false, onSuccess: @escaping StringCompletionCallback, onError: @escaping ErrorCallback) {
if #available(iOS 16.0, *), usePasskeysIfAvailable {
// Platform Authenticator
let platformAuthenticator = PlatformAuthenticator(authenticationDelegate: self)
let credSources = self.getCredSources(platformAuthenticator: platformAuthenticator)

if credSources!.isEmpty == false {
FRLog.i("Existing WebAuthn keys in storage, ", subModule: WebAuthn.module)
self.delegate?.selectCredential(keyNames: Array(credSources!.keys).sorted(), selectionCallback: { (keyName) in
if keyName != nil {
FRLog.v("Selected credential source received, proceeding with getAssertion operation", subModule: WebAuthn.module)
self.authenticateUsingFRWebAuthn(node: node, onSuccess: onSuccess, onError: onError)
} else {
self.authenticateUsingPasskeys(node: node, window: window, preferImmediatelyAvailableCredentials: preferImmediatelyAvailableCredentials, onSuccess: onSuccess, onError: onError)
}
})
} else {
self.authenticateUsingPasskeys(node: node, window: window, preferImmediatelyAvailableCredentials: preferImmediatelyAvailableCredentials, onSuccess: onSuccess, onError: onError)
}
} else {
self.authenticateUsingFRWebAuthn(node: node, onSuccess: onSuccess, onError: onError)
}
}

private func authenticateUsingPasskeys(node: Node? = nil, window: UIWindow? = UIApplication.shared.windows.first, preferImmediatelyAvailableCredentials: Bool = false, onSuccess: @escaping StringCompletionCallback, onError: @escaping ErrorCallback) {
if #available(iOS 16.0, *) {
FRLog.i("Performing WebAuthn authentication using FRWebAuthnManager and Passkeys", subModule: WebAuthn.module)
self.successCallback = onSuccess
self.errorCallback = onError
Expand All @@ -194,103 +218,126 @@ open class WebAuthnAuthenticationCallback: WebAuthnCallback {
webAuthnManager.delegate = self

webAuthnManager.signInWith(preferImmediatelyAvailableCredentials: preferImmediatelyAvailableCredentials, challenge: data, allowedCredentialsArray: self.allowCredentials, userVerificationPreference: self.convertUserVerification())
} else {
if self.isNewJSONFormat {
FRLog.i("Performing WebAuthn authentication for AM 7.1.0 or above", subModule: WebAuthn.module)
}
else {
FRLog.i("Performing WebAuthn authentication for AM 7.0.0 or below", subModule: WebAuthn.module)
}
}
}

private func authenticateUsingFRWebAuthn(node: Node? = nil, onSuccess: @escaping StringCompletionCallback, onError: @escaping ErrorCallback) {
if self.isNewJSONFormat {
FRLog.i("Performing WebAuthn authentication for AM 7.1.0 or above", subModule: WebAuthn.module)
}
else {
FRLog.i("Performing WebAuthn authentication for AM 7.0.0 or below", subModule: WebAuthn.module)
}

guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
FRLog.e("Bundle Identifier is missing")
onError(FRWAKError.unknown(platformError: nil, message: "Bundle Identifier is missing"))
return
}

// Platform Authenticator
let platformAuthenticator = PlatformAuthenticator(authenticationDelegate: self)
var options = self.setUpAuthenticator(platformAuthenticator: platformAuthenticator, bundleIdentifier: bundleIdentifier)

// Allowed credentials
for allowCred in allowCredentials {
options.addAllowCredential(credentialId: allowCred, transports: [.internal_])
}

self.webAuthnClient?.get(options, onSuccess: { (assertion) in

guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
FRLog.e("Bundle Identifier is missing")
onError(FRWAKError.unknown(platformError: nil, message: "Bundle Identifier is missing"))
return
}
var result = "\(assertion.response.clientDataJSON)"

// Platform Authenticator
let platformAuthenticator = PlatformAuthenticator(authenticationDelegate: self)
// For AM 7.0.0, origin only supports https scheme; to be updated for AM 7.1.0
var origin = CBConstants.originScheme + bundleIdentifier
// For AM 7.1.0 or above, origin should follow origin format according to FIDO AppId and Facet specification
if self.isNewJSONFormat {
origin = CBConstants.originPrefix + bundleIdentifier
let authInt8Arr = assertion.response.authenticatorData.map { Int8(bitPattern: $0) }
let sigInt8Arr = assertion.response.signature.map { Int8(bitPattern: $0) }

let authenticatorData = self.convertInt8ArrToStr(authInt8Arr)
result = result + "::\(authenticatorData)"
let signatureData = self.convertInt8ArrToStr(sigInt8Arr)
result = result + "::\(signatureData)"
result = result + "::\(assertion.id)"
if let userHandle = assertion.response.userHandle {
let encoded = Base64.encodeBase64(userHandle)
if let decoded = encoded.base64Decoded() {
result = result + "::\(decoded)"
}
}

let webAuthnClient = WebAuthnClient(origin: origin, authenticator: platformAuthenticator)
self.webAuthnClient = webAuthnClient
// Expected AM result for successful assertion
// {clientDataJSON as String}::{Int8 array of authenticatorData}::{Int8 array of signature}::{assertion identifier}::{user handle}

// PublicKey credential request options
var options = PublicKeyCredentialRequestOptions()
// If Node is given, set WebAuthn outcome to designated HiddenValueCallback
if let node = node {
FRLog.i("Found optional 'Node' instance, setting WebAuthn outcome to designated HiddenValueCallback", subModule: WebAuthn.module)
self.setWebAuthnOutcome(node: node, outcome: result)
}

// Default userVerification option set to preferred
options.userVerification = self.userVerification.convert()
if #available(iOS 16.0, *) {
FRLog.i("Local keypair exists and user authenticated locally succesfully. The device and FR SDK now supports Passkeys, in order to use it enable the functionality using `usePasskeysIfAvailable=true` and register a new keyPair.", subModule: WebAuthn.module)
self.delegate?.localKeyExistsAndPasskeysAreAvailable()
}

// Challenge
options.challenge = Bytes.fromString(self.challenge.urlSafeEncoding())
// Relying Party
options.rpId = self.relyingPartyId
// Timeout
options.timeout = UInt64(self.timeout/1000)
onSuccess(result)

// Allowed credentials
for allowCred in allowCredentials {
options.addAllowCredential(credentialId: allowCred, transports: [.internal_])
}
}) { (error) in

webAuthnClient.get(options, onSuccess: { (assertion) in

var result = "\(assertion.response.clientDataJSON)"

let authInt8Arr = assertion.response.authenticatorData.map { Int8(bitPattern: $0) }
let sigInt8Arr = assertion.response.signature.map { Int8(bitPattern: $0) }

let authenticatorData = self.convertInt8ArrToStr(authInt8Arr)
result = result + "::\(authenticatorData)"
let signatureData = self.convertInt8ArrToStr(sigInt8Arr)
result = result + "::\(signatureData)"
result = result + "::\(assertion.id)"
if let userHandle = assertion.response.userHandle {
let encoded = Base64.encodeBase64(userHandle)
if let decoded = encoded.base64Decoded() {
result = result + "::\(decoded)"
}
}

// Expected AM result for successful assertion
// {clientDataJSON as String}::{Int8 array of authenticatorData}::{Int8 array of signature}::{assertion identifier}::{user handle}

// If Node is given, set WebAuthn outcome to designated HiddenValueCallback
/// Converts internal WAKError into WebAuthnError
if let webAuthnError = error as? FRWAKError {
// Converts the error to public facing error
let publicError = webAuthnError.convert()
if let node = node {
FRLog.i("Found optional 'Node' instance, setting WebAuthn outcome to designated HiddenValueCallback", subModule: WebAuthn.module)
self.setWebAuthnOutcome(node: node, outcome: result)
}

if #available(iOS 16.0, *) {
FRLog.i("Local keypair exists and user authenticated locally succesfully. The device and FR SDK now supports Passkeys, in order to use it enable the functionality using `usePasskeysIfAvailable=true` and register a new keyPair.", subModule: WebAuthn.module)
self.delegate?.localKeyExistsAndPasskeysAreAvailable()
}

onSuccess(result)

}) { (error) in

/// Converts internal WAKError into WebAuthnError
if let webAuthnError = error as? FRWAKError {
// Converts the error to public facing error
let publicError = webAuthnError.convert()
if let node = node {
FRLog.i("Found optional 'Node' instance, setting WebAuthn error outcome to designated HiddenValueCallback", subModule: WebAuthn.module)
// Converts WebAuthnError to proper WebAuthn error outcome that can be understood by AM
self.setWebAuthnOutcome(node: node, outcome: publicError.convertToWebAuthnOutcome())
}
onError(publicError)
}
else {
onError(error)
FRLog.i("Found optional 'Node' instance, setting WebAuthn error outcome to designated HiddenValueCallback", subModule: WebAuthn.module)
// Converts WebAuthnError to proper WebAuthn error outcome that can be understood by AM
self.setWebAuthnOutcome(node: node, outcome: publicError.convertToWebAuthnOutcome())
}
onError(publicError)
}
else {
onError(error)
}
}
}

private func setUpAuthenticator(platformAuthenticator: PlatformAuthenticator, bundleIdentifier: String) -> PublicKeyCredentialRequestOptions {
// For AM 7.0.0, origin only supports https scheme; to be updated for AM 7.1.0
var origin = CBConstants.originScheme + bundleIdentifier
// For AM 7.1.0 or above, origin should follow origin format according to FIDO AppId and Facet specification
if self.isNewJSONFormat {
origin = CBConstants.originPrefix + bundleIdentifier
}

let webAuthnClient = WebAuthnClient(origin: origin, authenticator: platformAuthenticator)
self.webAuthnClient = webAuthnClient

// PublicKey credential request options
var options = PublicKeyCredentialRequestOptions()

// Default userVerification option set to preferred
options.userVerification = self.userVerification.convert()

// Challenge
options.challenge = Bytes.fromString(self.challenge.urlSafeEncoding())
// Relying Party
options.rpId = self.relyingPartyId
// Timeout
options.timeout = UInt64(self.timeout/1000)
return options
}

private func getCredSources(platformAuthenticator: PlatformAuthenticator) -> [String : PublicKeyCredentialSource]? {
guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
FRLog.e("Bundle Identifier is missing")
return nil
}

var options = self.setUpAuthenticator(platformAuthenticator: platformAuthenticator, bundleIdentifier: bundleIdentifier)
// Allowed credentials
for allowCred in allowCredentials {
options.addAllowCredential(credentialId: allowCred, transports: [.internal_])
}
let session = platformAuthenticator.newGetAssertionSession() as! PlatformAuthenticatorGetAssertionSession
let credSources = session.getCredentialsSources(rpId: self.relyingPartyId, allowCredentialDescriptorList: options.allowCredentials)
return credSources
}
}

Expand Down
1 change: 1 addition & 0 deletions FRAuth/FRAuth/WebAuthn/Authenticator/Authenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ protocol AuthenticatorGetAssertionSession {
// extensions: []
)

func selectCredentialsFromSources(sources: [String: PublicKeyCredentialSource], callback: @escaping WebAuthnCredentialsSelectionCallback)
func canPerformUserVerification() -> Bool

func start()
Expand Down
Loading