Skip to content

Commit c993348

Browse files
committed
feat: add ios support
1 parent 8065ea4 commit c993348

9 files changed

+775
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#import <React/RCTBridgeModule.h>
5+
#import <AuthenticationServices/AuthenticationServices.h>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#import "AmplifyRtnPasskeys.h"
5+
#import "AmplifyRtnPasskeys-Swift.h"
6+
7+
@implementation AmplifyRtnPasskeys
8+
9+
- (void)createPasskey:
10+
(JS::NativeAmplifyRtnPasskeys::PasskeyCreateOptionsJson &)input
11+
resolve:(nonnull RCTPromiseResolveBlock)resolve
12+
reject:(nonnull RCTPromiseRejectBlock)reject {
13+
14+
NSMutableArray *excludeCredentials = [@[] mutableCopy];
15+
16+
if (input.excludeCredentials().has_value()) {
17+
auto credentials = input.excludeCredentials().value();
18+
for (const auto &credential : credentials) {
19+
[excludeCredentials addObject:credential.id_()];
20+
}
21+
}
22+
23+
return [[AmplifyRtnPasskeysSwift alloc] createPasskey:input.rp().id_()
24+
userId:input.user().id_()
25+
userName:input.user().name()
26+
challenge:input.challenge()
27+
excludeCredentials:excludeCredentials
28+
resolve:resolve
29+
reject:reject];
30+
}
31+
32+
- (void)getPasskey:(JS::NativeAmplifyRtnPasskeys::PasskeyGetOptionsJson &)input
33+
resolve:(nonnull RCTPromiseResolveBlock)resolve
34+
reject:(nonnull RCTPromiseRejectBlock)reject {
35+
36+
NSMutableArray *allowCredentials = [@[] mutableCopy];
37+
38+
if (input.allowCredentials().has_value()) {
39+
auto credentials = input.allowCredentials().value();
40+
for (const auto &credential : credentials) {
41+
[allowCredentials addObject:credential.id_()];
42+
}
43+
}
44+
45+
return [[AmplifyRtnPasskeysSwift alloc] getPasskey:input.rpId()
46+
challenge:input.challenge()
47+
userVerification:input.userVerification()
48+
allowCredentials:allowCredentials
49+
resolve:resolve
50+
reject:reject];
51+
}
52+
53+
- (nonnull NSNumber *)getIsPasskeySupported {
54+
return [[AmplifyRtnPasskeysSwift alloc] getIsPasskeySupported];
55+
}
56+
57+
+ (NSString *)moduleName {
58+
return @"AmplifyRtnPasskeys";
59+
}
60+
61+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
62+
(const facebook::react::ObjCTurboModule::InitParams &)params {
63+
return std::make_shared<facebook::react::NativeAmplifyRtnPasskeysSpecJSI>(
64+
params);
65+
}
66+
67+
@end
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import AuthenticationServices
5+
import Foundation
6+
7+
@objc(AmplifyRtnPasskeysSwift)
8+
public class AmplifyRtnPasskeys: NSObject, AmplifyRtnPasskeysResultHandler {
9+
private var _passkeyDelegate: AmplifyRtnPasskeysDelegate?
10+
private var _promiseHandler: AmplifyRtnPasskeysPromiseHandler?
11+
12+
@objc
13+
@available(iOS 15.0, *)
14+
public func createPasskey(
15+
_ rpId: String,
16+
userId: String,
17+
userName: String,
18+
challenge: String,
19+
excludeCredentials: [String],
20+
resolve: @escaping RCTPromiseResolveBlock,
21+
reject: @escaping RCTPromiseRejectBlock
22+
) {
23+
24+
_promiseHandler = initializePromiseHandler(resolve, reject)
25+
26+
guard self.getIsPasskeySupported() == true else {
27+
handleError(errorName: "NOT_SUPPORTED", errorMessage: nil, error: nil)
28+
return
29+
}
30+
31+
let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
32+
relyingPartyIdentifier: rpId)
33+
34+
let platformKeyRequest =
35+
platformProvider.createCredentialRegistrationRequest(
36+
challenge: challenge.toBase64UrlDecodedData(),
37+
name: userName,
38+
userID: userId.toBase64UrlDecodedData()
39+
)
40+
41+
if #available(iOS 17.4, *) {
42+
let excludedCredentials:
43+
[ASAuthorizationPlatformPublicKeyCredentialDescriptor] =
44+
excludeCredentials.compactMap { credentialId in
45+
return .init(credentialID: credentialId.toBase64UrlDecodedData())
46+
}
47+
48+
platformKeyRequest.excludedCredentials = excludedCredentials
49+
}
50+
51+
let authController = initializeAuthController(
52+
platformKeyRequest: platformKeyRequest)
53+
54+
let passkeyDelegate = initializePasskeyDelegate(resultHandler: self)
55+
56+
_passkeyDelegate = passkeyDelegate
57+
58+
passkeyDelegate.performAuthForController(authController)
59+
}
60+
61+
@objc
62+
@available(iOS 15.0, *)
63+
public func getPasskey(
64+
_ rpId: String,
65+
challenge: String,
66+
userVerification: String,
67+
allowCredentials: [String],
68+
resolve: @escaping RCTPromiseResolveBlock,
69+
reject: @escaping RCTPromiseRejectBlock
70+
) {
71+
_promiseHandler = initializePromiseHandler(resolve, reject)
72+
73+
guard self.getIsPasskeySupported() == true else {
74+
handleError(errorName: "NOT_SUPPORTED")
75+
return
76+
}
77+
78+
let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
79+
relyingPartyIdentifier: rpId)
80+
81+
let platformKeyRequest = platformProvider.createCredentialAssertionRequest(
82+
challenge: challenge.toBase64UrlDecodedData()
83+
)
84+
85+
let allowedCredentials:
86+
[ASAuthorizationPlatformPublicKeyCredentialDescriptor] =
87+
allowCredentials.compactMap { credentialId in
88+
return .init(credentialID: credentialId.toBase64UrlDecodedData())
89+
}
90+
91+
platformKeyRequest.allowedCredentials = allowedCredentials
92+
93+
platformKeyRequest.userVerificationPreference =
94+
ASAuthorizationPublicKeyCredentialUserVerificationPreference(
95+
userVerification)
96+
97+
let authController = initializeAuthController(
98+
platformKeyRequest: platformKeyRequest)
99+
100+
let passkeyDelegate = initializePasskeyDelegate(resultHandler: self)
101+
102+
_passkeyDelegate = passkeyDelegate
103+
104+
passkeyDelegate.performAuthForController(authController)
105+
}
106+
107+
func handleSuccess(_ data: NSDictionary) {
108+
guard let handler = _promiseHandler else {
109+
return
110+
}
111+
handler.resolve(data)
112+
_promiseHandler = nil
113+
_passkeyDelegate = nil
114+
}
115+
116+
func handleError(
117+
errorName: String, errorMessage: String? = nil, error: (any Error)? = nil
118+
) {
119+
guard let handler = _promiseHandler else {
120+
return
121+
}
122+
handler.reject(errorName, errorMessage, error)
123+
_promiseHandler = nil
124+
_passkeyDelegate = nil
125+
}
126+
127+
func initializePromiseHandler(
128+
_ resolve: @escaping RCTPromiseResolveBlock,
129+
_ reject: @escaping RCTPromiseRejectBlock
130+
) -> AmplifyRtnPasskeysPromiseHandler {
131+
return AmplifyRtnPasskeysPromiseHandler(resolve, reject)
132+
}
133+
134+
func initializePasskeyDelegate(resultHandler: AmplifyRtnPasskeysResultHandler)
135+
-> AmplifyRtnPasskeysDelegate
136+
{
137+
return AmplifyRtnPasskeysDelegate(resultHandler: resultHandler)
138+
}
139+
140+
func initializeAuthController(platformKeyRequest: ASAuthorizationRequest)
141+
-> ASAuthorizationController
142+
{
143+
return ASAuthorizationController(authorizationRequests: [platformKeyRequest]
144+
)
145+
}
146+
147+
@objc
148+
public func getIsPasskeySupported() -> NSNumber {
149+
if #available(iOS 15.0, *) {
150+
return true
151+
}
152+
return false
153+
}
154+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import AuthenticationServices
5+
import Foundation
6+
7+
class AmplifyRtnPasskeysDelegate: NSObject,
8+
ASAuthorizationControllerDelegate,
9+
ASAuthorizationControllerPresentationContextProviding
10+
{
11+
private static let PUBLIC_KEY_TYPE = "public-key"
12+
private static let PLATFORM_ATTACHMENT = "platform"
13+
private static let INTERNAL_TRANSPORT = "internal"
14+
15+
private static let ERROR_MAP: [Int: String] = [
16+
1000: "UNKNOWN",
17+
1001: "CANCELED",
18+
1002: "INVALID_RESPONSE",
19+
1003: "NOT_HANDLED",
20+
1004: "FAILED",
21+
1005: "NOT_INTERACTIVE",
22+
1006: "DUPLICATE",
23+
]
24+
25+
let _resultHandler: AmplifyRtnPasskeysResultHandler
26+
27+
init(resultHandler: AmplifyRtnPasskeysResultHandler) {
28+
_resultHandler = resultHandler
29+
}
30+
31+
func performAuthForController(_ authController: ASAuthorizationController) {
32+
authController.delegate = self
33+
authController.presentationContextProvider = self
34+
authController.performRequests()
35+
}
36+
37+
func authorizationController(
38+
controller: ASAuthorizationController,
39+
didCompleteWithAuthorization authorization: ASAuthorization
40+
) {
41+
42+
switch authorization.credential {
43+
case let assertionCredential
44+
as ASAuthorizationPlatformPublicKeyCredentialAssertion:
45+
46+
let assertionResult: NSDictionary = [
47+
"id": assertionCredential.credentialID.toBase64UrlEncodedString(),
48+
"rawId": assertionCredential.credentialID.toBase64UrlEncodedString(),
49+
"authenticatorAttachment": AmplifyRtnPasskeysDelegate
50+
.PLATFORM_ATTACHMENT,
51+
"type": AmplifyRtnPasskeysDelegate.PUBLIC_KEY_TYPE,
52+
"response": [
53+
"authenticatorData": assertionCredential.rawAuthenticatorData
54+
.toBase64UrlEncodedString(),
55+
"clientDataJSON": assertionCredential.rawClientDataJSON
56+
.toBase64UrlEncodedString(),
57+
"signature": assertionCredential.signature.toBase64UrlEncodedString(),
58+
"userHandle": assertionCredential.userID.toBase64UrlEncodedString(),
59+
],
60+
]
61+
62+
_resultHandler.handleSuccess(assertionResult)
63+
64+
case let registrationCredential
65+
as ASAuthorizationPlatformPublicKeyCredentialRegistration:
66+
let registrationResult: NSDictionary = [
67+
"id": registrationCredential.credentialID.toBase64UrlEncodedString(),
68+
"rawId": registrationCredential.credentialID.toBase64UrlEncodedString(),
69+
"authenticatorAttachment": AmplifyRtnPasskeysDelegate
70+
.PLATFORM_ATTACHMENT,
71+
"type": AmplifyRtnPasskeysDelegate.PUBLIC_KEY_TYPE,
72+
"response": [
73+
"attestationObject": registrationCredential.rawAttestationObject!
74+
.toBase64UrlEncodedString(),
75+
"clientDataJSON": registrationCredential.rawClientDataJSON
76+
.toBase64UrlEncodedString(),
77+
"transports": [AmplifyRtnPasskeysDelegate.INTERNAL_TRANSPORT],
78+
],
79+
]
80+
81+
_resultHandler.handleSuccess(registrationResult)
82+
83+
default:
84+
_resultHandler.handleError(
85+
errorName: "FAILED", errorMessage: nil, error: nil)
86+
}
87+
}
88+
89+
func authorizationController(
90+
controller: ASAuthorizationController,
91+
didCompleteWithError error: any Error
92+
) {
93+
let errorMessage = error.localizedDescription
94+
95+
var errorName =
96+
AmplifyRtnPasskeysDelegate.ERROR_MAP[(error as NSError).code] ?? "UNKNOWN"
97+
98+
// pre-iOS 18 does not through explicit error for duplicate
99+
if errorMessage.contains(
100+
"credential matches an entry of the excludeCredentials list")
101+
{
102+
errorName = "DUPLICATE"
103+
}
104+
105+
// no explicit error with for SecurityError
106+
if errorMessage.contains("not associated with domain") {
107+
errorName = "RELYING_PARTY_MISMATCH"
108+
}
109+
110+
_resultHandler.handleError(
111+
errorName: errorName, errorMessage: errorMessage, error: error)
112+
}
113+
114+
func presentationAnchor(for controller: ASAuthorizationController)
115+
-> ASPresentationAnchor
116+
{
117+
return ASPresentationAnchor()
118+
}
119+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
extension String {
5+
// Converts base64Url encoded string to base64 Data
6+
func toBase64UrlDecodedData() -> Data {
7+
var base64String = self.replacingOccurrences(of: "_", with: "/")
8+
.replacingOccurrences(of: "-", with: "+")
9+
10+
while base64String.count % 4 != 0 {
11+
base64String.append("=")
12+
}
13+
14+
return Data(base64Encoded: base64String) ?? Data()
15+
}
16+
}
17+
18+
extension Data {
19+
// Converts base64 Data to base64url String
20+
func toBase64UrlEncodedString() -> String {
21+
return self.base64EncodedString()
22+
.replacingOccurrences(of: "/", with: "_")
23+
.replacingOccurrences(of: "+", with: "-")
24+
.replacingOccurrences(of: "=", with: "")
25+
}
26+
}
27+
28+
struct AmplifyRtnPasskeysPromiseHandler {
29+
let resolve: RCTPromiseResolveBlock
30+
let reject: RCTPromiseRejectBlock
31+
32+
init(
33+
_ resolve: @escaping RCTPromiseResolveBlock,
34+
_ reject: @escaping RCTPromiseRejectBlock
35+
) {
36+
self.resolve = resolve
37+
self.reject = reject
38+
}
39+
}
40+
41+
protocol AmplifyRtnPasskeysResultHandler {
42+
func handleSuccess(_ data: NSDictionary)
43+
func handleError(
44+
errorName: String, errorMessage: String?, error: (any Error)?)
45+
}

0 commit comments

Comments
 (0)