Skip to content

Commit

Permalink
chore: Introduce Passkeys library for expo
Browse files Browse the repository at this point in the history
In this commit, we are introducing a library to handle passkeys for Expo and Clerk. The library manages the creation and retrieval of passkeys for iOS, Android devices, and Expo web.

We have also provided an example app that serves as a playground and helps us test the clerk-expo libraries. The README includes instructions on how to set up the app for iOS devices, and we will soon provide instructions for Android devices as well.
  • Loading branch information
AlexNti committed Oct 18, 2024
1 parent 5c6391b commit d6fb405
Show file tree
Hide file tree
Showing 112 changed files with 37,042 additions and 0 deletions.
Empty file added .npmignore
Empty file.
5 changes: 5 additions & 0 deletions packages/expo/modules/clerk-expo-passkeys/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
root: true,
extends: ['universe/native', 'universe/web'],
ignorePatterns: ['build'],
};
59 changes: 59 additions & 0 deletions packages/expo/modules/clerk-expo-passkeys/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# OSX
#
.DS_Store

# VSCode
.vscode/
jsconfig.json

# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace

# Android/IJ
#
.classpath
.cxx
.gradle
.idea
.project
.settings
local.properties
android.iml
android/app/libs
android/keystores/debug.keystore

# Cocoapods
#
example/ios/Pods

# Ruby
example/vendor/

# node.js
#
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log

# Expo
.expo/*
.env
.yalc
14 changes: 14 additions & 0 deletions packages/expo/modules/clerk-expo-passkeys/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Exclude all top-level hidden directories by convention
/.*/

# Exclude tarballs generated by `npm pack`
/*.tgz

__mocks__
__tests__

/babel.config.js
/android/src/androidTest/
/android/src/test/
/android/build/
/example/
39 changes: 39 additions & 0 deletions packages/expo/modules/clerk-expo-passkeys/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Running the Example Project with Clerk on Your iOS Simulator or Device

## Steps

### Before executing this steps you will need to have XCode installed and have an Apple Developer Account

1. **Install module deps**
Run `npm i` on the root of the project.

2. **Navigate to the Example Folder**
Open your terminal and move into the `example` folder of the project.

3. **Set Up Environment Variables**
Add the `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` to the `.env` file in the root of your project.

4. **Configure Associated Domains**

- Go to the iOS Application tab on the Clerk Dashboard. For testing purposes, it’s recommended to use a development instance.
- Set up your associated domain file, which requires an Apple Developer account,You can find you App id prefix and Bundle ID at `https://developer.apple.com/account/resources/identifiers/list`
- Replace the `associatedDomains` entry in `app.json` with the front end api provided by Clerk e.x `coyote-6.clerk.accounts.dev` .
- Configure also the `bundleIdentifier` in `app.json` to match the bundle id that can be found at `https://developer.apple.com/account/resources/identifiers/list`

5. **Install Dependencies**
Run `npm install`.

6. **Prebuild the Project**
Run `npx expo prebuild --clean`.

7. **Run the Project on iOS**
Run `npm run ios`

8. **How to use the example**

- Navigate to Dashboard and enable the passkeys attribute at Email, Phone, Username page.
- Navigate to dashboard and create a new user from the Users tab, use only email and password as authentication attributes.
- Open your the device that you application is running and sign in with the user that you just created.
- After signing in you will see a screen that has an option to create a Passkey, press that option and register your passkey.
- Sign out
- In the sign in screen select the option sign in with passkey
53 changes: 53 additions & 0 deletions packages/expo/modules/clerk-expo-passkeys/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'maven-publish'

group = 'expo.modules.clerkexpopasskeys'
version = '0.1.0'

def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useExpoPublishing()

// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
// Most of the time, you may like to manage the Android SDK versions yourself.
def useManagedAndroidSdkVersions = false
if (useManagedAndroidSdkVersions) {
useDefaultAndroidSdkVersions()
} else {
buildscript {
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}
project.android {
compileSdkVersion safeExtGet("compileSdkVersion", 34)
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 35)
}
}
}

android {
namespace "expo.modules.clerkexpopasskeys"
defaultConfig {
versionCode 1
versionName "0.1.0"
}
lintOptions {
abortOnError false
}
}

dependencies {
implementation project(':expo-modules-core')
implementation("androidx.credentials:credentials:1.3.0-rc01")
implementation("androidx.credentials:credentials-play-services-auth:1.3.0-rc01")

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package expo.modules.clerkexpopasskeys

import androidx.credentials.exceptions.CreateCredentialCancellationException
import androidx.credentials.exceptions.CreateCredentialCustomException
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.CreateCredentialInterruptedException
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.GetCredentialInterruptedException
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialUnsupportedException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException
import expo.modules.kotlin.Promise

//https://developer.android.com/identity/sign-in/credential-manager#create-passkey
fun handleCreationFailure(e: CreateCredentialException, promise: Promise) {
when (e) {
is CreatePublicKeyCredentialDomException -> {
// Handle the passkey DOM errors thrown according to the
// WebAuthn spec.
promise.reject("CreatePublicKeyCredentialDomException", e.domError.toString(), e)
}

is CreateCredentialCancellationException -> {
// The user intentionally canceled the operation and chose not
// to register the credential.
promise.reject(
"CreateCredentialCancellationException", e.message,
e
)


}

is CreateCredentialInterruptedException -> {
// Retry-able error. Consider retrying the call.
promise.reject(
"CreateCredentialInterruptedException",
e.message,
e
)
}

is CreateCredentialProviderConfigurationException -> {
// Your app is missing the provider configuration dependency.
// Most likely, you're missing the
// "credentials-play-services-auth" module.
promise.reject(
"CreateCredentialProviderConfigurationException",
e.message,
e
)
}

is CreateCredentialUnknownException -> {
promise.reject(
"CreateCredentialUnknownException",
e.message,
e
)
}

is CreateCredentialCustomException -> {
// You have encountered an error from a 3rd-party SDK. If you
// make the API call with a request object that's a subclass of
// CreateCustomCredentialRequest using a 3rd-party SDK, then you
// should check for any custom exception type constants within
// that SDK to match with e.type. Otherwise, drop or log the
// exception.
promise.reject(
"CreateCredentialCustomException",
e.message,
e
)
}

else -> promise.reject("Error", e.message, e)
}
}

fun handleGetFailure(e: GetCredentialException, promise: Promise) {

when (e) {
is GetPublicKeyCredentialDomException -> {
promise.reject("GetPublicKeyCredentialDomException", e.domError.toString(), e)
}

is GetCredentialInterruptedException -> {
promise.reject("GetCredentialInterruptedException", e.message, e)
}

is GetCredentialCancellationException -> {
promise.reject(
"GetCredentialCancellationException",
e.message,
e
)
}

is GetCredentialUnknownException -> {
promise.reject("GetCredentialUnknownException", e.message, e)
}


is GetCredentialProviderConfigurationException -> {
promise.reject(
"GetCredentialProviderConfigurationException",
e.message,
e
)
}


is GetCredentialUnsupportedException -> {
promise.reject(
"GetCredentialUnsupportedException",
e.message,
e
)
}

is NoCredentialException -> {
promise.reject(
"NoCredentialException",
e.message,
e
)
}

else -> {
promise.reject("Error", e.message, e)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package expo.modules.clerkexpopasskeys

import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.GetCredentialException
import expo.modules.kotlin.Promise
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class ClerkExpoPasskeysModule : Module() {
private val mCoroutine = CoroutineScope(Dispatchers.Default)

override fun definition() = ModuleDefinition {
Name("ClerkExpoPasskeys")

AsyncFunction("create") { request: String, promise: Promise ->

mCoroutine.launch {
try {
val response = createPasskey(request, appContext)
promise.resolve(response)

} catch (e: CreateCredentialException) {
handleCreationFailure(e, promise)
}
}

}

AsyncFunction("get") { request: String, promise: Promise ->

mCoroutine.launch {
try {
val response = getPasskey(request, appContext)
promise.resolve(response)
} catch (e: GetCredentialException) {
handleGetFailure(e, promise)
}
}
}


}
}
Loading

0 comments on commit d6fb405

Please sign in to comment.