Skip to content

feat(rtn-passkeys): add ios support - 3a #14392

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 1 commit into
base: feat/rtn-passkeys/2-modify-existing-impl
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

#import <React/RCTBridgeModule.h>
#import <AuthenticationServices/AuthenticationServices.h>
67 changes: 67 additions & 0 deletions packages/rtn-passkeys/ios/AmplifyRtnPasskeys.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

#import "AmplifyRtnPasskeys.h"
#import "AmplifyRtnPasskeys-Swift.h"

@implementation AmplifyRtnPasskeys

- (void)createPasskey:
(JS::NativeAmplifyRtnPasskeys::PasskeyCreateOptionsJson &)input
resolve:(nonnull RCTPromiseResolveBlock)resolve
reject:(nonnull RCTPromiseRejectBlock)reject {

NSMutableArray *excludeCredentials = [@[] mutableCopy];

if (input.excludeCredentials().has_value()) {
auto credentials = input.excludeCredentials().value();
for (const auto &credential : credentials) {
[excludeCredentials addObject:credential.id_()];
}
}

return [[AmplifyRtnPasskeysSwift alloc] createPasskey:input.rp().id_()
userId:input.user().id_()
userName:input.user().name()
challenge:input.challenge()
excludeCredentials:excludeCredentials
resolve:resolve
reject:reject];
}

- (void)getPasskey:(JS::NativeAmplifyRtnPasskeys::PasskeyGetOptionsJson &)input
resolve:(nonnull RCTPromiseResolveBlock)resolve
reject:(nonnull RCTPromiseRejectBlock)reject {

NSMutableArray *allowCredentials = [@[] mutableCopy];

if (input.allowCredentials().has_value()) {
auto credentials = input.allowCredentials().value();
for (const auto &credential : credentials) {
[allowCredentials addObject:credential.id_()];
}
}

return [[AmplifyRtnPasskeysSwift alloc] getPasskey:input.rpId()
challenge:input.challenge()
userVerification:input.userVerification()
allowCredentials:allowCredentials
resolve:resolve
reject:reject];
}

- (nonnull NSNumber *)getIsPasskeySupported {
return [[AmplifyRtnPasskeysSwift alloc] getIsPasskeySupported];
}

+ (NSString *)moduleName {
return @"AmplifyRtnPasskeys";
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeAmplifyRtnPasskeysSpecJSI>(
params);
}

@end
154 changes: 154 additions & 0 deletions packages/rtn-passkeys/ios/AmplifyRtnPasskeys.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import AuthenticationServices
import Foundation

@objc(AmplifyRtnPasskeysSwift)
public class AmplifyRtnPasskeys: NSObject, AmplifyRtnPasskeysResultHandler {
private var _passkeyDelegate: AmplifyRtnPasskeysDelegate?
private var _promiseHandler: AmplifyRtnPasskeysPromiseHandler?

@objc
@available(iOS 15.0, *)
public func createPasskey(
_ rpId: String,
userId: String,
userName: String,
challenge: String,
excludeCredentials: [String],
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {

_promiseHandler = initializePromiseHandler(resolve, reject)

guard self.getIsPasskeySupported() == true else {
handleError(errorName: "NOT_SUPPORTED", errorMessage: nil, error: nil)
return
}

let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: rpId)

let platformKeyRequest =
platformProvider.createCredentialRegistrationRequest(
challenge: challenge.toBase64UrlDecodedData(),
name: userName,
userID: userId.toBase64UrlDecodedData()
)

if #available(iOS 17.4, *) {
let excludedCredentials:
[ASAuthorizationPlatformPublicKeyCredentialDescriptor] =
excludeCredentials.compactMap { credentialId in
return .init(credentialID: credentialId.toBase64UrlDecodedData())
}

platformKeyRequest.excludedCredentials = excludedCredentials
}

let authController = initializeAuthController(
platformKeyRequest: platformKeyRequest)

let passkeyDelegate = initializePasskeyDelegate(resultHandler: self)

_passkeyDelegate = passkeyDelegate

passkeyDelegate.performAuthForController(authController)
}

@objc
@available(iOS 15.0, *)
public func getPasskey(
_ rpId: String,
challenge: String,
userVerification: String,
allowCredentials: [String],
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
_promiseHandler = initializePromiseHandler(resolve, reject)

guard self.getIsPasskeySupported() == true else {
handleError(errorName: "NOT_SUPPORTED")
return
}

let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: rpId)

let platformKeyRequest = platformProvider.createCredentialAssertionRequest(
challenge: challenge.toBase64UrlDecodedData()
)

let allowedCredentials:
[ASAuthorizationPlatformPublicKeyCredentialDescriptor] =
allowCredentials.compactMap { credentialId in
return .init(credentialID: credentialId.toBase64UrlDecodedData())
}

platformKeyRequest.allowedCredentials = allowedCredentials

platformKeyRequest.userVerificationPreference =
ASAuthorizationPublicKeyCredentialUserVerificationPreference(
userVerification)

let authController = initializeAuthController(
platformKeyRequest: platformKeyRequest)

let passkeyDelegate = initializePasskeyDelegate(resultHandler: self)

_passkeyDelegate = passkeyDelegate

passkeyDelegate.performAuthForController(authController)
}

func handleSuccess(_ data: NSDictionary) {
guard let handler = _promiseHandler else {
return
}
handler.resolve(data)
_promiseHandler = nil
_passkeyDelegate = nil
}

func handleError(
errorName: String, errorMessage: String? = nil, error: (any Error)? = nil
) {
guard let handler = _promiseHandler else {
return
}
handler.reject(errorName, errorMessage, error)
_promiseHandler = nil
_passkeyDelegate = nil
}

func initializePromiseHandler(
_ resolve: @escaping RCTPromiseResolveBlock,
_ reject: @escaping RCTPromiseRejectBlock
) -> AmplifyRtnPasskeysPromiseHandler {
return AmplifyRtnPasskeysPromiseHandler(resolve, reject)
}

func initializePasskeyDelegate(resultHandler: AmplifyRtnPasskeysResultHandler)
-> AmplifyRtnPasskeysDelegate
{
return AmplifyRtnPasskeysDelegate(resultHandler: resultHandler)
}

func initializeAuthController(platformKeyRequest: ASAuthorizationRequest)
-> ASAuthorizationController
{
return ASAuthorizationController(authorizationRequests: [platformKeyRequest]
)
}

@objc
public func getIsPasskeySupported() -> NSNumber {
if #available(iOS 17.4, *) {
return true
}
return false
}
}
119 changes: 119 additions & 0 deletions packages/rtn-passkeys/ios/AmplifyRtnPasskeysDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import AuthenticationServices
import Foundation

class AmplifyRtnPasskeysDelegate: NSObject,
ASAuthorizationControllerDelegate,
ASAuthorizationControllerPresentationContextProviding
{
private static let PUBLIC_KEY_TYPE = "public-key"
private static let PLATFORM_ATTACHMENT = "platform"
private static let INTERNAL_TRANSPORT = "internal"

private static let ERROR_MAP: [Int: String] = [
1000: "UNKNOWN",
1001: "CANCELED",
1002: "INVALID_RESPONSE",
1003: "NOT_HANDLED",
1004: "FAILED",
1005: "NOT_INTERACTIVE",
1006: "DUPLICATE",
]

let _resultHandler: AmplifyRtnPasskeysResultHandler

init(resultHandler: AmplifyRtnPasskeysResultHandler) {
_resultHandler = resultHandler
}

func performAuthForController(_ authController: ASAuthorizationController) {
authController.delegate = self
authController.presentationContextProvider = self
authController.performRequests()
}

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

switch authorization.credential {
case let assertionCredential
Comment on lines +42 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: indentation?

as ASAuthorizationPlatformPublicKeyCredentialAssertion:

let assertionResult: NSDictionary = [
"id": assertionCredential.credentialID.toBase64UrlEncodedString(),
"rawId": assertionCredential.credentialID.toBase64UrlEncodedString(),
"authenticatorAttachment": AmplifyRtnPasskeysDelegate
.PLATFORM_ATTACHMENT,
"type": AmplifyRtnPasskeysDelegate.PUBLIC_KEY_TYPE,
"response": [
"authenticatorData": assertionCredential.rawAuthenticatorData
.toBase64UrlEncodedString(),
"clientDataJSON": assertionCredential.rawClientDataJSON
.toBase64UrlEncodedString(),
"signature": assertionCredential.signature.toBase64UrlEncodedString(),
"userHandle": assertionCredential.userID.toBase64UrlEncodedString(),
],
]

_resultHandler.handleSuccess(assertionResult)

case let registrationCredential
as ASAuthorizationPlatformPublicKeyCredentialRegistration:
let registrationResult: NSDictionary = [
"id": registrationCredential.credentialID.toBase64UrlEncodedString(),
"rawId": registrationCredential.credentialID.toBase64UrlEncodedString(),
"authenticatorAttachment": AmplifyRtnPasskeysDelegate
.PLATFORM_ATTACHMENT,
"type": AmplifyRtnPasskeysDelegate.PUBLIC_KEY_TYPE,
"response": [
"attestationObject": registrationCredential.rawAttestationObject!
.toBase64UrlEncodedString(),
"clientDataJSON": registrationCredential.rawClientDataJSON
.toBase64UrlEncodedString(),
"transports": [AmplifyRtnPasskeysDelegate.INTERNAL_TRANSPORT],
],
]

_resultHandler.handleSuccess(registrationResult)

default:
_resultHandler.handleError(
errorName: "FAILED", errorMessage: nil, error: nil)
}
}

func authorizationController(
controller: ASAuthorizationController,
didCompleteWithError error: any Error
) {
let errorMessage = error.localizedDescription

var errorName =
AmplifyRtnPasskeysDelegate.ERROR_MAP[(error as NSError).code] ?? "UNKNOWN"

// pre-iOS 18 does not through explicit error for duplicate
if errorMessage.contains(
"credential matches an entry of the excludeCredentials list")
{
errorName = "DUPLICATE"
}

// no explicit error with for SecurityError
if errorMessage.contains("not associated with domain") {
errorName = "RELYING_PARTY_MISMATCH"
}

_resultHandler.handleError(
errorName: errorName, errorMessage: errorMessage, error: error)
}

func presentationAnchor(for controller: ASAuthorizationController)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: what's this function for?

-> ASPresentationAnchor
{
return ASPresentationAnchor()
}
}
45 changes: 45 additions & 0 deletions packages/rtn-passkeys/ios/AmplifyRtnPasskeysHelpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

extension String {
// Converts base64Url encoded string to base64 Data
func toBase64UrlDecodedData() -> Data {
var base64String = self.replacingOccurrences(of: "_", with: "/")
.replacingOccurrences(of: "-", with: "+")

while base64String.count % 4 != 0 {
base64String.append("=")
}

return Data(base64Encoded: base64String) ?? Data()
}
}

extension Data {
// Converts base64 Data to base64url String
func toBase64UrlEncodedString() -> String {
return self.base64EncodedString()
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "=", with: "")
}
}

struct AmplifyRtnPasskeysPromiseHandler {
let resolve: RCTPromiseResolveBlock
let reject: RCTPromiseRejectBlock

init(
_ resolve: @escaping RCTPromiseResolveBlock,
_ reject: @escaping RCTPromiseRejectBlock
) {
self.resolve = resolve
self.reject = reject
}
}

protocol AmplifyRtnPasskeysResultHandler {
func handleSuccess(_ data: NSDictionary)
func handleError(
errorName: String, errorMessage: String?, error: (any Error)?)
}
Loading
Loading