From 1ea44e963cde7efc335c935fff52b09e40424f29 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 4 Jul 2024 20:20:40 +0200 Subject: [PATCH 01/19] Add credential manager dependency --- android/build.gradle | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 07ab927..8e9367b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -46,9 +46,10 @@ def safeExtGet(prop, fallback) { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.21" - implementation 'se.curity.identityserver:identityserver.haapi.android.sdk:4.0.0' - implementation 'com.google.code.gson:gson:2.10' + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22" + implementation "se.curity.identityserver:identityserver.haapi.android.sdk:4.2.0" + implementation "com.google.code.gson:gson:2.10" implementation "com.facebook.react:react-android:${safeExtGet('reactNativeVersion', '+')}" + implementation "androidx.credentials:credentials:1.2.2" } - + \ No newline at end of file From 1766b2692a914ad6e3381fed0987c402f14a48df Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 4 Jul 2024 20:21:19 +0200 Subject: [PATCH 02/19] Webauthn support --- .../haapi/react/HaapiInteractionHandler.kt | 202 ++++++++++++++ .../java/io/curity/haapi/react/HaapiModule.kt | 254 ++++++++---------- .../java/io/curity/haapi/react/JsonUtil.kt | 41 +++ .../curity/haapi/react/events/EventEmitter.kt | 34 +++ .../io/curity/haapi/react/events/EventType.kt | 41 +++ .../java/io/curity/haapi/react/exceptions.kt | 3 +- .../haapi/react/webauthn/WebAuthnHandler.kt | 145 ++++++++++ .../curity/haapi/react/webauthn/parameters.kt | 65 +++++ .../haapi/react/webauthn/webauthnRequests.kt | 105 ++++++++ 9 files changed, 746 insertions(+), 144 deletions(-) create mode 100644 android/src/main/java/io/curity/haapi/react/HaapiInteractionHandler.kt create mode 100644 android/src/main/java/io/curity/haapi/react/JsonUtil.kt create mode 100644 android/src/main/java/io/curity/haapi/react/events/EventEmitter.kt create mode 100644 android/src/main/java/io/curity/haapi/react/events/EventType.kt create mode 100644 android/src/main/java/io/curity/haapi/react/webauthn/WebAuthnHandler.kt create mode 100644 android/src/main/java/io/curity/haapi/react/webauthn/parameters.kt create mode 100644 android/src/main/java/io/curity/haapi/react/webauthn/webauthnRequests.kt diff --git a/android/src/main/java/io/curity/haapi/react/HaapiInteractionHandler.kt b/android/src/main/java/io/curity/haapi/react/HaapiInteractionHandler.kt new file mode 100644 index 0000000..d58d962 --- /dev/null +++ b/android/src/main/java/io/curity/haapi/react/HaapiInteractionHandler.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2024 Curity AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.curity.haapi.react + +import android.util.Log +import com.facebook.react.bridge.ReactApplicationContext +import io.curity.haapi.react.events.EventEmitter +import io.curity.haapi.react.events.EventType +import io.curity.haapi.react.events.EventType.HaapiLoading +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import se.curity.identityserver.haapi.android.sdk.HaapiManager +import se.curity.identityserver.haapi.android.sdk.OAuthTokenManager +import se.curity.identityserver.haapi.android.sdk.models.HaapiResponse +import se.curity.identityserver.haapi.android.sdk.models.Link +import se.curity.identityserver.haapi.android.sdk.models.OAuthAuthorizationResponseStep +import se.curity.identityserver.haapi.android.sdk.models.actions.FormActionModel +import se.curity.identityserver.haapi.android.sdk.models.oauth.TokenResponse +import kotlin.coroutines.CoroutineContext + +class HaapiInteractionHandler(private val _reactContext: ReactApplicationContext) { + + companion object { + private var _accessorRepository: HaapiAccessorRepository? = null + } + + private val _haapiScope = CoroutineScope(Dispatchers.IO) + private val _eventEmitter = EventEmitter(_reactContext) + + + /** + * Load the configuration. This needs to be called before any interaction with HAAPI + * If called multiple times, the repo is closed to be able to load new configuration. + * + * @throws FailedToInitalizeHaapiException when the the configuration fails to load + */ + @Throws(FailedToInitalizeHaapiException::class) + fun loadConfiguration(conf: Map) { + // In case the app was recycled in dev mode, close the repo so we may set it up again + if (_accessorRepository != null) { + _accessorRepository!!.close() + } + + try { + _accessorRepository = HaapiAccessorRepository(conf, _reactContext) + } catch (e: Exception) { + Log.w(TAG, "Failed to load configuration ${e.message}") + throw FailedToInitalizeHaapiException("Failed to load configuration", e) + } + } + + /** + * Starts the flow by performing an authorization request to the configured server. + * This will perform attestation and obtain an API access token + * + * @throws FailedHaapiRequestException if the request fails + */ + @Throws(FailedHaapiRequestException::class) + fun startAuthentication(onSuccess: (HaapiResponse) -> Unit) { + withHaapiManager(onSuccess) { haapiManager, context -> + haapiManager.start(context) + } + } + + @Throws(FailedHaapiRequestException::class) + fun followLink(link: Link, onSuccess: (HaapiResponse) -> Unit) { + withHaapiManager(onSuccess) { haapiManager, context -> + haapiManager.followLink(link, context) + } + } + + @Throws(FailedHaapiRequestException::class) + fun submitForm( + form: FormActionModel, + parameters: Map, onSuccess: (HaapiResponse) -> Unit + ) { + withHaapiManager(onSuccess) { haapiManager, context -> + haapiManager.submitForm(form, parameters, context) + } + } + + @Throws(FailedTokenManagerRequestException::class) + fun exchangeCode(codeResponse: OAuthAuthorizationResponseStep, onSuccess: (TokenResponse) -> Unit) { + withTokenManager(onSuccess) { tokenManager, context -> + tokenManager.fetchAccessToken(codeResponse.properties.code, context) + } + } + + @Throws(FailedTokenManagerRequestException::class) + fun refreshAccessToken(refreshToken: String, onSuccess: (TokenResponse) -> Unit) { + Log.d(TAG, "Refreshing access token") + + try { + withTokenManager(onSuccess) { tokenManager, coroutineContext -> + tokenManager.refreshAccessToken(refreshToken, coroutineContext) + } + } catch (e: Exception) { + Log.d(TAG, "Failed to refresh tokens: ${e.message}") + throw FailedTokenManagerRequestException("Failed to refresh token", e) + } + } + + fun logoutAndRevokeTokens(accessToken: String, refreshToken: String? = null) { + try { + if (refreshToken != null) { + Log.d(TAG, "Revoking refresh token") + withTokenManager { tokenManager, context -> + tokenManager.revokeRefreshToken(refreshToken!!, context) + null + } + } else { + Log.d(TAG, "Revoking access token") + withTokenManager { tokenManager, context -> + tokenManager.revokeAccessToken(accessToken, context) + null + } + } + } catch (e: Exception) { + Log.d(TAG, "Failed to revoke tokens: ${e.message}") + } + + _accessorRepository?.close() + } + + fun closeHaapiConnection() { + _accessorRepository?.close() + } + + @Throws(FailedHaapiRequestException::class) + private fun withHaapiManager( + onSuccess: (HaapiResponse) -> Unit, + accessorRequest: suspend (manager: HaapiManager, context: CoroutineContext) -> HaapiResponse + ) { + _eventEmitter.sendEvent(HaapiLoading) + + val manager = _accessorRepository?.accessor?.haapiManager ?: throw notInitialized() + + _haapiScope.launch { + try { + val response = accessorRequest(manager, this.coroutineContext) + onSuccess(response) + } catch (e: Exception) { + Log.w(TAG, "Failed to make HAAPI request: ${e.message}") + _eventEmitter.sendEvent(EventType.HaapiFinishedLoading) + throw FailedHaapiRequestException("Failed to make HAAPI request: ${e.message}", e) + } + + _eventEmitter.sendEvent(EventType.HaapiFinishedLoading) + } + } + + @Throws(FailedTokenManagerRequestException::class) + private fun withTokenManager( + onSuccess: ((TokenResponse) -> Unit)? = null, + accessorRequest: suspend (tokenManager: OAuthTokenManager, coroutineContext: CoroutineContext) -> TokenResponse? + ) { + val manager = _accessorRepository?.accessor?.oAuthTokenManager ?: throw notInitialized() + + _haapiScope.launch { + _eventEmitter.sendEvent(HaapiLoading) + + try { + val response = accessorRequest(manager, this.coroutineContext) + if (onSuccess != null && response != null) { + onSuccess(response) + } + } catch (e: Exception) { + Log.w(TAG, "Failed to make token request: ${e.message}") + _eventEmitter.sendEvent(EventType.HaapiFinishedLoading) + throw FailedTokenManagerRequestException("Failed to make token request", e) + } + + _eventEmitter.sendEvent(EventType.HaapiFinishedLoading) + } + + } + + private fun notInitialized(): HaapiNotInitializedException { + Log.w( + TAG, "Accessor repository not initialized. " + + "Please run load() with configuration before accessing HAAPI functionality" + ) + _eventEmitter.sendEvent(EventType.HaapiFinishedLoading) + + throw HaapiNotInitializedException() + } +} \ No newline at end of file diff --git a/android/src/main/java/io/curity/haapi/react/HaapiModule.kt b/android/src/main/java/io/curity/haapi/react/HaapiModule.kt index 6c3e3ec..a90d8c0 100644 --- a/android/src/main/java/io/curity/haapi/react/HaapiModule.kt +++ b/android/src/main/java/io/curity/haapi/react/HaapiModule.kt @@ -16,21 +16,20 @@ package io.curity.haapi.react import android.util.Log -import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.WritableNativeMap -import com.facebook.react.modules.core.DeviceEventManagerModule import com.google.gson.Gson -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import io.curity.haapi.react.JsonUtil.jsonToNativeMap +import io.curity.haapi.react.JsonUtil.mapToNativeMap +import io.curity.haapi.react.events.EventEmitter +import io.curity.haapi.react.events.EventType +import io.curity.haapi.react.events.EventType.* +import io.curity.haapi.react.webauthn.WebAuthnHandler import se.curity.identityserver.haapi.android.driver.HaapiLogger -import se.curity.identityserver.haapi.android.sdk.HaapiManager -import se.curity.identityserver.haapi.android.sdk.OAuthTokenManager import se.curity.identityserver.haapi.android.sdk.models.AuthenticatorSelectorStep import se.curity.identityserver.haapi.android.sdk.models.ContinueSameStep import se.curity.identityserver.haapi.android.sdk.models.HaapiRepresentation @@ -42,12 +41,13 @@ import se.curity.identityserver.haapi.android.sdk.models.PollingStep import se.curity.identityserver.haapi.android.sdk.models.ProblemRepresentation import se.curity.identityserver.haapi.android.sdk.models.ProblemType import se.curity.identityserver.haapi.android.sdk.models.RepresentationType +import se.curity.identityserver.haapi.android.sdk.models.WebAuthnAuthenticationClientOperationStep +import se.curity.identityserver.haapi.android.sdk.models.WebAuthnRegistrationClientOperationStep import se.curity.identityserver.haapi.android.sdk.models.actions.Action import se.curity.identityserver.haapi.android.sdk.models.actions.FormActionModel import se.curity.identityserver.haapi.android.sdk.models.oauth.ErrorTokenResponse import se.curity.identityserver.haapi.android.sdk.models.oauth.SuccessfulTokenResponse import se.curity.identityserver.haapi.android.sdk.models.oauth.TokenResponse -import kotlin.coroutines.CoroutineContext const val TAG = "HaapiNative" @@ -59,6 +59,9 @@ class HaapiModule(private val _reactContext: ReactApplicationContext) : ReactCon private var _haapiResponse: HaapiRepresentation? = null private var _tokenResponse: SuccessfulTokenResponse? = null private val _gson = Gson() + private val _handler = HaapiInteractionHandler(_reactContext) + private val _eventEmitter = EventEmitter(_reactContext) + private val _webAuthnHandler = WebAuthnHandler(_reactContext) init { HaapiLogger.enabled = true @@ -66,10 +69,6 @@ class HaapiModule(private val _reactContext: ReactApplicationContext) : ReactCon _reactContext.addLifecycleEventListener(this) } - companion object { - private var _accessorRepository: HaapiAccessorRepository? = null - } - override fun onHostResume() { Log.d(TAG, "App was resumed") } @@ -84,28 +83,20 @@ class HaapiModule(private val _reactContext: ReactApplicationContext) : ReactCon @ReactMethod fun load(conf: ReadableMap, promise: Promise) { - if(_accessorRepository != null) { - _accessorRepository!!.close() - } - try { - _accessorRepository = HaapiAccessorRepository(conf, _reactContext) - } catch (e: Exception) { - Log.w(TAG, "Failed to load configuration ${e.message}") + _handler.loadConfiguration(conf.toHashMap()) + } catch (e: HaapiException) { rejectRequest(e, promise) - return } promise.resolve(true) } @ReactMethod fun start(promise: Promise) { - Log.d(TAG, "Start was called") + try { - withHaapiManager(promise) { haapiManager, context -> - haapiManager.start(context) - } + _handler.startAuthentication { response -> handleHaapiResponse(response, promise) } } catch (e: Exception) { Log.e(TAG, e.message ?: "Failed to attest $e") rejectRequest(e, promise) @@ -115,35 +106,24 @@ class HaapiModule(private val _reactContext: ReactApplicationContext) : ReactCon @ReactMethod fun logout(promise: Promise) { Log.d(TAG, "Logout was called, revoking tokens") - try { - if (_tokenResponse != null && _tokenResponse!!.refreshToken != null) { - Log.d(TAG, "Revoking refresh token") - withTokenManager(promise) { tokenManager, context -> - tokenManager.revokeRefreshToken(_tokenResponse!!.refreshToken!!, context) - null - } - } else if (_tokenResponse != null) { - Log.d(TAG, "Revoking access token") - withTokenManager(promise){ tokenManager, context -> - tokenManager.revokeAccessToken(_tokenResponse!!.accessToken, context) - null - } - } - } catch (e: Exception) { - Log.d(TAG, "Failed to revoke tokens: ${e.message}") + + if (_tokenResponse != null) { + _handler.logoutAndRevokeTokens(_tokenResponse!!.accessToken, _tokenResponse!!.refreshToken) + } else { + _handler.closeHaapiConnection() } - _accessorRepository?.close() _tokenResponse = null - resolveRequest("LoggedOut", "{}", promise) + resolveRequest(LoggedOut, "{}", promise) } @ReactMethod fun refreshAccessToken(refreshToken: String, promise: Promise) { Log.d(TAG, "Refreshing access token") + try { - withTokenManager(promise) { tokenManager, coroutineContext -> - tokenManager.refreshAccessToken(refreshToken, coroutineContext) + _handler.refreshAccessToken(refreshToken) { tokenResponse -> + handleTokenResponse(tokenResponse, promise) } } catch (e: Exception) { Log.d(TAG, "Failed to revoke tokens: ${e.message}") @@ -153,22 +133,27 @@ class HaapiModule(private val _reactContext: ReactApplicationContext) : ReactCon @ReactMethod fun navigate(linkMap: ReadableMap, promise: Promise) { + val linkJson = _gson.toJson(linkMap.toHashMap()) val link = _gson.fromJson(linkJson, Link::class.java) try { - withHaapiManager(promise) { haapiManager, context -> - haapiManager.followLink(link, context) - } + _handler.followLink(link) { response -> handleHaapiResponse(response, promise) } } catch (e: Exception) { Log.d(TAG, "Failed to navigate to link: ${e.message}") rejectRequest(e, promise) } } - @ReactMethod - fun submitForm(action: ReadableMap, parameters: ReadableMap, promise: Promise) { - getAction(action, _haapiResponse as HaapiRepresentation, parameters, promise) + fun submitForm(actionMap: ReadableMap, parameters: ReadableMap, promise: Promise) { + + val action = findAction(actionMap, _haapiResponse as HaapiRepresentation) + if (action == null) { + Log.d(TAG, "Failed to find action to submit. Possible re-submit") + return + } + + submitModel(action.model, parameters.toHashMap(), promise) } /** @@ -188,24 +173,32 @@ class HaapiModule(private val _reactContext: ReactApplicationContext) : ReactCon } private fun submitModel(model: FormActionModel, parameters: Map, promise: Promise) { - - Log.d(TAG, "Submitting form $model}") + Log.d(TAG, "Submitting form $model") try { - withHaapiManager(promise) { haapiManager, _ -> - haapiManager.submitForm(model, parameters) + _handler.submitForm(model, parameters) { response -> + handleHaapiResponse(response, promise) } } catch (e: Exception) { + Log.w(TAG, "Failed to submit form: ${e.message}") rejectRequest(e, promise) } + } private fun handleCodeResponse(response: OAuthAuthorizationResponseStep, promise: Promise) { - withTokenManager(promise) { manager, context -> - manager.fetchAccessToken(response.properties.code, context) + try { + _handler.exchangeCode(response) { tokenResponse -> + handleTokenResponse(tokenResponse, promise) + } + + } catch (e: Exception) { + Log.w(TAG, "Failed to exchange code: ${e.message}") + rejectRequest(e, promise) } } private fun handleTokenResponse(tokenResponse: TokenResponse, promise: Promise) { + if (tokenResponse is SuccessfulTokenResponse) { val tokenMap = mapOf( "accessToken" to tokenResponse.accessToken, @@ -215,17 +208,20 @@ class HaapiModule(private val _reactContext: ReactApplicationContext) : ReactCon "expiresIn" to tokenResponse.expiresIn.seconds ) _tokenResponse = tokenResponse - resolveRequest("TokenResponse", _gson.toJson(tokenMap), promise) + resolveRequest(TokenResponse, _gson.toJson(tokenMap), promise) } else { tokenResponse as ErrorTokenResponse val errorResponseMap = mapOf( "error" to tokenResponse.error, "error_description" to tokenResponse.errorDescription ) - resolveRequest("TokenResponseError", _gson.toJson(errorResponseMap), promise) + resolveRequest(TokenResponseError, JsonUtil.toJsonString(errorResponseMap), promise) } } - private fun getAction(actionMap: ReadableMap, representation: HaapiRepresentation, parameters: ReadableMap, promise: Promise) { + private fun findAction( + actionMap: ReadableMap, + representation: HaapiRepresentation, + ): Action.Form? { // TODO: Match more precise val submitKind = actionMap.getString("kind") Log.d(TAG, "Looking for form with kind $submitKind") @@ -234,49 +230,61 @@ class HaapiModule(private val _reactContext: ReactApplicationContext) : ReactCon val action = representation.actions.find { it.toJsonObject().get("kind") == submitKind } - (action as? Action.Form)?.let { submitModel(it.model, parameters.toHashMap(), promise) } + return action as? Action.Form } private fun handleHaapiResponse(response: HaapiResponse, promise: Promise) { + when (response) { is HaapiRepresentation -> { Log.d(TAG, "Updating reference to representation") _haapiResponse = response when (response) { + is WebAuthnRegistrationClientOperationStep -> handleWebAuthnRegistration(response, promise) + + is WebAuthnAuthenticationClientOperationStep -> handleWebAuthnAuthentication(response, promise) + is AuthenticatorSelectorStep -> resolveRequest( - "AuthenticationSelectorStep", response.toJsonString(), promise + AuthenticationSelectorStep, response.toJsonString(), promise ) is ContinueSameStep -> resolveRequest( - "ContinueSameStep", response.toJsonString(), promise + ContinueSameStep, response.toJsonString(), promise ) is OAuthAuthorizationResponseStep -> handleCodeResponse(response, promise) is PollingStep -> handlePollingStep(response, promise) - else -> if (response.type == RepresentationType.AUTHENTICATION_STEP) { - resolveRequest("AuthenticationStep", response.toJsonString(), promise) - } else { - Log.d(TAG, "Unknown step ${response.type}") - resolveRequest("UnknownResponse", response.toJsonString(), promise) - } + else -> when (response.type) { + RepresentationType.AUTHENTICATION_STEP -> { + resolveRequest(AuthenticationStep, response.toJsonString(), promise) + } + RepresentationType.REGISTRATION_STEP -> { + resolveRequest(RegistrationStep, response.toJsonString(), promise) + } + + else -> { + Log.d(TAG, "Unknown step ${response.type}") + resolveRequest(UnknownResponse, response.toJsonString(), promise) + } + } } } is ProblemRepresentation -> { when (response.type) { ProblemType.IncorrectCredentialsProblem -> { - resolveRequest("IncorrectCredentials", response.toJsonString(), promise) + resolveRequest(IncorrectCredentials, response.toJsonString(), promise) } ProblemType.SessionAndAccessTokenMismatchProblem -> { - resolveRequest("SessionTimedOut", response.toJsonString(), promise) + resolveRequest(SessionTimedOut, response.toJsonString(), promise) } else -> { - resolveRequest("ProblemRepresentation", response.toJsonString(), promise) + resolveRequest(ProblemRepresentation, response.toJsonString(), promise) } } } @@ -285,93 +293,53 @@ class HaapiModule(private val _reactContext: ReactApplicationContext) : ReactCon Log.d(TAG, response.toJsonString()) } + private fun handleWebAuthnAuthentication( + response: WebAuthnAuthenticationClientOperationStep, + promise: Promise + ) = + _webAuthnHandler.authenticationFlow(response, promise) { parameters -> + submitModel(response.continueFormActionModel, parameters, promise) + } + + private fun handleWebAuthnRegistration( + response: WebAuthnRegistrationClientOperationStep, + promise: Promise + ) = + _webAuthnHandler.registrationFlow(response, promise) { parameters -> + submitModel(response.continueFormActionModel, parameters, promise) + } + private fun handlePollingStep(pollingStep: PollingStep, promise: Promise) { - sendEvent("PollingStep", pollingStep.toJsonString()) + _eventEmitter.sendEvent(PollingStep, pollingStep.toJsonString()) + when (pollingStep.properties.status) { - PollingStatus.PENDING -> resolveRequest("PollingStepResult", pollingStep.toJsonString(), promise) + PollingStatus.PENDING -> resolveRequest(PollingStepResult, pollingStep.toJsonString(), promise) PollingStatus.FAILED -> { - sendEvent("StopPolling", "{}") + _eventEmitter.sendEvent(StopPolling, "{}") submitModel(pollingStep.mainAction.model, emptyMap(), promise) } + PollingStatus.DONE -> { - sendEvent("StopPolling", "{}") + _eventEmitter.sendEvent(StopPolling, "{}") submitModel(pollingStep.mainAction.model, emptyMap(), promise) } } } - @Throws(HaapiException::class) - private fun withTokenManager(promise: Promise, accessorRequest: suspend (tokenManager: OAuthTokenManager, coroutineContext: CoroutineContext) -> TokenResponse?) { - _accessorRepository ?: handleNotInitialized() - - runBlocking { - launch { - try { - val response = - accessorRequest(_accessorRepository!!.accessor.oAuthTokenManager, this.coroutineContext) - if (response != null) { - handleTokenResponse(response, promise) - promise.resolve(response.toJsonString()) - } - } catch (e: Exception) { - Log.w(TAG, "Failed to make token request: ${e.message}") - throw FailedTokenManagerRequestException("Failed to make token request", e) - } - } - } - - } - - @Throws(HaapiException::class) - private fun withHaapiManager(promise: Promise, accessorRequest: suspend (manager: HaapiManager, context: CoroutineContext) -> HaapiResponse) { - _accessorRepository ?: handleNotInitialized() - - runBlocking { - launch { - try { - val response = accessorRequest(_accessorRepository!!.accessor.haapiManager, this.coroutineContext) - handleHaapiResponse(response, promise) - } catch (e: Exception) { - Log.w(TAG, "Failed to make HAAPI request: ${e.message}") - throw FailedHaapiRequestException("Failed to make HAAPI request: ${e.message}", e) - } - } - } - } - - private fun handleNotInitialized() { - Log.w( - TAG, "Accessor repository not initialized. " + - "Please run load() with configuration before accessing HAAPI functionality" - ) - throw HaapiNotInitializedException() - } - - - private fun sendEvent(eventName: String, json: String) { - Log.d(TAG, "Firing event $eventName") + private fun rejectRequest(exception: Exception, promise: Promise) { + val errorDescription = exception.message ?: "general error" + val jsonMap = mutableMapOf("error" to "HaapiError", "error_description" to errorDescription) - val reactMap = jsonToMap(json) - _reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(eventName, reactMap) - } + Log.d(TAG, "Rejecting request using with description '$errorDescription'") - private fun rejectRequest(exception: Exception, promise: Promise) { - val map = Arguments.createMap() - map.putString("error", "HaapiError") - map.putString("error_description", exception.message) - _reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit("HaapiError", map.copy()) - promise.reject(exception, map) + _eventEmitter.sendEvent(HaapiException, JsonUtil.toJsonString(jsonMap)) + promise.reject(exception, mapToNativeMap(jsonMap)) } - private fun jsonToMap(json: String): WritableNativeMap? { - var map = HashMap() - map = _gson.fromJson(json, map::class.java) - return Arguments.makeNativeMap(map) - } + private fun resolveRequest(eventType: EventType, body: String, promise: Promise) { + Log.d(TAG, "Resolving request using event type $eventType and body: $body") - private fun resolveRequest(eventType: String, body: String, promise: Promise) { - sendEvent(eventType, body) - promise.resolve(jsonToMap(body)) + _eventEmitter.sendEvent(eventType, body) + promise.resolve(jsonToNativeMap(body)) } - } diff --git a/android/src/main/java/io/curity/haapi/react/JsonUtil.kt b/android/src/main/java/io/curity/haapi/react/JsonUtil.kt new file mode 100644 index 0000000..2f16147 --- /dev/null +++ b/android/src/main/java/io/curity/haapi/react/JsonUtil.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Curity AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.curity.haapi.react + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableNativeMap +import com.google.gson.Gson + +object JsonUtil { + private val gson = Gson() + + fun jsonToNativeMap(json: String): WritableNativeMap { + val map = jsonToMap(json) + return Arguments.makeNativeMap(map) + } + + fun mapToNativeMap(map: Map): WritableNativeMap { + return Arguments.makeNativeMap(map) + } + + fun jsonToMap(json: String): Map { + val map = HashMap() + return gson.fromJson(json, map::class.java) + } + + fun toJsonString(any: Any): String = gson.toJson(any) +} \ No newline at end of file diff --git a/android/src/main/java/io/curity/haapi/react/events/EventEmitter.kt b/android/src/main/java/io/curity/haapi/react/events/EventEmitter.kt new file mode 100644 index 0000000..31e08dc --- /dev/null +++ b/android/src/main/java/io/curity/haapi/react/events/EventEmitter.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Curity AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.curity.haapi.react.events + +import android.util.Log +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.modules.core.DeviceEventManagerModule +import io.curity.haapi.react.JsonUtil.jsonToNativeMap +import io.curity.haapi.react.TAG + +class EventEmitter(private val _reactContext: ReactApplicationContext) { + + fun sendEvent(type: EventType, json: String = "{}") { + Log.d(TAG, "Firing event $type") + + val reactMap = jsonToNativeMap(json) + _reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(type.toString(), + reactMap) + } +} \ No newline at end of file diff --git a/android/src/main/java/io/curity/haapi/react/events/EventType.kt b/android/src/main/java/io/curity/haapi/react/events/EventType.kt new file mode 100644 index 0000000..23ffaa9 --- /dev/null +++ b/android/src/main/java/io/curity/haapi/react/events/EventType.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Curity AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.curity.haapi.react.events + +enum class EventType { + WebAuthnAuthenticationStep, + WebAuthnUserCancelled, + AuthenticationSelectorStep, + ContinueSameStep, + AuthenticationStep, + RegistrationStep, + TokenResponse, + TokenResponseError, + UnknownResponse, + IncorrectCredentials, + SessionTimedOut, + PollingStep, + StopPolling, + HaapiException, + PollingStepResult, + ProblemRepresentation, + LoggedOut, + HaapiLoading, + HaapiFinishedLoading, + WebAuthnRegistrationFailed, + WebAuthnRegistrationFailedKeyRegistered +} \ No newline at end of file diff --git a/android/src/main/java/io/curity/haapi/react/exceptions.kt b/android/src/main/java/io/curity/haapi/react/exceptions.kt index 15b8f11..4a5baf3 100644 --- a/android/src/main/java/io/curity/haapi/react/exceptions.kt +++ b/android/src/main/java/io/curity/haapi/react/exceptions.kt @@ -15,7 +15,8 @@ */ package io.curity.haapi.react -open class HaapiException(message: String, e: Throwable?) : Exception(message, e) +open class HaapiException(message: String, e: Throwable? = null) : Exception(message, e) +class FailedToInitalizeHaapiException(message: String, e: Throwable) : HaapiException(message, e) class FailedHaapiRequestException(message: String, e: Throwable?) : HaapiException(message, e) class FailedTokenManagerRequestException(message: String, e: Throwable?) : HaapiException(message, e) class HaapiNotInitializedException(message: String = "Module not initialized") : HaapiException(message, null) diff --git a/android/src/main/java/io/curity/haapi/react/webauthn/WebAuthnHandler.kt b/android/src/main/java/io/curity/haapi/react/webauthn/WebAuthnHandler.kt new file mode 100644 index 0000000..0a29ff6 --- /dev/null +++ b/android/src/main/java/io/curity/haapi/react/webauthn/WebAuthnHandler.kt @@ -0,0 +1,145 @@ +package io.curity.haapi.react.webauthn + +import android.util.Log +import androidx.credentials.CreatePublicKeyCredentialRequest +import androidx.credentials.CredentialManager +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.exceptions.CreateCredentialCancellationException +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.GetCredentialCancellationException +import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.domerrors.InvalidStateError +import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import io.curity.haapi.react.HaapiException +import io.curity.haapi.react.JsonUtil +import io.curity.haapi.react.TAG +import io.curity.haapi.react.events.EventEmitter +import io.curity.haapi.react.events.EventType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import se.curity.identityserver.haapi.android.sdk.internal.JsonObjectable +import se.curity.identityserver.haapi.android.sdk.models.WebAuthnAuthenticationClientOperationStep +import se.curity.identityserver.haapi.android.sdk.models.WebAuthnRegistrationClientOperationStep + +class WebAuthnHandler(private val _reactContext: ReactApplicationContext) { + private val _eventEmitter = EventEmitter(_reactContext) + private val _passkeyScope = CoroutineScope(Dispatchers.IO) + + fun authenticationFlow( + clientOperation: WebAuthnAuthenticationClientOperationStep, + promise: Promise, + onSuccess: (parameters: Map) -> Unit + ) { + // Give the app the chance to show a webauthn step + _eventEmitter.sendEvent(EventType.WebAuthnAuthenticationStep, clientOperation.toJsonString()) + + // Start passkey authentication + val credentialManager = CredentialManager.create(_reactContext.applicationContext) + val requestOptions = WebAuthnAuthenticationRequest.from(clientOperation.actionModel.publicKey) + val getPublicKeyCredentialOption = GetPublicKeyCredentialOption(JsonUtil.toJsonString(requestOptions)) + val credentialRequest = GetCredentialRequest(listOf(getPublicKeyCredentialOption)) + + _passkeyScope.launch { + try { + Log.d(TAG, "Querying for passkey credential") + val result = credentialManager.getCredential(_reactContext.applicationContext, credentialRequest) + val parameters = AuthenticationResultParameters.from(result.credential.data).asMap() + Log.d(TAG, "Successful passkey authentication, continuing flow with parameters $parameters") + onSuccess(parameters) + } catch (e: GetCredentialException) { + handleAuthenticationFailure(e, clientOperation, promise) + } + } + } + + fun registrationFlow( + clientOperation: WebAuthnRegistrationClientOperationStep, + promise: Promise, + onSuccess: (parameters: Map) -> Unit + ) { + val credentialManager = CredentialManager.create(_reactContext.applicationContext) + val request = CreatePublicKeyCredentialRequest(createRegisterJson(clientOperation)) + + _passkeyScope.launch { + try { + Log.d(TAG, "Asking user to register a passkey credential") + + val result = credentialManager.createCredential(_reactContext.applicationContext, request) + + val parameters = RegistrationResultParameters.from(result.data).asMap() + Log.d(TAG, "Successful passkey registration, continuing flow with parameters $parameters") + + onSuccess(parameters) + } catch (e: CreateCredentialException) { + Log.d(TAG, "Failed to register webauthn device") + handleRegistrationFailure(e, clientOperation, promise) + } + } + } + + private fun createRegisterJson(response: WebAuthnRegistrationClientOperationStep): String { + val publicKeyRequest = response?.requestPublicKey + ?: throw HaapiException("No platform device request in passkey haapi response") + + val json = JsonUtil.toJsonString(WebAuthnRegistrationRequest.from(publicKeyRequest)) + Log.d(TAG, "Created registration json: $json") + return json + } + + + private fun handleRegistrationFailure( + e: CreateCredentialException, + clientOperation: WebAuthnRegistrationClientOperationStep, + promise: Promise + ) { + when (e) { + is CreateCredentialCancellationException -> { + Log.d(TAG, "User cancelled the registration") + webauthnFailed(EventType.WebAuthnUserCancelled, clientOperation, promise) + } + + is CreatePublicKeyCredentialDomException -> { + Log.i(TAG, "Registration failed: ${e.message}, type: ${e.type}") + if (e.domError is InvalidStateError) { + webauthnFailed(EventType.WebAuthnRegistrationFailedKeyRegistered, clientOperation, promise) + } else { + webauthnFailed(EventType.WebAuthnRegistrationFailed, clientOperation, promise) + } + } + + else -> Log.i(TAG, "Registration failed: ${e.message}, type: ${e.type}") + } + } + + private fun handleAuthenticationFailure( + e: GetCredentialException, + clientOperation: WebAuthnAuthenticationClientOperationStep, + promise: Promise + ) { + when (e) { + is GetCredentialCancellationException -> { + Log.d(TAG, "User cancelled passkey authentication. Allow the try again or register a device") + webauthnFailed(EventType.WebAuthnUserCancelled, clientOperation, promise) + } + + else -> { + Log.d(TAG, "Failed to authenticate using webauthn device") + webauthnFailed(EventType.WebAuthnRegistrationFailed, clientOperation, promise) + } + } + } + + private fun webauthnFailed( + eventType: EventType, + clientOperation: JsonObjectable, + promise: Promise, + ) { + val jsonString = clientOperation.toJsonString() + _eventEmitter.sendEvent(eventType, jsonString) + promise.resolve(JsonUtil.jsonToNativeMap(jsonString)) + } +} \ No newline at end of file diff --git a/android/src/main/java/io/curity/haapi/react/webauthn/parameters.kt b/android/src/main/java/io/curity/haapi/react/webauthn/parameters.kt new file mode 100644 index 0000000..f6ed356 --- /dev/null +++ b/android/src/main/java/io/curity/haapi/react/webauthn/parameters.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2024 Curity AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.curity.haapi.react.webauthn + +import android.os.Bundle +import io.curity.haapi.react.HaapiException +import io.curity.haapi.react.JsonUtil + + +data class RegistrationResultParameters(private val credential: Map) { + fun asMap(): Map { + // In the response from OS, the response member contains fields not supported by the server. + val response = credential["response"] as? Map<*, *> + ?: throw HaapiException("Missing response in webauthn registration response") + + val clientDataJson = response["clientDataJSON"] + ?: throw HaapiException("Missing clientDataJSON in webauthn registration response") + + val attestationObject = response["attestationObject"] + ?: throw HaapiException("Missing clientDataJSON in webauthn registration response") + + val newResponse = mapOf("clientDataJSON" to clientDataJson, "attestationObject" to attestationObject) + + val mutableCredential = credential.toMutableMap() + mutableCredential["response"] = newResponse + + return mapOf("credential" to mutableCredential) + } + + companion object { + fun from(bundle: Bundle): RegistrationResultParameters { + val rawResponseJson = bundle.getString("androidx.credentials.BUNDLE_KEY_REGISTRATION_RESPONSE_JSON") + ?: throw HaapiException("No platform device request in passkey haapi response") + + return RegistrationResultParameters(JsonUtil.jsonToMap(rawResponseJson)) + } + } +} + +data class AuthenticationResultParameters(private val credential: Map) { + + companion object { + fun from(bundle: Bundle): AuthenticationResultParameters { + val jsonString = bundle.getString("androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON") + ?: throw HaapiException("Not a successful result") + return AuthenticationResultParameters(JsonUtil.jsonToMap(jsonString)) + } + } + + fun asMap(): Map = mapOf("credential" to credential) +} \ No newline at end of file diff --git a/android/src/main/java/io/curity/haapi/react/webauthn/webauthnRequests.kt b/android/src/main/java/io/curity/haapi/react/webauthn/webauthnRequests.kt new file mode 100644 index 0000000..f127d5a --- /dev/null +++ b/android/src/main/java/io/curity/haapi/react/webauthn/webauthnRequests.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2024 Curity AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.curity.haapi.react.webauthn + +import se.curity.identityserver.haapi.android.sdk.models.actions.ClientOperationActionModel +import se.curity.identityserver.haapi.android.sdk.models.actions.ClientOperationActionModel.WebAuthnRegistration.PublicKey +import se.curity.identityserver.haapi.android.sdk.models.actions.Credential +import java.util.stream.Collectors +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +data class RelyingParty(val name: String, val id: String) + +data class User(val name: String, val displayName: String, val id: String) + +data class AuthenticatorSelection( + val authenticatorAttachment: String, + val userVerification: String = "required", + val requiresResidentKey: Boolean = true, val residentKey: String = "required" +) + +@OptIn(ExperimentalEncodingApi::class) +private fun encodeToUrlSafeString(bytes: ByteArray): String = Base64.UrlSafe.encode(bytes) + +private fun mapCredentials(credentials: List): List = + credentials.stream().map { UrlSafeCredential.from(it) }.collect(Collectors.toList()) + +data class UrlSafeCredential(val id: String, val type: String = "public-key") { + companion object { + fun from(credential: Credential): UrlSafeCredential = + UrlSafeCredential(encodeToUrlSafeString(credential.idByteArray), credential.type) + } +} + +data class WebAuthnAuthenticationRequest( + val rpId: String, + val userVerification: String = "required", + val challenge: String, + val extensions: Map = emptyMap(), + val allowCredentials: List, + val platformCredentials: List, + val crossPlatformCredentials: List, +) { + + companion object { + fun from(publicKey: ClientOperationActionModel.WebAuthnAuthentication.PublicKey): WebAuthnAuthenticationRequest { + val allowCredentials = mapCredentials(publicKey.allowCredentials) + val platformCredentials = mapCredentials(publicKey.platformCredentials) + val crossPlatformCredentials = mapCredentials(publicKey.crossPlatformCredentials) + return WebAuthnAuthenticationRequest( + publicKey.relyingPartyId, + publicKey.userVerification, + encodeToUrlSafeString(publicKey.challenge), + emptyMap(), + allowCredentials, + platformCredentials, + crossPlatformCredentials + ) + } + + } +} + +data class WebAuthnRegistrationRequest( + val rp: RelyingParty, + val user: User, + val challenge: String, + val authenticatorSelection: AuthenticatorSelection, + val excludeCredentials: List, + val pubKeyCredParams: List, + val extensions: Map = emptyMap() +) { + + companion object { + fun from(publicKey: PublicKey): WebAuthnRegistrationRequest { + val rp = RelyingParty(publicKey.relyingPartyName, publicKey.relyingPartyId) + val user = User(publicKey.userName, publicKey.userDisplayName, encodeToUrlSafeString(publicKey.userId)) + val pubkeyParams = publicKey.credentialParameters + val selection = AuthenticatorSelection( + publicKey.authenticatorSelection.authenticatorAttachment, + publicKey.authenticatorSelection.userVerification + ) + val excludedCredentials = mapCredentials(publicKey.excludedCredentials) + return WebAuthnRegistrationRequest( + rp, user, encodeToUrlSafeString(publicKey.challenge), selection, + excludedCredentials, pubkeyParams + ) + } + } + +} From eca15d1821eb4d3c516a46b2d3e4b4fac13d5645 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 21 Nov 2024 08:58:57 +0100 Subject: [PATCH 03/19] rebased on latest main --- ios/HaapiModule.xcodeproj/project.pbxproj | 44 -- ios/Podfile | 38 -- ios/Podfile.lock | 610 ---------------------- react-native-haapi-module.podspec | 2 +- 4 files changed, 1 insertion(+), 693 deletions(-) delete mode 100644 ios/Podfile delete mode 100644 ios/Podfile.lock diff --git a/ios/HaapiModule.xcodeproj/project.pbxproj b/ios/HaapiModule.xcodeproj/project.pbxproj index d3ba347..654c626 100644 --- a/ios/HaapiModule.xcodeproj/project.pbxproj +++ b/ios/HaapiModule.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 5E46EC5AAE31118A736DE542 /* libPods-HaapiModule.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7751C9D7B3F15EC5702CA498 /* libPods-HaapiModule.a */; }; 6571695A2B8E230C00A1B372 /* TrustAllCertsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657169592B8E230C00A1B372 /* TrustAllCertsDelegate.swift */; }; 65B6F8682B8CDC5500AC54A9 /* HaapiModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B6F8652B8CDC5500AC54A9 /* HaapiModule.swift */; }; 65B6F8692B8CDC5500AC54A9 /* HaapiModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B6F8662B8CDC5500AC54A9 /* HaapiModule.m */; }; @@ -27,15 +26,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 2064A052866A8BEFDEC5032B /* Pods-HaapiModule.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HaapiModule.debug.xcconfig"; path = "Target Support Files/Pods-HaapiModule/Pods-HaapiModule.debug.xcconfig"; sourceTree = ""; }; 657169592B8E230C00A1B372 /* TrustAllCertsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustAllCertsDelegate.swift; sourceTree = ""; }; 65B6F83F2B8CD91800AC54A9 /* libHaapiModule.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libHaapiModule.a; sourceTree = BUILT_PRODUCTS_DIR; }; 65B6F8642B8CDC5500AC54A9 /* HaapiModule-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "HaapiModule-Bridging-Header.h"; sourceTree = ""; }; 65B6F8652B8CDC5500AC54A9 /* HaapiModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HaapiModule.swift; sourceTree = ""; }; 65B6F8662B8CDC5500AC54A9 /* HaapiModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HaapiModule.m; sourceTree = ""; }; 65B6F8672B8CDC5500AC54A9 /* ConfigurationHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationHelper.swift; sourceTree = ""; }; - 7751C9D7B3F15EC5702CA498 /* libPods-HaapiModule.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-HaapiModule.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 9654A8B9DEEC1259A745EA63 /* Pods-HaapiModule.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HaapiModule.release.xcconfig"; path = "Target Support Files/Pods-HaapiModule/Pods-HaapiModule.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -43,7 +39,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5E46EC5AAE31118A736DE542 /* libPods-HaapiModule.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -53,8 +48,6 @@ 5D8522AD620596F14AA4AAE0 /* Pods */ = { isa = PBXGroup; children = ( - 2064A052866A8BEFDEC5032B /* Pods-HaapiModule.debug.xcconfig */, - 9654A8B9DEEC1259A745EA63 /* Pods-HaapiModule.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -65,7 +58,6 @@ 65B6F8632B8CDC5500AC54A9 /* HaapiModule */, 65B6F8402B8CD91800AC54A9 /* Products */, 5D8522AD620596F14AA4AAE0 /* Pods */, - 84C1BB77743FEC0AAD2C1194 /* Frameworks */, ); sourceTree = ""; }; @@ -89,14 +81,6 @@ path = HaapiModule; sourceTree = ""; }; - 84C1BB77743FEC0AAD2C1194 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 7751C9D7B3F15EC5702CA498 /* libPods-HaapiModule.a */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -104,7 +88,6 @@ isa = PBXNativeTarget; buildConfigurationList = 65B6F8462B8CD91800AC54A9 /* Build configuration list for PBXNativeTarget "HaapiModule" */; buildPhases = ( - 475C86EC09D6C430EB9DCD03 /* [CP] Check Pods Manifest.lock */, 65B6F83B2B8CD91800AC54A9 /* Sources */, 65B6F83C2B8CD91800AC54A9 /* Frameworks */, 65B6F83D2B8CD91800AC54A9 /* CopyFiles */, @@ -152,31 +135,6 @@ }; /* End PBXProject section */ -/* Begin PBXShellScriptBuildPhase section */ - 475C86EC09D6C430EB9DCD03 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-HaapiModule-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 65B6F83B2B8CD91800AC54A9 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -340,7 +298,6 @@ }; 65B6F8472B8CD91800AC54A9 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2064A052866A8BEFDEC5032B /* Pods-HaapiModule.debug.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; @@ -357,7 +314,6 @@ }; 65B6F8482B8CD91800AC54A9 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9654A8B9DEEC1259A745EA63 /* Pods-HaapiModule.release.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Podfile b/ios/Podfile deleted file mode 100644 index 000369e..0000000 --- a/ios/Podfile +++ /dev/null @@ -1,38 +0,0 @@ -# Resolve react_native_pods.rb with node to allow for hoisting -require Pod::Executable.execute_command('node', ['-p', - 'require.resolve( - "react-native/scripts/react_native_pods.rb", - {paths: [process.argv[1]]}, - )', __dir__]).strip - -platform :ios, '14.0' -prepare_react_native_project! - - -linkage = ENV['USE_FRAMEWORKS'] -if linkage != nil - Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green - use_frameworks! :linkage => linkage.to_sym -end - -target 'HaapiModule' do - config = use_native_modules! - - pod 'IdsvrHaapiSdk', '4.1.0' - - use_react_native!( - :path => config[:reactNativePath], - # An absolute path to your application root. - :app_path => "#{Pod::Config.instance.installation_root}/.." - ) - - post_install do |installer| - # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 - react_native_post_install( - installer, - config[:reactNativePath], - :mac_catalyst_enabled => false - ) - __apply_Xcode_12_5_M1_post_install_workaround(installer) - end -end diff --git a/ios/Podfile.lock b/ios/Podfile.lock deleted file mode 100644 index 439f21b..0000000 --- a/ios/Podfile.lock +++ /dev/null @@ -1,610 +0,0 @@ -PODS: - - boost (1.76.0) - - DoubleConversion (1.1.6) - - FBLazyVector (0.72.10) - - FBReactNativeSpec (0.72.10): - - RCT-Folly (= 2021.07.22.00) - - RCTRequired (= 0.72.10) - - RCTTypeSafety (= 0.72.10) - - React-Core (= 0.72.10) - - React-jsi (= 0.72.10) - - ReactCommon/turbomodule/core (= 0.72.10) - - fmt (6.2.1) - - glog (0.3.5) - - hermes-engine (0.72.10): - - hermes-engine/Pre-built (= 0.72.10) - - hermes-engine/Pre-built (0.72.10) - - IdsvrHaapiSdk (4.1.0) - - libevent (2.1.12) - - RCT-Folly (2021.07.22.00): - - boost - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Default (= 2021.07.22.00) - - RCT-Folly/Default (2021.07.22.00): - - boost - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - RCT-Folly/Futures (2021.07.22.00): - - boost - - DoubleConversion - - fmt (~> 6.2.1) - - glog - - libevent - - RCTRequired (0.72.10) - - RCTTypeSafety (0.72.10): - - FBLazyVector (= 0.72.10) - - RCTRequired (= 0.72.10) - - React-Core (= 0.72.10) - - React (0.72.10): - - React-Core (= 0.72.10) - - React-Core/DevSupport (= 0.72.10) - - React-Core/RCTWebSocket (= 0.72.10) - - React-RCTActionSheet (= 0.72.10) - - React-RCTAnimation (= 0.72.10) - - React-RCTBlob (= 0.72.10) - - React-RCTImage (= 0.72.10) - - React-RCTLinking (= 0.72.10) - - React-RCTNetwork (= 0.72.10) - - React-RCTSettings (= 0.72.10) - - React-RCTText (= 0.72.10) - - React-RCTVibration (= 0.72.10) - - React-callinvoker (0.72.10) - - React-Codegen (0.72.10): - - DoubleConversion - - FBReactNativeSpec - - glog - - hermes-engine - - RCT-Folly - - RCTRequired - - RCTTypeSafety - - React-Core - - React-jsi - - React-jsiexecutor - - React-NativeModulesApple - - React-rncore - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - React-Core (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.72.10) - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/CoreModulesHeaders (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/Default (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/DevSupport (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.72.10) - - React-Core/RCTWebSocket (= 0.72.10) - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector (= 0.72.10) - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/RCTActionSheetHeaders (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/RCTAnimationHeaders (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/RCTBlobHeaders (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/RCTImageHeaders (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/RCTLinkingHeaders (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/RCTNetworkHeaders (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/RCTSettingsHeaders (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/RCTTextHeaders (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/RCTVibrationHeaders (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-Core/RCTWebSocket (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.72.10) - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket (= 0.6.1) - - Yoga - - React-CoreModules (0.72.10): - - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.72.10) - - React-Codegen (= 0.72.10) - - React-Core/CoreModulesHeaders (= 0.72.10) - - React-jsi (= 0.72.10) - - React-RCTBlob - - React-RCTImage (= 0.72.10) - - ReactCommon/turbomodule/core (= 0.72.10) - - SocketRocket (= 0.6.1) - - React-cxxreact (0.72.10): - - boost (= 1.76.0) - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-callinvoker (= 0.72.10) - - React-debug (= 0.72.10) - - React-jsi (= 0.72.10) - - React-jsinspector (= 0.72.10) - - React-logger (= 0.72.10) - - React-perflogger (= 0.72.10) - - React-runtimeexecutor (= 0.72.10) - - React-debug (0.72.10) - - React-hermes (0.72.10): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - RCT-Folly/Futures (= 2021.07.22.00) - - React-cxxreact (= 0.72.10) - - React-jsi - - React-jsiexecutor (= 0.72.10) - - React-jsinspector (= 0.72.10) - - React-perflogger (= 0.72.10) - - React-jsi (0.72.10): - - boost (= 1.76.0) - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-jsiexecutor (0.72.10): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-cxxreact (= 0.72.10) - - React-jsi (= 0.72.10) - - React-perflogger (= 0.72.10) - - React-jsinspector (0.72.10) - - React-logger (0.72.10): - - glog - - React-NativeModulesApple (0.72.10): - - hermes-engine - - React-callinvoker - - React-Core - - React-cxxreact - - React-jsi - - React-runtimeexecutor - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - React-perflogger (0.72.10) - - React-RCTActionSheet (0.72.10): - - React-Core/RCTActionSheetHeaders (= 0.72.10) - - React-RCTAnimation (0.72.10): - - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.72.10) - - React-Codegen (= 0.72.10) - - React-Core/RCTAnimationHeaders (= 0.72.10) - - React-jsi (= 0.72.10) - - ReactCommon/turbomodule/core (= 0.72.10) - - React-RCTAppDelegate (0.72.10): - - RCT-Folly - - RCTRequired - - RCTTypeSafety - - React-Core - - React-CoreModules - - React-hermes - - React-NativeModulesApple - - React-RCTImage - - React-RCTNetwork - - React-runtimescheduler - - ReactCommon/turbomodule/core - - React-RCTBlob (0.72.10): - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-Codegen (= 0.72.10) - - React-Core/RCTBlobHeaders (= 0.72.10) - - React-Core/RCTWebSocket (= 0.72.10) - - React-jsi (= 0.72.10) - - React-RCTNetwork (= 0.72.10) - - ReactCommon/turbomodule/core (= 0.72.10) - - React-RCTImage (0.72.10): - - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.72.10) - - React-Codegen (= 0.72.10) - - React-Core/RCTImageHeaders (= 0.72.10) - - React-jsi (= 0.72.10) - - React-RCTNetwork (= 0.72.10) - - ReactCommon/turbomodule/core (= 0.72.10) - - React-RCTLinking (0.72.10): - - React-Codegen (= 0.72.10) - - React-Core/RCTLinkingHeaders (= 0.72.10) - - React-jsi (= 0.72.10) - - ReactCommon/turbomodule/core (= 0.72.10) - - React-RCTNetwork (0.72.10): - - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.72.10) - - React-Codegen (= 0.72.10) - - React-Core/RCTNetworkHeaders (= 0.72.10) - - React-jsi (= 0.72.10) - - ReactCommon/turbomodule/core (= 0.72.10) - - React-RCTSettings (0.72.10): - - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.72.10) - - React-Codegen (= 0.72.10) - - React-Core/RCTSettingsHeaders (= 0.72.10) - - React-jsi (= 0.72.10) - - ReactCommon/turbomodule/core (= 0.72.10) - - React-RCTText (0.72.10): - - React-Core/RCTTextHeaders (= 0.72.10) - - React-RCTVibration (0.72.10): - - RCT-Folly (= 2021.07.22.00) - - React-Codegen (= 0.72.10) - - React-Core/RCTVibrationHeaders (= 0.72.10) - - React-jsi (= 0.72.10) - - ReactCommon/turbomodule/core (= 0.72.10) - - React-rncore (0.72.10) - - React-runtimeexecutor (0.72.10): - - React-jsi (= 0.72.10) - - React-runtimescheduler (0.72.10): - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-callinvoker - - React-debug - - React-jsi - - React-runtimeexecutor - - React-utils (0.72.10): - - glog - - RCT-Folly (= 2021.07.22.00) - - React-debug - - ReactCommon/turbomodule/bridging (0.72.10): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-callinvoker (= 0.72.10) - - React-cxxreact (= 0.72.10) - - React-jsi (= 0.72.10) - - React-logger (= 0.72.10) - - React-perflogger (= 0.72.10) - - ReactCommon/turbomodule/core (0.72.10): - - DoubleConversion - - glog - - hermes-engine - - RCT-Folly (= 2021.07.22.00) - - React-callinvoker (= 0.72.10) - - React-cxxreact (= 0.72.10) - - React-jsi (= 0.72.10) - - React-logger (= 0.72.10) - - React-perflogger (= 0.72.10) - - SocketRocket (0.6.1) - - Yoga (1.14.0) - -DEPENDENCIES: - - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) - - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - - IdsvrHaapiSdk (= 4.1.0) - - libevent (~> 2.1.12) - - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) - - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) - - React (from `../node_modules/react-native/`) - - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) - - React-Codegen (from `build/generated/ios`) - - React-Core (from `../node_modules/react-native/`) - - React-Core/RCTWebSocket (from `../node_modules/react-native/`) - - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) - - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) - - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) - - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) - - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) - - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) - - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) - - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) - - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) - - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) - - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) - - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) - - React-RCTText (from `../node_modules/react-native/Libraries/Text`) - - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - - React-rncore (from `../node_modules/react-native/ReactCommon`) - - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) - -SPEC REPOS: - trunk: - - fmt - - IdsvrHaapiSdk - - libevent - - SocketRocket - -EXTERNAL SOURCES: - boost: - :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" - DoubleConversion: - :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" - FBLazyVector: - :path: "../node_modules/react-native/Libraries/FBLazyVector" - FBReactNativeSpec: - :path: "../node_modules/react-native/React/FBReactNativeSpec" - glog: - :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" - hermes-engine: - :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" - RCT-Folly: - :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" - RCTRequired: - :path: "../node_modules/react-native/Libraries/RCTRequired" - RCTTypeSafety: - :path: "../node_modules/react-native/Libraries/TypeSafety" - React: - :path: "../node_modules/react-native/" - React-callinvoker: - :path: "../node_modules/react-native/ReactCommon/callinvoker" - React-Codegen: - :path: build/generated/ios - React-Core: - :path: "../node_modules/react-native/" - React-CoreModules: - :path: "../node_modules/react-native/React/CoreModules" - React-cxxreact: - :path: "../node_modules/react-native/ReactCommon/cxxreact" - React-debug: - :path: "../node_modules/react-native/ReactCommon/react/debug" - React-hermes: - :path: "../node_modules/react-native/ReactCommon/hermes" - React-jsi: - :path: "../node_modules/react-native/ReactCommon/jsi" - React-jsiexecutor: - :path: "../node_modules/react-native/ReactCommon/jsiexecutor" - React-jsinspector: - :path: "../node_modules/react-native/ReactCommon/jsinspector" - React-logger: - :path: "../node_modules/react-native/ReactCommon/logger" - React-NativeModulesApple: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" - React-perflogger: - :path: "../node_modules/react-native/ReactCommon/reactperflogger" - React-RCTActionSheet: - :path: "../node_modules/react-native/Libraries/ActionSheetIOS" - React-RCTAnimation: - :path: "../node_modules/react-native/Libraries/NativeAnimation" - React-RCTAppDelegate: - :path: "../node_modules/react-native/Libraries/AppDelegate" - React-RCTBlob: - :path: "../node_modules/react-native/Libraries/Blob" - React-RCTImage: - :path: "../node_modules/react-native/Libraries/Image" - React-RCTLinking: - :path: "../node_modules/react-native/Libraries/LinkingIOS" - React-RCTNetwork: - :path: "../node_modules/react-native/Libraries/Network" - React-RCTSettings: - :path: "../node_modules/react-native/Libraries/Settings" - React-RCTText: - :path: "../node_modules/react-native/Libraries/Text" - React-RCTVibration: - :path: "../node_modules/react-native/Libraries/Vibration" - React-rncore: - :path: "../node_modules/react-native/ReactCommon" - React-runtimeexecutor: - :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" - React-runtimescheduler: - :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" - React-utils: - :path: "../node_modules/react-native/ReactCommon/react/utils" - ReactCommon: - :path: "../node_modules/react-native/ReactCommon" - Yoga: - :path: "../node_modules/react-native/ReactCommon/yoga" - -SPEC CHECKSUMS: - boost: 7dcd2de282d72e344012f7d6564d024930a6a440 - DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 - FBLazyVector: f91d538f197fa71a7d5b77ec2069d49550c0eb96 - FBReactNativeSpec: b13d1c23d6ed82d6b66aad7a253edf8ba76c4a4c - fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - hermes-engine: 90e4033deb00bee33330a9f15eff0f874bd82f6d - IdsvrHaapiSdk: cb63cc81cb1bf71f98a354e6d23eea29c439e1c9 - libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 - RCTRequired: b4d3068afa6f52ec5260a8417053b1f1b421483d - RCTTypeSafety: a4551b3d338c96435f63bf06d564055c1d3cc0ac - React: 66caa2a8192a35d7ba466a5fdf5dc06ee4a5f6dd - React-callinvoker: e5b55e46894c2dd1bcdc19d4f82b0f7f631d1237 - React-Codegen: 0cf41e00026c5eba61f6bdcabd6e4bf659754f33 - React-Core: 2ce84187a00913f287b96753c56c7819ed7d90d5 - React-CoreModules: 893e7c5eed1ef8fe9e1ee1d913581c946e55b305 - React-cxxreact: 075d98dc664c0e9607cc0c45d41dc052bcc7313b - React-debug: abc6213dcb9eafcf5242cbb194fef4c70c91871f - React-hermes: 133cfa220ef836406f693ed7db56a509032ce433 - React-jsi: 9b45fd040d575f8ae6771bf1960641a58eb0bdd4 - React-jsiexecutor: 45ef2ec6dcde31b90469175ec76ddac77b91dfc3 - React-jsinspector: de0198127395fec3058140a20c045167f761bb16 - React-logger: dc3a2b174d79c2da635059212747d8d929b54e06 - React-NativeModulesApple: c3e696ff867e4bc212266cbdf7e862e48a0166fd - React-perflogger: 43287389ea08993c300897a46f95cfac04bb6c1a - React-RCTActionSheet: 923afe77f9bb89da7c1f98e2730bfc9dde0eed6d - React-RCTAnimation: afd4d94c5e1f731e32ac99800850be06564ac642 - React-RCTAppDelegate: fb2e1447d014557f29e214fe2eb777442f808a3b - React-RCTBlob: 167e2c6c3643f093058c51e76ecc653fc8236033 - React-RCTImage: 867de82a17630a08a3fa64b0cd6677dd19bf6eaf - React-RCTLinking: 885dde8bc5d397c3e72c76315f1f9b5030b3a70e - React-RCTNetwork: efec71102220b96ac8605d0253debd859ca0c817 - React-RCTSettings: 077065d0a4e925b017fe8538afa574d8fb52391f - React-RCTText: 7adddb518ac362b2398fedf0c64105e0dab29441 - React-RCTVibration: de6b7218e415d82788e0965f278dddb2ef88b372 - React-rncore: f0d8c23481a6c263a343fa7fd3816d943754b720 - React-runtimeexecutor: 2b2c09edbca4c9a667428e8c93959f66b3b53140 - React-runtimescheduler: 6ca43e8deadf01ff06b3f01abf8f0e4d508e23c3 - React-utils: 372b83030a74347331636909278bf0a60ec30d59 - ReactCommon: 38824bfffaf4c51fbe03a2730b4fd874ef34d67b - SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - Yoga: d0003f849d2b5224c072cef6568b540d8bb15cd3 - -PODFILE CHECKSUM: cd47dae2e416d877843a4d8b3ca2e076d225258f - -COCOAPODS: 1.15.2 diff --git a/react-native-haapi-module.podspec b/react-native-haapi-module.podspec index 347d12c..a8a2682 100644 --- a/react-native-haapi-module.podspec +++ b/react-native-haapi-module.podspec @@ -16,4 +16,4 @@ Pod::Spec.new do |s| s.dependency "React-Core" s.dependency "IdsvrHaapiSdk", "4.1.4" -end \ No newline at end of file +end From b9b005ffae8fd5f11b9e6ea166ed38ce90843098 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Tue, 1 Oct 2024 15:06:42 +0200 Subject: [PATCH 04/19] Passkeys implementation --- ios/HaapiModule/ConfigurationHelper.swift | 2 +- ios/HaapiModule/EventType.swift | 40 ++++ ios/HaapiModule/HaapiModule.swift | 120 +++++++++--- .../WebAuthnClientOperationHandler.swift | 181 ++++++++++++++++++ 4 files changed, 311 insertions(+), 32 deletions(-) create mode 100644 ios/HaapiModule/EventType.swift create mode 100644 ios/HaapiModule/WebAuthnClientOperationHandler.swift diff --git a/ios/HaapiModule/ConfigurationHelper.swift b/ios/HaapiModule/ConfigurationHelper.swift index 5130ba0..9c4b035 100644 --- a/ios/HaapiModule/ConfigurationHelper.swift +++ b/ios/HaapiModule/ConfigurationHelper.swift @@ -66,4 +66,4 @@ class ConfigurationHelper { .split(separator: " ") .map { String($0) } } -} +} \ No newline at end of file diff --git a/ios/HaapiModule/EventType.swift b/ios/HaapiModule/EventType.swift new file mode 100644 index 0000000..c31e8b4 --- /dev/null +++ b/ios/HaapiModule/EventType.swift @@ -0,0 +1,40 @@ +// +// Copyright (C) 2024 Curity AB. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public enum EventType : String, CaseIterable { + case AuthenticationStep + case AuthenticationSelectorStep + case PollingStep + case PollingStepResult + case ContinueSameStep + case TokenResponse + case TokenResponseError + case HaapiError + case SessionTimedOut + case IncorrectCredentials + case StopPolling + case ProblemRepresentation + case WebAuthnAuthenticationStep + case WebAuthnUserCancelled + case WebAuthnAuthenticationFailed + case WebAuthnRegistrationFailed + case WebAuthnRegistrationFailedKeyRegistered + case HaapiLoading + case HaapiFinishedLoading + case UnknownResponse +} diff --git a/ios/HaapiModule/HaapiModule.swift b/ios/HaapiModule/HaapiModule.swift index 8dd9c45..fff694c 100644 --- a/ios/HaapiModule/HaapiModule.swift +++ b/ios/HaapiModule/HaapiModule.swift @@ -16,6 +16,7 @@ import Foundation import IdsvrHaapiSdk +import OSLog @objc(HaapiModule) class HaapiModule: RCTEventEmitter { @@ -24,17 +25,22 @@ class HaapiModule: RCTEventEmitter { var resolve: RCTPromiseResolveBlock var reject: RCTPromiseRejectBlock } - + + private var haapiManager: HaapiManager? + private var oauthTokenManager: OAuthTokenManager? + enum InitializationError: Error { case moduleNotLoaded(msg: String = "Configuration not created. Run load() on the module before starting a flow") case failedToCreateHaapiManager(_ rootCause: Error) case failedToCreateOAuthTokenManager(_ rootCause: Error) } - + private var currentRepresentation: HaapiRepresentation? private var haapiConfiguration: HaapiConfiguration? - private var jsonDecoder: JSONDecoder = JSONDecoder() - private var jsonEncoder: JSONEncoder = JSONEncoder() + + private let logger = Logger() + private let jsonDecoder: JSONDecoder = JSONDecoder() + private let jsonEncoder: JSONEncoder = JSONEncoder() private var backingHaapiManager: HaapiManager? private var backingTokenManager: OAuthTokenManager? @@ -44,29 +50,29 @@ class HaapiModule: RCTEventEmitter { if (backingHaapiManager != nil) { return backingHaapiManager! } - + if(haapiConfiguration == nil) { throw InitializationError.moduleNotLoaded() } - + do { backingHaapiManager = try HaapiManager(haapiConfiguration: haapiConfiguration!) return backingHaapiManager! - + } catch { throw InitializationError.failedToCreateHaapiManager(error) } } } - + private var oauthTokenManager: OAuthTokenManager { get throws { if backingTokenManager != nil { return backingTokenManager! } - + guard haapiConfiguration != nil else { throw InitializationError.moduleNotLoaded() } - + backingTokenManager = OAuthTokenManager(oauthTokenConfiguration: haapiConfiguration!) return backingTokenManager! } @@ -76,22 +82,7 @@ class HaapiModule: RCTEventEmitter { super.init() HaapiLogger.followUpTags = DriverFollowUpTag.allCases + SdkFollowUpTag.allCases } - - enum EventType : String, CaseIterable { - case AuthenticationStep - case AuthenticationSelectorStep - case PollingStep - case PollingStepResult - case ContinueSameStep - case TokenResponse - case TokenResponseError - case HaapiError - case SessionTimedOut - case IncorrectCredentials - case StopPolling - case ProblemRepresentation - } - + override func supportedEvents() -> [String]! { return EventType.allCases.map { $0.rawValue } } @@ -187,7 +178,7 @@ class HaapiModule: RCTEventEmitter { self.handle(tokenResponse: tokenResponse, promise: promise) }) } catch { - + } } @@ -209,7 +200,6 @@ class HaapiModule: RCTEventEmitter { } private func processHaapiResult(_ haapiResult: HaapiResult, promise: Promise) { - switch haapiResult { case .representation(let representation): do { @@ -237,6 +227,10 @@ class HaapiModule: RCTEventEmitter { private func process(haapiRepresentation: HaapiRepresentation, promise: Promise) throws { switch(haapiRepresentation) { + case let step as WebAuthnAuthenticationClientOperationStep: + handle(webauthnStep: step, promise: promise) + case let step as WebAuthnRegistrationClientOperationStep: + handle(webauthnRegistrationStep: step, promise: promise) case is AuthenticatorSelectorStep: resolveRequest(eventType: EventType.AuthenticationSelectorStep, body: haapiRepresentation, promise: promise) case is InteractiveFormStep: @@ -252,6 +246,62 @@ class HaapiModule: RCTEventEmitter { } } + private func handle(webauthnRegistrationStep: WebAuthnRegistrationClientOperationStep, + promise: Promise) { + logger.debug("Handle webauthn registration step") + // Start passkey registration + if #available(iOS 15.0, *) { + WebAuthnClientOperationHandler().register(operation: webauthnRegistrationStep) { result in + switch result { + case .success(let credential): + guard let credentialOptions = webauthnRegistrationStep.actionModel.platformOptions else { + return self.rejectRequestWithError(description: "Failed to got credential options", promise:promise) + } + + let attestationObject = credential.rawAttestationObject ?? Data() + + let parameters = webauthnRegistrationStep.formattedParametersForRegistration(credentialOptions: credentialOptions, + attestationObject: attestationObject, + rawClientDataJSON: credential.rawClientDataJSON, + credentialID: credential.credentialID) + self.submitModel(model: webauthnRegistrationStep.continueAction.model, parameters: parameters, promise: promise) + case .failure(.userCancelled): + self.logger.debug("User cancelled the webauthn registration dialog") + self.resolveRequest(eventType: EventType.WebAuthnUserCancelled, body: webauthnRegistrationStep, promise: promise) + case .failure(let error): + self.logger.info("WebAuthn registration failed: \(error)") + self.resolveRequest(eventType: EventType.WebAuthnRegistrationFailed, body: webauthnRegistrationStep, promise: promise) + } + } + } else { + rejectRequestWithError(description: "Passkeys are not supported on OS version before 15.0", promise: promise) + } + } + + private func handle(webauthnStep: WebAuthnAuthenticationClientOperationStep, + promise: Promise) { + if #available(iOS 15.0, *) { + WebAuthnClientOperationHandler().authenticate(operation: webauthnStep) { result in + switch result { + case .success(let assertion): + let parameters = webauthnStep.formattedParametersForAssertion(rawAuthenticatorData: assertion.rawAuthenticatorData, + rawClientDataJSON: assertion.rawClientDataJSON, + signature: assertion.signature, + credentialID: assertion.credentialID) + self.submitModel(model: webauthnStep.continueAction.model, parameters: parameters, promise: promise) + case .failure(.userCancelled): + self.logger.debug("User cancelled the authentication dialog") + self.resolveRequest(eventType: EventType.WebAuthnUserCancelled, body: webauthnStep, promise: promise) + case .failure(let error): + self.logger.info("WebAuthn authentication failed: \(error)") + self.resolveRequest(eventType: EventType.WebAuthnAuthenticationFailed, body: webauthnStep, promise: promise) + } + } + } else { + rejectRequestWithError(description: "Passkeys are not supported on OS version before 15.0", promise: promise) + } + } + private func handle(pollingStep: PollingStep, promise: Promise) throws { sendHaapiEvent(EventType.PollingStep, body: pollingStep, promise: promise) @@ -274,13 +324,20 @@ class HaapiModule: RCTEventEmitter { let tokenResponse = SuccessTokenResponse(successfulTokenResponse) resolveRequest(eventType: EventType.TokenResponse, body: tokenResponse, promise: promise) case .errorToken(let errorTokenResponse): - // Request succeeded, but with contents indicating an. Resolve with contents, so that frontend can act on it. + // Request succeeded, but with contents indicating an error. Resolve with contents, so that frontend can act on it. resolveRequest(eventType: EventType.TokenResponseError, body: errorTokenResponse, promise: promise) case .error: rejectRequestWithError(description: "Failed to execute token request", promise: promise) } } + private func submitModel(model: FormActionModel, + parameters: [String: Any] = [:], + promise: Promise) { + haapiManager?.submitForm(model, parameters: parameters, completionHandler: { haapiResult in + self.processHaapiResult(haapiResult, promise: promise) + }) + } private func submitModel(model: FormActionModel, promise: Promise) throws { try haapiManager.submitForm(model, parameters: [:], completionHandler: { haapiResult in @@ -297,6 +354,7 @@ class HaapiModule: RCTEventEmitter { private func sendHaapiEvent(_ type: EventType, body: Codable, promise: Promise) { do { let encodedBody = try encodeObject(body) + logger.debug("Sending event: \(type.rawValue)") self.sendEvent(withName: type.rawValue, body: encodedBody) } catch { @@ -323,8 +381,9 @@ class HaapiModule: RCTEventEmitter { private func resolveRequest(eventType: EventType, body: Codable, promise: Promise) { do { let encodedBody = try encodeObject(body) + promise.resolve(encodedBody) - self.sendEvent(withName: eventType.rawValue, body: encodedBody) + self.sendHaapiEvent(eventType, body: body, promise: promise) } catch { rejectRequestWithError(description: "Could not encode response as json. Error: \(error)", promise: promise) @@ -346,5 +405,4 @@ class HaapiModule: RCTEventEmitter { self.expiresIn = tokenResponse.expiresIn } } - } diff --git a/ios/HaapiModule/WebAuthnClientOperationHandler.swift b/ios/HaapiModule/WebAuthnClientOperationHandler.swift new file mode 100644 index 0000000..d0482d3 --- /dev/null +++ b/ios/HaapiModule/WebAuthnClientOperationHandler.swift @@ -0,0 +1,181 @@ +// +// WebAuthnHandler.swift +// react-native-haapi-module +// +// Created by Daniel Lindau on 2024-09-03. +// + +import Foundation +import AuthenticationServices +import IdsvrHaapiSdk +import OSLog + +protocol WebAuthnRegistrationResponseHandler { + var onSuccess: () -> Void { get } + var onError: (_ error: String) -> Void { get } +} + +public enum WebAuthnClientOperationError : Error { + case missingParameter(message: String) + case failedOperation(message: String) + case userCancelled +} + +@available(iOS 15.0, *) +typealias WebAuthnRegistrationCompletionHandler = (Result) -> Void + +@available(iOS 15.0, *) +typealias WebAuthnAuthenticationCompletionHandler = (Result) -> Void + +@available(iOS 15.0, *) +class WebAuthnClientOperationHandler : NSObject { + private let logger = Logger() + + @available(iOS 15.0, *) + func register(operation: WebAuthnRegistrationClientOperationStep, completionHandler: @escaping WebAuthnRegistrationCompletionHandler) { + var registrationRequest:ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest + do { + registrationRequest = try buildCredentialRegistrationRequest(operation: operation) + } + catch let error { + completionHandler(.failure(WebAuthnClientOperationError.failedOperation(message: error.localizedDescription))) + return + } + + logger.info("Starting passkey registration") + + let authController = ASAuthorizationController(authorizationRequests: [registrationRequest]) + + Task { + do { + guard let credential = try await WebAuthnClientOperationDelegate().performRequestsFor(authorizationController: authController) as? ASAuthorizationPublicKeyCredentialRegistration else { + completionHandler(.failure(WebAuthnClientOperationError.failedOperation(message: "Did not receive a credential after registration request"))) + return + } + completionHandler(.success(credential)) + } catch WebAuthnClientOperationError.userCancelled { + logger.debug("User cancelled the registration") + completionHandler(.failure(WebAuthnClientOperationError.userCancelled)) + } catch { + logger.warning("Failed to perform assertion operation: \(error.localizedDescription)") + completionHandler(.failure(WebAuthnClientOperationError.failedOperation(message: error.localizedDescription))) + } + + } + } + + func authenticate(operation: WebAuthnAuthenticationClientOperationStep, completionHandler: @escaping WebAuthnAuthenticationCompletionHandler) { + var assertionRequest: ASAuthorizationPlatformPublicKeyCredentialAssertionRequest + do { + assertionRequest = try buildAssertionRequest(operation: operation) + } + catch { + completionHandler(.failure(WebAuthnClientOperationError.failedOperation(message: error.localizedDescription))) + return + } + + logger.debug("Starting passkey authentication") + + let authController = ASAuthorizationController(authorizationRequests: [assertionRequest]) + Task { + do { + guard let assertion = try await WebAuthnClientOperationDelegate().performRequestsFor(authorizationController: authController) as? ASAuthorizationPublicKeyCredentialAssertion else { + completionHandler(.failure(WebAuthnClientOperationError.failedOperation(message: "Did not receive an assertion after assertion request"))) + return + } + completionHandler(.success(assertion)) + } catch WebAuthnClientOperationError.userCancelled { + logger.debug("User cancelled the authentication") + completionHandler(.failure(WebAuthnClientOperationError.userCancelled)) + } catch { + logger.warning("Failed to perform assertion operation: \(error.localizedDescription)") + completionHandler(.failure(WebAuthnClientOperationError.failedOperation(message: error.localizedDescription))) + } + + } + } + + private func buildAssertionRequest(operation: WebAuthnAuthenticationClientOperationStep) throws -> ASAuthorizationPlatformPublicKeyCredentialAssertionRequest { + + let credentialOptions = operation.actionModel.credentialOptions + guard let challenge = credentialOptions.challengeData else {throw WebAuthnClientOperationError.missingParameter(message: "Failed to get challenge from server credential request") } + guard let rpId = credentialOptions.relyingPartyId else {throw WebAuthnClientOperationError.missingParameter(message: "Failed to get rpId from server credential request") } + guard let userVerification = credentialOptions.userVerificationPreference else { throw WebAuthnClientOperationError.missingParameter(message: "Failed to get userVerification from server credential request") } + + let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId) + + let request = platformProvider.createCredentialAssertionRequest(challenge: challenge) + request.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference(rawValue: userVerification) + + let allowedCredentials: [ASAuthorizationPlatformPublicKeyCredentialDescriptor] = + credentialOptions.platformAllowCredentials?.map { cred in + return ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: cred.credentialID) + } ?? [] + + request.allowedCredentials = allowedCredentials + + return request + } + + private func buildCredentialRegistrationRequest(operation: WebAuthnRegistrationClientOperationStep) throws -> ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest { + + guard let createCredentialOptions = operation.actionModel.platformOptions else {throw WebAuthnClientOperationError.missingParameter(message: "Failed to get credential request from server") } + guard let challenge = createCredentialOptions.challengeData else {throw WebAuthnClientOperationError.missingParameter(message: "Failed to get challenge from server credential request") } + guard let rpId = createCredentialOptions.relyingPartyId else {throw WebAuthnClientOperationError.missingParameter(message: "Failed to get rpId from server credential request") } + guard let userName = createCredentialOptions.userName else {throw WebAuthnClientOperationError.missingParameter(message: "Failed to get userName from server credential request") } + guard let userId = createCredentialOptions.userIdData else {throw WebAuthnClientOperationError.missingParameter(message: "Failed to get userId from server credential request") } + + let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId) + + return platformProvider.createCredentialRegistrationRequest(challenge: challenge, name: userName, userID: userId) + } + + +} + +@available(iOS 15.0, *) +class WebAuthnClientOperationDelegate : NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { + private let logger = Logger() + private var continuation: CheckedContinuation? + + override init() {super.init()} + + func performRequestsFor(authorizationController: ASAuthorizationController) async throws -> ASAuthorizationCredential { + return try await withCheckedThrowingContinuation { continuation in + self.continuation = continuation + authorizationController.delegate = self + authorizationController.presentationContextProvider = self + authorizationController.performRequests() + } + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + switch authorization.credential { + case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration: + logger.info("A new key was registered: \(credentialRegistration)") + continuation?.resume(returning: credentialRegistration) + break + case let assertion as ASAuthorizationPlatformPublicKeyCredentialAssertion: + logger.debug("A key was used to authenticate: \(assertion)") + continuation?.resume(returning: assertion) + break + default: + logger.warning("Received unknown authorization type") + continuation?.resume(throwing: WebAuthnClientOperationError.failedOperation(message: "Received unknown authorization result")) + } + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + switch ((error as? ASAuthorizationError)?.code) { + case .canceled: + continuation?.resume(throwing: WebAuthnClientOperationError.userCancelled) + break; + default: + continuation?.resume(throwing: WebAuthnClientOperationError.failedOperation(message: error.localizedDescription)) + } + } + + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return UIApplication.shared.windows.first! + } +} From e6074447424f54e90a6d05817c8312759ccac45b Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Tue, 1 Oct 2024 15:50:18 +0200 Subject: [PATCH 05/19] Copyright --- .../haapi/react/webauthn/WebAuthnHandler.kt | 16 ++++++++++++++++ .../WebAuthnClientOperationHandler.swift | 15 ++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/io/curity/haapi/react/webauthn/WebAuthnHandler.kt b/android/src/main/java/io/curity/haapi/react/webauthn/WebAuthnHandler.kt index 0a29ff6..7775a3b 100644 --- a/android/src/main/java/io/curity/haapi/react/webauthn/WebAuthnHandler.kt +++ b/android/src/main/java/io/curity/haapi/react/webauthn/WebAuthnHandler.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2024 Curity AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.curity.haapi.react.webauthn import android.util.Log diff --git a/ios/HaapiModule/WebAuthnClientOperationHandler.swift b/ios/HaapiModule/WebAuthnClientOperationHandler.swift index d0482d3..d116cb1 100644 --- a/ios/HaapiModule/WebAuthnClientOperationHandler.swift +++ b/ios/HaapiModule/WebAuthnClientOperationHandler.swift @@ -1,8 +1,17 @@ // -// WebAuthnHandler.swift -// react-native-haapi-module +// Copyright (C) 2024 Curity AB. // -// Created by Daniel Lindau on 2024-09-03. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // import Foundation From 7de14421ecb7869f5ca20c6c5952f4f63ef0a38a Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Tue, 1 Oct 2024 15:52:56 +0200 Subject: [PATCH 06/19] Note that the iOS module cannot be built standalone --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cff9b4d..175b0d4 100644 --- a/README.md +++ b/README.md @@ -174,8 +174,12 @@ used. * Then in your application, depend on your file using `npm install $path_to_file/react-native-haapi-module/curity-react-native-haapi-module-0.4.2.tgz --save` +### iOS Development + +This module cannot be compiled as it is, instead add a file system dependeny to the example application and open that +workspace. + ## Known limitations - Registration steps no yet supported -- External Browser flow not yet supported -- Webauthn/Passkeys not yet supported \ No newline at end of file +- External Browser flow not yet supported \ No newline at end of file From f728c3239a1613ac7cc5ba059554f8bac918b91b Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 3 Oct 2024 09:29:39 +0200 Subject: [PATCH 07/19] Send is loading events --- ios/HaapiModule/HaapiModule.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ios/HaapiModule/HaapiModule.swift b/ios/HaapiModule/HaapiModule.swift index fff694c..71507e6 100644 --- a/ios/HaapiModule/HaapiModule.swift +++ b/ios/HaapiModule/HaapiModule.swift @@ -117,6 +117,8 @@ class HaapiModule: RCTEventEmitter { closeManagers() } + sendHaapiEvent(EventType.HaapiLoading, body: ["loading": true], promise: promise) + do { try haapiManager.start(completionHandler: { haapiResult in self.processHaapiResult(haapiResult, promise: promise) }) } catch { @@ -140,6 +142,7 @@ class HaapiModule: RCTEventEmitter { do { let actionObject = try JSONSerialization.data(withJSONObject: model) let formActionModel = try jsonDecoder.decode(FormActionModel.self, from: actionObject) + sendHaapiEvent(EventType.HaapiLoading, body: ["loading": true], promise: promise) try haapiManager.submitForm(formActionModel, parameters: parameters, completionHandler: { haapiResult in self.processHaapiResult(haapiResult, promise: promise) }) } catch { rejectRequestWithError(description: "Failed to construct form to submit: \(error)", promise: promise) @@ -173,6 +176,7 @@ class HaapiModule: RCTEventEmitter { resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { let promise = Promise(resolve: resolve, reject: reject) + sendHaapiEvent(EventType.HaapiLoading, body: ["loading": true], promise: promise) do { try oauthTokenManager.refreshAccessToken(with: refreshToken, completionHandler: { tokenResponse in self.handle(tokenResponse: tokenResponse, promise: promise) @@ -344,7 +348,7 @@ class HaapiModule: RCTEventEmitter { self.processHaapiResult(haapiResult, promise: promise) }) } - + private func handle(codeStep: OAuthAuthorizationResponseStep, promise: Promise) throws { try oauthTokenManager.fetchAccessToken(with: codeStep.oauthAuthorizationResponseProperties.code!, dpop: haapiManager.dpop, completionHandler: { tokenResponse in self.handle(tokenResponse: tokenResponse, promise: promise) @@ -374,11 +378,13 @@ class HaapiModule: RCTEventEmitter { private func rejectRequestWithError(description: String, promise: Promise) { sendHaapiEvent(EventType.HaapiError, body: ["error": "HaapiError", "error_description": description], promise: promise) + sendHaapiEvent(EventType.HaapiFinishedLoading, body: ["loading": false], promise: promise) promise.reject("HaapiError", description, nil) closeManagers() } private func resolveRequest(eventType: EventType, body: Codable, promise: Promise) { + sendHaapiEvent(EventType.HaapiFinishedLoading, body: ["loading": false], promise: promise) do { let encodedBody = try encodeObject(body) From c8a166a30231286c2fa0f25e1d4d9621e56d8b09 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 3 Oct 2024 16:51:32 +0200 Subject: [PATCH 08/19] Send event earlier --- .../java/io/curity/haapi/react/HaapiInteractionHandler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/io/curity/haapi/react/HaapiInteractionHandler.kt b/android/src/main/java/io/curity/haapi/react/HaapiInteractionHandler.kt index d58d962..daf0906 100644 --- a/android/src/main/java/io/curity/haapi/react/HaapiInteractionHandler.kt +++ b/android/src/main/java/io/curity/haapi/react/HaapiInteractionHandler.kt @@ -169,11 +169,11 @@ class HaapiInteractionHandler(private val _reactContext: ReactApplicationContext onSuccess: ((TokenResponse) -> Unit)? = null, accessorRequest: suspend (tokenManager: OAuthTokenManager, coroutineContext: CoroutineContext) -> TokenResponse? ) { + _eventEmitter.sendEvent(HaapiLoading) + val manager = _accessorRepository?.accessor?.oAuthTokenManager ?: throw notInitialized() _haapiScope.launch { - _eventEmitter.sendEvent(HaapiLoading) - try { val response = accessorRequest(manager, this.coroutineContext) if (onSuccess != null && response != null) { From f5d4361485b36b8e3bd7d69a71e6498afad11240 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Mon, 14 Oct 2024 14:00:32 +0200 Subject: [PATCH 09/19] Use Map all over --- .../curity/haapi/react/HaapiAccessorRepository.kt | 8 +++----- .../curity/haapi/react/HaapiConfigurationUtil.kt | 14 +++++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/io/curity/haapi/react/HaapiAccessorRepository.kt b/android/src/main/java/io/curity/haapi/react/HaapiAccessorRepository.kt index 7766570..98dd3e9 100644 --- a/android/src/main/java/io/curity/haapi/react/HaapiAccessorRepository.kt +++ b/android/src/main/java/io/curity/haapi/react/HaapiAccessorRepository.kt @@ -17,24 +17,22 @@ package io.curity.haapi.react import android.util.Log import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReadableMap import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import se.curity.identityserver.haapi.android.sdk.HaapiAccessor import se.curity.identityserver.haapi.android.sdk.HaapiAccessorFactory class HaapiAccessorRepository( - conf: ReadableMap, + conf: Map, reactContext: ReactApplicationContext ) { private val _factory: HaapiAccessorFactory private var backingAccessor: HaapiAccessor? = null init { - val confMap = conf.toHashMap() - val haapiConfiguration = HaapiConfigurationUtil.createConfiguration(conf.toHashMap(), reactContext) + val haapiConfiguration = HaapiConfigurationUtil.createConfiguration(conf, reactContext) val factory = HaapiAccessorFactory(haapiConfiguration) - HaapiConfigurationUtil.addFallbackConfiguration(factory, confMap, reactContext) + HaapiConfigurationUtil.addFallbackConfiguration(factory, conf, reactContext) _factory = factory backingAccessor = null } diff --git a/android/src/main/java/io/curity/haapi/react/HaapiConfigurationUtil.kt b/android/src/main/java/io/curity/haapi/react/HaapiConfigurationUtil.kt index 61b2038..f09b050 100644 --- a/android/src/main/java/io/curity/haapi/react/HaapiConfigurationUtil.kt +++ b/android/src/main/java/io/curity/haapi/react/HaapiConfigurationUtil.kt @@ -36,7 +36,7 @@ import javax.net.ssl.X509TrustManager object HaapiConfigurationUtil { - fun createConfiguration(conf: HashMap, reactContext: ReactApplicationContext) = HaapiConfiguration( + fun createConfiguration(conf: Map, reactContext: ReactApplicationContext) = HaapiConfiguration( keyStoreAlias = asStringOrDefault( conf, "keyStoreAlias", "haapi-react-native-android" ), @@ -74,7 +74,7 @@ object HaapiConfigurationUtil { tokenBoundConfiguration = createTokenBoundConfiguration(reactContext) ) - fun addFallbackConfiguration(accessorFactory: HaapiAccessorFactory, conf: HashMap, context: Context) { + fun addFallbackConfiguration(accessorFactory: HaapiAccessorFactory, conf: Map, context: Context) { val registrationEndpoint = asOptionalUri(conf, "registrationEndpointUri") ?: return val fallbackTemplate = asStringOrThrow(conf, "fallback_template_id") val registrationClientSecret = asStringOrThrow(conf, "registration_secret") @@ -95,19 +95,19 @@ object HaapiConfigurationUtil { currentTimeMillisProvider = { System.currentTimeMillis() } ) - private fun asStringMap(conf: HashMap, parameter: String): Map { + private fun asStringMap(conf: Map, parameter: String): Map { return conf[parameter] as? Map ?: mapOf() } - private fun asStringOrDefault(conf: HashMap, parameter: String, defaultValue: String): String = + private fun asStringOrDefault(conf: Map, parameter: String, defaultValue: String): String = asOptionalString(conf, parameter) ?: defaultValue - private fun asStringOrThrow(conf: HashMap, parameter: String): String = + private fun asStringOrThrow(conf: Map, parameter: String): String = asOptionalString(conf, parameter) ?: throw RuntimeException("Missing $parameter in configuration") - private fun asOptionalString(conf: HashMap, parameter: String): String? = conf[parameter] as? String? + private fun asOptionalString(conf: Map, parameter: String): String? = conf[parameter] as? String? - private fun asOptionalUri(conf: HashMap, parameter: String): URI? { + private fun asOptionalUri(conf: Map, parameter: String): URI? { val optionalValue = asOptionalString(conf, parameter) return if (optionalValue != null) URI(optionalValue) else null } From e84cb6e77736384b0848749ab36ce8d8c33d6fb5 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 21 Nov 2024 09:52:00 +0100 Subject: [PATCH 10/19] Adapt to merged changes --- ios/HaapiModule/HaapiModule.swift | 92 +++++++++++++++---------------- 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/ios/HaapiModule/HaapiModule.swift b/ios/HaapiModule/HaapiModule.swift index 71507e6..42fd3a4 100644 --- a/ios/HaapiModule/HaapiModule.swift +++ b/ios/HaapiModule/HaapiModule.swift @@ -20,15 +20,12 @@ import OSLog @objc(HaapiModule) class HaapiModule: RCTEventEmitter { - + struct Promise { var resolve: RCTPromiseResolveBlock var reject: RCTPromiseRejectBlock } - private var haapiManager: HaapiManager? - private var oauthTokenManager: OAuthTokenManager? - enum InitializationError: Error { case moduleNotLoaded(msg: String = "Configuration not created. Run load() on the module before starting a flow") case failedToCreateHaapiManager(_ rootCause: Error) @@ -41,7 +38,7 @@ class HaapiModule: RCTEventEmitter { private let logger = Logger() private let jsonDecoder: JSONDecoder = JSONDecoder() private let jsonEncoder: JSONEncoder = JSONEncoder() - + private var backingHaapiManager: HaapiManager? private var backingTokenManager: OAuthTokenManager? @@ -86,13 +83,13 @@ class HaapiModule: RCTEventEmitter { override func supportedEvents() -> [String]! { return EventType.allCases.map { $0.rawValue } } - + @objc(load:resolver:rejecter:) func load(configuration: Dictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { let promise = Promise(resolve: resolve, reject: reject) - + do { haapiConfiguration = try ConfigurationHelper.createHaapiConfiguration(data: configuration) promise.resolve(true) @@ -100,23 +97,23 @@ class HaapiModule: RCTEventEmitter { rejectRequestWithError(description: "Could not configure module: \(error)", promise: promise) } } - - + + @objc(start:rejecter:) func start(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { - + let promise = Promise(resolve: resolve, reject: reject) - + if(haapiConfiguration == nil) { rejectRequestWithError(description: "Configuration not created. Run load() on the module before starting a flow", promise: promise) return } - + if(backingHaapiManager != nil) { closeManagers() } - + sendHaapiEvent(EventType.HaapiLoading, body: ["loading": true], promise: promise) do { @@ -126,19 +123,19 @@ class HaapiModule: RCTEventEmitter { return } } - + @objc func submitForm(_ action: Dictionary, parameters: Dictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { let promise = Promise(resolve: resolve, reject: reject) - + guard let model = action["model"] else { rejectRequestWithError(description: "", promise: promise) return } - + do { let actionObject = try JSONSerialization.data(withJSONObject: model) let formActionModel = try jsonDecoder.decode(FormActionModel.self, from: actionObject) @@ -148,20 +145,20 @@ class HaapiModule: RCTEventEmitter { rejectRequestWithError(description: "Failed to construct form to submit: \(error)", promise: promise) } } - + @objc func navigate(_ linkMap: Dictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { - + var mutableLinkMap = linkMap // should be optional in SDK if(mutableLinkMap["rel"] == nil) { mutableLinkMap["rel"] = "link" } - + let promise = Promise(resolve: resolve, reject: reject) - + do { let linkObject = try JSONSerialization.data(withJSONObject: mutableLinkMap) let link = try jsonDecoder.decode(Link.self, from: linkObject) @@ -170,7 +167,7 @@ class HaapiModule: RCTEventEmitter { rejectRequestWithError(description: "Failed to construct link: \(error)", promise: promise) } } - + @objc func refreshAccessToken(_ refreshToken: String, resolver resolve: @escaping RCTPromiseResolveBlock, @@ -185,24 +182,24 @@ class HaapiModule: RCTEventEmitter { } } - + @objc(logout:rejecter:) func logout(resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) { closeManagers() resolve(true) } - + override static func requiresMainQueueSetup() -> Bool { return true } - + private func closeManagers() { backingHaapiManager?.close() backingHaapiManager = nil backingTokenManager = nil } - + private func processHaapiResult(_ haapiResult: HaapiResult, promise: Promise) { switch haapiResult { case .representation(let representation): @@ -217,7 +214,7 @@ class HaapiModule: RCTEventEmitter { rejectRequestWithError(description: "Unknown error: " + error.localizedDescription, promise: promise) } } - + private func process(problemRepresentation: ProblemRepresentation, promise: Promise) { switch(problemRepresentation.type) { case .incorrectCredentialsProblem: @@ -228,7 +225,7 @@ class HaapiModule: RCTEventEmitter { resolveRequest(eventType: EventType.ProblemRepresentation, body: problemRepresentation, promise: promise) } } - + private func process(haapiRepresentation: HaapiRepresentation, promise: Promise) throws { switch(haapiRepresentation) { case let step as WebAuthnAuthenticationClientOperationStep: @@ -249,7 +246,7 @@ class HaapiModule: RCTEventEmitter { rejectRequestWithError(description: "Unknown step", promise: promise) } } - + private func handle(webauthnRegistrationStep: WebAuthnRegistrationClientOperationStep, promise: Promise) { logger.debug("Handle webauthn registration step") @@ -309,19 +306,19 @@ class HaapiModule: RCTEventEmitter { private func handle(pollingStep: PollingStep, promise: Promise) throws { sendHaapiEvent(EventType.PollingStep, body: pollingStep, promise: promise) - + switch(pollingStep.pollingProperties.status) { case .pending: resolveRequest(eventType: EventType.PollingStepResult, body: pollingStep, promise: promise) case .failed: sendHaapiEvent(EventType.StopPolling, body: pollingStep, promise: promise) - try submitModel(model: pollingStep.mainAction.model, promise: promise) + submitModel(model: pollingStep.mainAction.model, promise: promise) case .done: sendHaapiEvent(EventType.StopPolling, body: pollingStep, promise: promise) - try submitModel(model: pollingStep.mainAction.model, promise: promise) + submitModel(model: pollingStep.mainAction.model, promise: promise) } } - + private func handle(tokenResponse: TokenResponse, promise: Promise) { switch(tokenResponse) { case .successfulToken(let successfulTokenResponse): @@ -334,19 +331,18 @@ class HaapiModule: RCTEventEmitter { rejectRequestWithError(description: "Failed to execute token request", promise: promise) } } - + private func submitModel(model: FormActionModel, parameters: [String: Any] = [:], promise: Promise) { - haapiManager?.submitForm(model, parameters: parameters, completionHandler: { haapiResult in - self.processHaapiResult(haapiResult, promise: promise) - }) - } - private func submitModel(model: FormActionModel, - promise: Promise) throws { - try haapiManager.submitForm(model, parameters: [:], completionHandler: { haapiResult in - self.processHaapiResult(haapiResult, promise: promise) - }) + do { + try haapiManager.submitForm(model, parameters: parameters, completionHandler: { haapiResult in + self.processHaapiResult(haapiResult, promise: promise) + }) + } + catch { + rejectRequestWithError(description: "Failed to submit model", promise: promise) + } } private func handle(codeStep: OAuthAuthorizationResponseStep, promise: Promise) throws { @@ -354,7 +350,7 @@ class HaapiModule: RCTEventEmitter { self.handle(tokenResponse: tokenResponse, promise: promise) }) } - + private func sendHaapiEvent(_ type: EventType, body: Codable, promise: Promise) { do { let encodedBody = try encodeObject(body) @@ -365,7 +361,7 @@ class HaapiModule: RCTEventEmitter { rejectRequestWithError(description: "Could not encode event as json. Error: \(error)", promise: promise) } } - + private func encodeObject(_ object: Codable) throws -> Any { do { let jsonData = try jsonEncoder.encode(object) @@ -375,14 +371,14 @@ class HaapiModule: RCTEventEmitter { throw NSError() } } - + private func rejectRequestWithError(description: String, promise: Promise) { sendHaapiEvent(EventType.HaapiError, body: ["error": "HaapiError", "error_description": description], promise: promise) sendHaapiEvent(EventType.HaapiFinishedLoading, body: ["loading": false], promise: promise) promise.reject("HaapiError", description, nil) closeManagers() } - + private func resolveRequest(eventType: EventType, body: Codable, promise: Promise) { sendHaapiEvent(EventType.HaapiFinishedLoading, body: ["loading": false], promise: promise) do { @@ -395,14 +391,14 @@ class HaapiModule: RCTEventEmitter { rejectRequestWithError(description: "Could not encode response as json. Error: \(error)", promise: promise) } } - + private struct SuccessTokenResponse : Codable { var accessToken: String var refreshToken: String? var idToken: String? var scope: String var expiresIn: Int - + init(_ tokenResponse : SuccessfulTokenResponse) { self.accessToken = tokenResponse.accessToken self.idToken = tokenResponse.idToken From a82877180ec3b32b225dda205943b4f9b63457bd Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 21 Nov 2024 10:08:20 +0100 Subject: [PATCH 11/19] Add LoggedOut event to match android --- ios/HaapiModule/EventType.swift | 1 + ios/HaapiModule/HaapiModule.swift | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ios/HaapiModule/EventType.swift b/ios/HaapiModule/EventType.swift index c31e8b4..8d2cfe5 100644 --- a/ios/HaapiModule/EventType.swift +++ b/ios/HaapiModule/EventType.swift @@ -36,5 +36,6 @@ public enum EventType : String, CaseIterable { case WebAuthnRegistrationFailedKeyRegistered case HaapiLoading case HaapiFinishedLoading + case LoggedOut case UnknownResponse } diff --git a/ios/HaapiModule/HaapiModule.swift b/ios/HaapiModule/HaapiModule.swift index 42fd3a4..8d6100d 100644 --- a/ios/HaapiModule/HaapiModule.swift +++ b/ios/HaapiModule/HaapiModule.swift @@ -184,10 +184,10 @@ class HaapiModule: RCTEventEmitter { } @objc(logout:rejecter:) - func logout(resolver resolve: RCTPromiseResolveBlock, - rejecter reject: RCTPromiseRejectBlock) { + func logout(resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { closeManagers() - resolve(true) + resolveRequest(eventType: EventType.LoggedOut, body: ["loggedout": true], promise: Promise(resolve: resolve, reject: reject)) } override static func requiresMainQueueSetup() -> Bool { From bb168131693457ac8452b922a91e55e1492ba01b Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 21 Nov 2024 10:08:33 +0100 Subject: [PATCH 12/19] Rename HaapiException to HaapiError to match ios --- android/src/main/java/io/curity/haapi/react/HaapiModule.kt | 2 +- android/src/main/java/io/curity/haapi/react/events/EventType.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/io/curity/haapi/react/HaapiModule.kt b/android/src/main/java/io/curity/haapi/react/HaapiModule.kt index a90d8c0..5bc5d0b 100644 --- a/android/src/main/java/io/curity/haapi/react/HaapiModule.kt +++ b/android/src/main/java/io/curity/haapi/react/HaapiModule.kt @@ -332,7 +332,7 @@ class HaapiModule(private val _reactContext: ReactApplicationContext) : ReactCon Log.d(TAG, "Rejecting request using with description '$errorDescription'") - _eventEmitter.sendEvent(HaapiException, JsonUtil.toJsonString(jsonMap)) + _eventEmitter.sendEvent(HaapiError, JsonUtil.toJsonString(jsonMap)) promise.reject(exception, mapToNativeMap(jsonMap)) } diff --git a/android/src/main/java/io/curity/haapi/react/events/EventType.kt b/android/src/main/java/io/curity/haapi/react/events/EventType.kt index 23ffaa9..b302149 100644 --- a/android/src/main/java/io/curity/haapi/react/events/EventType.kt +++ b/android/src/main/java/io/curity/haapi/react/events/EventType.kt @@ -30,7 +30,7 @@ enum class EventType { SessionTimedOut, PollingStep, StopPolling, - HaapiException, + HaapiError, PollingStepResult, ProblemRepresentation, LoggedOut, From f9b7e991f8838f8c9ea9986154812d34075bca19 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 21 Nov 2024 10:28:48 +0100 Subject: [PATCH 13/19] New events docs --- README.md | 119 ++++++++---------- .../io/curity/haapi/react/events/EventType.kt | 6 +- ios/HaapiModule/HaapiModule.swift | 2 +- 3 files changed, 57 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 175b0d4..0afae53 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ # react-native-haapi-module -[![Quality](https://img.shields.io/badge/quality-test-yellow)](https://curity.io/resources/code-examples/status/) -[![Availability](https://img.shields.io/badge/availability-source-blue)](https://curity.io/resources/code-examples/status/) +[![Quality](https://img.shields.io/badge/quality-test-yellow)](https://curity.io/resources/code-examples/status/) [![Availability](https://img.shields.io/badge/availability-source-blue)](https://curity.io/resources/code-examples/status/) -This a react-native Native Module that use the Hypermedia Authentication API of the Curity Identity Server. The module -utilizes the iOS and Android SDK to perform attestation and communication with the API. +This a react-native Native Module that use the Hypermedia Authentication API of the Curity Identity Server. The module utilizes the iOS and Android SDK to perform attestation and communication with the API. -https://curity.io/product/authentication-service/authentication-api/ + ## Getting started @@ -14,29 +12,28 @@ https://curity.io/product/authentication-service/authentication-api/ ## Configuration -| Parameter Name | Platform | Required | Default | Description | -|----------------------------|----------|----------|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `appRedirect` | both | false | `app:start` | Redirect URI to use in OAuth requests. Needs to be registered in server config | -| `keyStoreAlias` | android | false | `haapi-react-native-android` | Keystore alias for keys used in an authentication flow. Only used on Android | -| `configurationName` | ios | false | `HaapiModule` | The name to use for the configuration on iOS. If you are in testing mode and switching environments, make sure that each environment sets a different name | -| `clientId` | both | true | | The registered `client_id` | -| `baseUri` | both | true | | Base URI of the server. Used for relative redirects. | -| `tokenEndpointUri` | both | true | | URI of the token endpoint. | -| `authorizationEndpointUri` | both | true | | URI of the authorize endpoint. | -| `revocationEndpointUri` | both | true | | URI of the revocation endpoint. | -| `registrationEndpointUri` | android | false | | URI of the registration endpoint. Required if fallback registration should be used. | -| `fallback_template_id` | android | false | | Name of the template client to be used in fallback. Required if fallback registration should be used. | -| `registration_secret` | android | false | | Name of the template client to be used in fallback. Required if fallback registration should be used. | -| `validateTlsCertificate` | both | false | true | If the server TLS certificate should be validated. Set to `false` to accept self signed certificates. | -| `acrValues` | both | false | `""` | Space separated string to send in authorize request. | -| `scope` | both | false | `""` | Space separated string of scopes to request. | -| `extraRequestParameters` | both | false | `{}` | Map of extra parameters to send in the request to the authorize endpoint. | -| `extraHttpHeaders` | both | false | `{}` | Map of extra http headers to send in all requests to the authentication API. | +Parameter Name | Platform | Required | Default | Description +-------------------------- | -------- | -------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- +`appRedirect` | both | false | `app:start` | Redirect URI to use in OAuth requests. Needs to be registered in server config +`keyStoreAlias` | android | false | `haapi-react-native-android` | Keystore alias for keys used in an authentication flow. Only used on Android +`configurationName` | ios | false | `HaapiModule` | The name to use for the configuration on iOS. If you are in testing mode and switching environments, make sure that each environment sets a different name +`clientId` | both | true | | The registered `client_id` +`baseUri` | both | true | | Base URI of the server. Used for relative redirects. +`tokenEndpointUri` | both | true | | URI of the token endpoint. +`authorizationEndpointUri` | both | true | | URI of the authorize endpoint. +`revocationEndpointUri` | both | true | | URI of the revocation endpoint. +`registrationEndpointUri` | android | false | | URI of the registration endpoint. Required if fallback registration should be used. +`fallback_template_id` | android | false | | Name of the template client to be used in fallback. Required if fallback registration should be used. +`registration_secret` | android | false | | Name of the template client to be used in fallback. Required if fallback registration should be used. +`validateTlsCertificate` | both | false | true | If the server TLS certificate should be validated. Set to `false` to accept self signed certificates. +`acrValues` | both | false | `""` | Space separated string to send in authorize request. +`scope` | both | false | `""` | Space separated string of scopes to request. +`extraRequestParameters` | both | false | `{}` | Map of extra parameters to send in the request to the authorize endpoint. +`extraHttpHeaders` | both | false | `{}` | Map of extra http headers to send in all requests to the authentication API. ## Usage -All functions of the module are async operations. The application may use events produced by the module to drive the -authentication flow, or rely on results return by promises. +All functions of the module are async operations. The application may use events produced by the module to drive the authentication flow, or rely on results return by promises. ### Load @@ -73,15 +70,11 @@ HaapiModule.load(HaapiConfiguration).catch(e => { export default HaapiModule; ``` -`load()` may be called multiple times with different configuration, to be able to start authentication flows requesting -different `acr` or `scope`. +`load()` may be called multiple times with different configuration, to be able to start authentication flows requesting different `acr` or `scope`. ## Start -After the module has been loaded, the `start()` function may be called. `start()` will setup the communication with -HAAPI, perform attestation, and then start emitting events for the application to react on. Receiving events will allow -the application to know more about the contents of the current state than if it were to receive the raw HaapiResponse. -The module will follow redirect responses automatically. +After the module has been loaded, the `start()` function may be called. `start()` will setup the communication with HAAPI, perform attestation, and then start emitting events for the application to react on. Receiving events will allow the application to know more about the contents of the current state than if it were to receive the raw HaapiResponse. The module will follow redirect responses automatically. ```javascript try { @@ -102,8 +95,7 @@ eventEmitter.addListener("EventName", () => { ## Navigate -To follow a link in a HAAPI response, the `navigate(model)` function can be used. `model` is an object conforming -to [Link](https://curity.io/docs/haapi-data-model/latest/links.html) +To follow a link in a HAAPI response, the `navigate(model)` function can be used. `model` is an object conforming to [Link](https://curity.io/docs/haapi-data-model/latest/links.html) ```javascript try { @@ -115,8 +107,7 @@ try { ## Submit form -To submit a form in an action, use the submitForm(action, parameters), where `action` is the form to submit, -and `parameters` is an object containing the field names and the values to fill the form. +To submit a form in an action, use the submitForm(action, parameters), where `action` is the form to submit, and `parameters` is an object containing the field names and the values to fill the form. ```javascript try { @@ -128,8 +119,7 @@ try { ## Refresh Access Token -Refresh the access token using the refresh token. The application may listen to the -events `TokenResponse`/`TokenResponseError` for the result of the refresh. +Refresh the access token using the refresh token. The application may listen to the events `TokenResponse`/`TokenResponseError` for the result of the refresh. ```javascript HaapiModule.refreshAccessToken(refreshToken); @@ -145,41 +135,38 @@ HaapiModule.logout().then(/* Remove tokens from state */); ## Events -| Event Name | Emitted when | -|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| AuthenticationStep | An action is required by the user as part of authentication. See [Authentication Step](https://curity.io/docs/haapi-data-model/latest/authentication-step.html) | -| AuthenticationSelectorStep | An `AuthenticationStep` with the kind `authenticator-selector` is received. An authenticator selector screen should be be shown to the user. | -| ContinueSameStep | A screen should be shown to the user, containing some information. The only required action by the user is to accept or in some cases cancel. [Continue Same Step](https://curity.io/docs/haapi-data-model/latest/continue-same-step.html) | -| PollingStep | An authentication step that requires polling was received. May contain information for the user for how to proceed authentication out of band. [Polling Step](https://curity.io/docs/haapi-data-model/latest/polling-step.html) | -| PollingStepResult | A poll result was received with the `status` `PENDING`. The application may show new information to the user and continue polling. | -| StopPolling | A successful poll result was received. Application should stop polling, and the module will continue execution and may issue new events. | -| TokenResponse | Authentication was successful, and the resulting token(s) was received. The payload of the event will contain `accessToken`, `expiresIn` and `scope`. May contain `refreshToken` and `idToken` | -| TokenResponseError | Authentication was successful, but the token request returned an error. | -| SessionTimedOut | The authentication process took too long, and timed out. The user will have to start over using `start()` method again. | -| IncorrectCredentials | The user enter wrong credentials in an `AuthenticationStep`. Show an error to the user and allow them to try again. [Invalid Input Problem](https://curity.io/docs/haapi-data-model/latest/invalid-input-problem.html) | -| ProblemRepresentation | The server returned an unexpected problem. [Problem](https://curity.io/docs/haapi-data-model/latest/problem.html) | -| HaapiError | An unexpected problem happened. Event will have members `error` and `error_description` | +Event Name | Emitted when +--------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +AuthenticationStep | An action is required by the user as part of authentication. See [Authentication Step](https://curity.io/docs/haapi-data-model/latest/authentication-step.html) +AuthenticationSelectorStep | An `AuthenticationStep` with the kind `authenticator-selector` is received. An authenticator selector screen should be be shown to the user. +ContinueSameStep | A screen should be shown to the user, containing some information. The only required action by the user is to accept or in some cases cancel. [Continue Same Step](https://curity.io/docs/haapi-data-model/latest/continue-same-step.html) +PollingStep | An authentication step that requires polling was received. May contain information for the user for how to proceed authentication out of band. [Polling Step](https://curity.io/docs/haapi-data-model/latest/polling-step.html) +PollingStepResult | A poll result was received with the `status` `PENDING`. The application may show new information to the user and continue polling. +StopPolling | A successful poll result was received. Application should stop polling, and the module will continue execution and may issue new events. +TokenResponse | Authentication was successful, and the resulting token(s) was received. The payload of the event will contain `accessToken`, `expiresIn` and `scope`. May contain `refreshToken` and `idToken` +TokenResponseError | Authentication was successful, but the token request returned an error. +SessionTimedOut | The authentication process took too long, and timed out. The user will have to start over using `start()` method again. +IncorrectCredentials | The user enter wrong credentials in an `AuthenticationStep`. Show an error to the user and allow them to try again. [Invalid Input Problem](https://curity.io/docs/haapi-data-model/latest/invalid-input-problem.html) +ProblemRepresentation | The server returned an unexpected problem. [Problem](https://curity.io/docs/haapi-data-model/latest/problem.html) +HaapiError | An unexpected problem happened. Event will have members `error` and `error_description` +RegistrationStep | Registration is expected of the user. See [Registration Step](https://curity.io/docs/haapi-data-model/latest/registration-step.html) +UnkownResponse | Server returned a response that is not supported by the module +HaapiLoading | The module has started a request and is waiting on a response +HaapiFinishedLoading | The module received response and finished processing +LoggedOut | The module finished the logout +WebAuthnAuthenticationStep | Current authentication step is a webauthn/passkeys step. The module will perform a client operation to ask the user to authenticate on their device. The full step is provided to the client to be able to show an appropriate screen. [Login with WebAuthn](https://curity.io/docs/haapi-data-model/latest/webauthn-authentication-step.html) +WebAuthnUserCancelled | User cancelled the authentication request. App should show appropriate screens for how to proceed +WebAuthnRegistrationFailed | Registration of a webauthn device failed +WebAuthnRegistrationFailedKeyRegistered | Registration of a webauthn device failed. Reason is likely because the key is already registered. User should proceed to authenticate using the key. ## Example implementation -See for example implementation in javascript which is mostly -driven by events. +See for example implementation in javascript which is mostly driven by events. ## Development -To deploy changes in the modules to an application without publishing a new package, a file system dependency may be -used. - -* Pack your module with: `npm pack`. This will provide a `.tgz` file containing the module. -* Then in your application, depend on your file - using `npm install $path_to_file/react-native-haapi-module/curity-react-native-haapi-module-0.4.2.tgz --save` - -### iOS Development - -This module cannot be compiled as it is, instead add a file system dependeny to the example application and open that -workspace. +This module cannot be compiled as it is, instead add a file system dependency to the example application and open that workspace. See the [example repository](https://github.com/curityio/react-native-haapi-example) for instructions. ## Known limitations -- Registration steps no yet supported -- External Browser flow not yet supported \ No newline at end of file +- External Browser flow not yet supported diff --git a/android/src/main/java/io/curity/haapi/react/events/EventType.kt b/android/src/main/java/io/curity/haapi/react/events/EventType.kt index b302149..a216f54 100644 --- a/android/src/main/java/io/curity/haapi/react/events/EventType.kt +++ b/android/src/main/java/io/curity/haapi/react/events/EventType.kt @@ -17,8 +17,6 @@ package io.curity.haapi.react.events enum class EventType { - WebAuthnAuthenticationStep, - WebAuthnUserCancelled, AuthenticationSelectorStep, ContinueSameStep, AuthenticationStep, @@ -37,5 +35,7 @@ enum class EventType { HaapiLoading, HaapiFinishedLoading, WebAuthnRegistrationFailed, - WebAuthnRegistrationFailedKeyRegistered + WebAuthnRegistrationFailedKeyRegistered, + WebAuthnAuthenticationStep, + WebAuthnUserCancelled, } \ No newline at end of file diff --git a/ios/HaapiModule/HaapiModule.swift b/ios/HaapiModule/HaapiModule.swift index 8d6100d..5d4ef85 100644 --- a/ios/HaapiModule/HaapiModule.swift +++ b/ios/HaapiModule/HaapiModule.swift @@ -407,4 +407,4 @@ class HaapiModule: RCTEventEmitter { self.expiresIn = tokenResponse.expiresIn } } -} +} \ No newline at end of file From 9aa596855a45ed5429b090cf80f3cbca78db0774 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 21 Nov 2024 10:30:27 +0100 Subject: [PATCH 14/19] Reject request when refresh fails --- ios/HaapiModule/HaapiModule.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/HaapiModule/HaapiModule.swift b/ios/HaapiModule/HaapiModule.swift index 5d4ef85..1f1f8db 100644 --- a/ios/HaapiModule/HaapiModule.swift +++ b/ios/HaapiModule/HaapiModule.swift @@ -179,7 +179,7 @@ class HaapiModule: RCTEventEmitter { self.handle(tokenResponse: tokenResponse, promise: promise) }) } catch { - + rejectRequestWithError(description: "Could not refresh access token: \(error)", promise: promise) } } @@ -407,4 +407,4 @@ class HaapiModule: RCTEventEmitter { self.expiresIn = tokenResponse.expiresIn } } -} \ No newline at end of file +} From c519a9ea7dcfebcd1b925942545400fe74ee7986 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 21 Nov 2024 12:00:30 +0100 Subject: [PATCH 15/19] Only send pollresult on pending requests --- android/src/main/java/io/curity/haapi/react/HaapiModule.kt | 7 +++++-- ios/HaapiModule/HaapiModule.swift | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/io/curity/haapi/react/HaapiModule.kt b/android/src/main/java/io/curity/haapi/react/HaapiModule.kt index 5bc5d0b..861b851 100644 --- a/android/src/main/java/io/curity/haapi/react/HaapiModule.kt +++ b/android/src/main/java/io/curity/haapi/react/HaapiModule.kt @@ -310,10 +310,13 @@ class HaapiModule(private val _reactContext: ReactApplicationContext) : ReactCon } private fun handlePollingStep(pollingStep: PollingStep, promise: Promise) { - _eventEmitter.sendEvent(PollingStep, pollingStep.toJsonString()) when (pollingStep.properties.status) { - PollingStatus.PENDING -> resolveRequest(PollingStepResult, pollingStep.toJsonString(), promise) + PollingStatus.PENDING -> { + _eventEmitter.sendEvent(PollingStep, pollingStep.toJsonString()) + resolveRequest(PollingStepResult, pollingStep.toJsonString(), promise) + } + PollingStatus.FAILED -> { _eventEmitter.sendEvent(StopPolling, "{}") submitModel(pollingStep.mainAction.model, emptyMap(), promise) diff --git a/ios/HaapiModule/HaapiModule.swift b/ios/HaapiModule/HaapiModule.swift index 1f1f8db..2b25285 100644 --- a/ios/HaapiModule/HaapiModule.swift +++ b/ios/HaapiModule/HaapiModule.swift @@ -305,10 +305,10 @@ class HaapiModule: RCTEventEmitter { private func handle(pollingStep: PollingStep, promise: Promise) throws { - sendHaapiEvent(EventType.PollingStep, body: pollingStep, promise: promise) switch(pollingStep.pollingProperties.status) { case .pending: + sendHaapiEvent(EventType.PollingStep, body: pollingStep, promise: promise) resolveRequest(eventType: EventType.PollingStepResult, body: pollingStep, promise: promise) case .failed: sendHaapiEvent(EventType.StopPolling, body: pollingStep, promise: promise) From 565bedc93f440d188625c9f97522942af5e83220 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 21 Nov 2024 15:59:25 +0100 Subject: [PATCH 16/19] Doc update --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0afae53..3ee258c 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ eventEmitter.addListener("EventName", () => { }); ``` +Since `start()` will start an authentication flow, it's recommended to only call it when a user performs an action to start the login. + ## Navigate To follow a link in a HAAPI response, the `navigate(model)` function can be used. `model` is an object conforming to [Link](https://curity.io/docs/haapi-data-model/latest/links.html) @@ -169,4 +171,5 @@ This module cannot be compiled as it is, instead add a file system dependency to ## Known limitations +- Registration steps are not yet fully supported - External Browser flow not yet supported From 81b6d7a3be08f0bfb82ab570fec67b129c0e5229 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 21 Nov 2024 16:16:38 +0100 Subject: [PATCH 17/19] Use same sdk version as ios --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 8e9367b..8988cef 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -47,7 +47,7 @@ def safeExtGet(prop, fallback) { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22" - implementation "se.curity.identityserver:identityserver.haapi.android.sdk:4.2.0" + implementation "se.curity.identityserver:identityserver.haapi.android.sdk:4.1.0" implementation "com.google.code.gson:gson:2.10" implementation "com.facebook.react:react-android:${safeExtGet('reactNativeVersion', '+')}" implementation "androidx.credentials:credentials:1.2.2" From 49b215df99eda67b004b11580f95606931d43baa Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 21 Nov 2024 16:40:11 +0100 Subject: [PATCH 18/19] Remove project files, since it shouldn't be developed from here --- ios/HaapiModule.xcodeproj/project.pbxproj | 354 ------------------ .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - 3 files changed, 372 deletions(-) delete mode 100644 ios/HaapiModule.xcodeproj/project.pbxproj delete mode 100644 ios/HaapiModule.xcworkspace/contents.xcworkspacedata delete mode 100644 ios/HaapiModule.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/ios/HaapiModule.xcodeproj/project.pbxproj b/ios/HaapiModule.xcodeproj/project.pbxproj deleted file mode 100644 index 654c626..0000000 --- a/ios/HaapiModule.xcodeproj/project.pbxproj +++ /dev/null @@ -1,354 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 56; - objects = { - -/* Begin PBXBuildFile section */ - 6571695A2B8E230C00A1B372 /* TrustAllCertsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657169592B8E230C00A1B372 /* TrustAllCertsDelegate.swift */; }; - 65B6F8682B8CDC5500AC54A9 /* HaapiModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B6F8652B8CDC5500AC54A9 /* HaapiModule.swift */; }; - 65B6F8692B8CDC5500AC54A9 /* HaapiModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B6F8662B8CDC5500AC54A9 /* HaapiModule.m */; }; - 65B6F86A2B8CDC5500AC54A9 /* ConfigurationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65B6F8672B8CDC5500AC54A9 /* ConfigurationHelper.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 65B6F83D2B8CD91800AC54A9 /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "include/$(PRODUCT_NAME)"; - dstSubfolderSpec = 16; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 657169592B8E230C00A1B372 /* TrustAllCertsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustAllCertsDelegate.swift; sourceTree = ""; }; - 65B6F83F2B8CD91800AC54A9 /* libHaapiModule.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libHaapiModule.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 65B6F8642B8CDC5500AC54A9 /* HaapiModule-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "HaapiModule-Bridging-Header.h"; sourceTree = ""; }; - 65B6F8652B8CDC5500AC54A9 /* HaapiModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HaapiModule.swift; sourceTree = ""; }; - 65B6F8662B8CDC5500AC54A9 /* HaapiModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HaapiModule.m; sourceTree = ""; }; - 65B6F8672B8CDC5500AC54A9 /* ConfigurationHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationHelper.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 65B6F83C2B8CD91800AC54A9 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 5D8522AD620596F14AA4AAE0 /* Pods */ = { - isa = PBXGroup; - children = ( - ); - path = Pods; - sourceTree = ""; - }; - 65B6F8362B8CD91800AC54A9 = { - isa = PBXGroup; - children = ( - 65B6F8632B8CDC5500AC54A9 /* HaapiModule */, - 65B6F8402B8CD91800AC54A9 /* Products */, - 5D8522AD620596F14AA4AAE0 /* Pods */, - ); - sourceTree = ""; - }; - 65B6F8402B8CD91800AC54A9 /* Products */ = { - isa = PBXGroup; - children = ( - 65B6F83F2B8CD91800AC54A9 /* libHaapiModule.a */, - ); - name = Products; - sourceTree = ""; - }; - 65B6F8632B8CDC5500AC54A9 /* HaapiModule */ = { - isa = PBXGroup; - children = ( - 65B6F8642B8CDC5500AC54A9 /* HaapiModule-Bridging-Header.h */, - 65B6F8652B8CDC5500AC54A9 /* HaapiModule.swift */, - 65B6F8662B8CDC5500AC54A9 /* HaapiModule.m */, - 65B6F8672B8CDC5500AC54A9 /* ConfigurationHelper.swift */, - 657169592B8E230C00A1B372 /* TrustAllCertsDelegate.swift */, - ); - path = HaapiModule; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 65B6F83E2B8CD91800AC54A9 /* HaapiModule */ = { - isa = PBXNativeTarget; - buildConfigurationList = 65B6F8462B8CD91800AC54A9 /* Build configuration list for PBXNativeTarget "HaapiModule" */; - buildPhases = ( - 65B6F83B2B8CD91800AC54A9 /* Sources */, - 65B6F83C2B8CD91800AC54A9 /* Frameworks */, - 65B6F83D2B8CD91800AC54A9 /* CopyFiles */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = HaapiModule; - productName = HaapiModule; - productReference = 65B6F83F2B8CD91800AC54A9 /* libHaapiModule.a */; - productType = "com.apple.product-type.library.static"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 65B6F8372B8CD91800AC54A9 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1500; - LastUpgradeCheck = 1500; - TargetAttributes = { - 65B6F83E2B8CD91800AC54A9 = { - CreatedOnToolsVersion = 15.0.1; - LastSwiftMigration = 1500; - }; - }; - }; - buildConfigurationList = 65B6F83A2B8CD91800AC54A9 /* Build configuration list for PBXProject "HaapiModule" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 65B6F8362B8CD91800AC54A9; - productRefGroup = 65B6F8402B8CD91800AC54A9 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 65B6F83E2B8CD91800AC54A9 /* HaapiModule */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXSourcesBuildPhase section */ - 65B6F83B2B8CD91800AC54A9 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 65B6F86A2B8CDC5500AC54A9 /* ConfigurationHelper.swift in Sources */, - 65B6F8682B8CDC5500AC54A9 /* HaapiModule.swift in Sources */, - 65B6F8692B8CDC5500AC54A9 /* HaapiModule.m in Sources */, - 6571695A2B8E230C00A1B372 /* TrustAllCertsDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 65B6F8442B8CD91800AC54A9 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "c++17"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION, - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - OTHER_CFLAGS = "$(inherited)"; - OTHER_CPLUSPLUSFLAGS = "$(inherited)"; - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); - REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OBJC_BRIDGING_HEADER = "HaapiModule/HaapiModule-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - USE_HERMES = true; - }; - name = Debug; - }; - 65B6F8452B8CD91800AC54A9 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "c++17"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_NO_COMMON_BLOCKS = YES; - GCC_PREPROCESSOR_DEFINITIONS = ( - "$(inherited)", - _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION, - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - OTHER_CFLAGS = "$(inherited)"; - OTHER_CPLUSPLUSFLAGS = "$(inherited)"; - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); - REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OBJC_BRIDGING_HEADER = "HaapiModule/HaapiModule-Bridging-Header.h"; - USE_HERMES = true; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 65B6F8472B8CD91800AC54A9 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ALY8Q7BNK9; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OBJC_BRIDGING_HEADER = "HaapiModule/HaapiModule-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 65B6F8482B8CD91800AC54A9 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ALY8Q7BNK9; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OBJC_BRIDGING_HEADER = "HaapiModule/HaapiModule-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 65B6F83A2B8CD91800AC54A9 /* Build configuration list for PBXProject "HaapiModule" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 65B6F8442B8CD91800AC54A9 /* Debug */, - 65B6F8452B8CD91800AC54A9 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 65B6F8462B8CD91800AC54A9 /* Build configuration list for PBXNativeTarget "HaapiModule" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 65B6F8472B8CD91800AC54A9 /* Debug */, - 65B6F8482B8CD91800AC54A9 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 65B6F8372B8CD91800AC54A9 /* Project object */; -} diff --git a/ios/HaapiModule.xcworkspace/contents.xcworkspacedata b/ios/HaapiModule.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index dfdceda..0000000 --- a/ios/HaapiModule.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/ios/HaapiModule.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/HaapiModule.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/ios/HaapiModule.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - From d983219abefe1633464c710a40165d0f8cf340a8 Mon Sep 17 00:00:00 2001 From: Daniel Lindau Date: Thu, 21 Nov 2024 16:52:13 +0100 Subject: [PATCH 19/19] Add note on passkeys --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ee258c..f71ec65 100644 --- a/README.md +++ b/README.md @@ -157,10 +157,14 @@ HaapiLoading | The module has started a request and i HaapiFinishedLoading | The module received response and finished processing LoggedOut | The module finished the logout WebAuthnAuthenticationStep | Current authentication step is a webauthn/passkeys step. The module will perform a client operation to ask the user to authenticate on their device. The full step is provided to the client to be able to show an appropriate screen. [Login with WebAuthn](https://curity.io/docs/haapi-data-model/latest/webauthn-authentication-step.html) -WebAuthnUserCancelled | User cancelled the authentication request. App should show appropriate screens for how to proceed +WebAuthnUserCancelled | User canceled the authentication request. App should show appropriate screens for how to proceed WebAuthnRegistrationFailed | Registration of a webauthn device failed WebAuthnRegistrationFailedKeyRegistered | Registration of a webauthn device failed. Reason is likely because the key is already registered. User should proceed to authenticate using the key. +## Passkeys +Passkey implementation is supported on iOS 15.0+, and on Android the Credential Manager API is used which should support passkeys from version 9, API level 28. +Additional configuration is necessary to be able to use passkeys in the app, but also on Curity configuration. See the Curity article on how to enable [Mobile Logins Using Passkeys](https://curity.io/resources/learn/mobile-logins-using-native-passkeys/) + ## Example implementation See for example implementation in javascript which is mostly driven by events.