Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 192 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ React Native Credentials Manager

![App Screens](IMG/flow.png)

A React Native library that implements the [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager) API for Android. This library allows you to manage passwords, passkeys and google signin in your React Native applications.
A React Native library that implements the [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager) API for Android and [AuthenticationServices](https://developer.apple.com/documentation/authenticationservices) for iOS. This library allows you to manage passwords, passkeys and platform-specific sign-in (Google Sign-In on Android, Apple Sign In on iOS) in your React Native applications.

<p align="center">
<a href="https://www.npmjs.com/package/react-native-credentials-manager">
Expand All @@ -18,5 +18,196 @@ A React Native library that implements the [Credential Manager](https://develope
</a>
</p>

## Platform Support

- ✅ **Android**: Full implementation with Credential Manager API (Android 14+ / API 34+)
- ✅ **iOS**: Full implementation with AuthenticationServices (iOS 16.0+)

### Platform-Specific Features

| Feature | Android | iOS |
| ------------------------- | ------------------------- | --------------------------------- |
| Passkeys | ✅ Credential Manager API | ✅ AuthenticationServices |
| AutoFill Password Support | ✅ Credential Manager API | ✅ AuthenticationServices |
| Manual Password Storage | ✅ Credential Manager API | ❌ Not supported (iOS limitation) |
| Third-party Sign In | ✅ Google Sign In | ✅ Apple Sign In |

> [!NOTE] > **iOS Implementation**: This library strictly follows Apple's Authentication Services framework. Manual password storage is not supported on iOS as it's not part of Apple's official Authentication Services APIs. Use AutoFill passwords instead.

> [!IMPORTANT]
> 📚 **Documentation has moved!** The complete documentation is now available at [https://docs.benjamineruvieru.com/docs/react-native-credentials-manager/](https://docs.benjamineruvieru.com/docs/react-native-credentials-manager/)

## Platform-Specific Parameters

When using this library, be aware that some parameters are platform-specific:

| Function | Parameter | Platform Support | Notes |
| ---------------------- | --------------------------------------- | ---------------- | ----------------------------------------------------------------- |
| `signUpWithPasskeys()` | `preferImmediatelyAvailableCredentials` | Android only | This parameter is ignored on iOS |
| `signUpWithPassword()` | All parameters | Android only | This function is not supported on iOS and will throw an error |
| `signIn()` | `googleSignIn` | Android only | This parameter is ignored on iOS |
| `signIn()` | `appleSignIn` | iOS only | This parameter is ignored on Android |
| `signUpWithGoogle()` | All parameters | Cross-platform | Uses Google Sign-In on Android, Apple Sign-In on iOS |
| `signUpWithApple()` | All parameters | iOS only | This function is not supported on Android and will throw an error |

### Handling Platform Differences

To handle these platform differences, you can use conditional code:

```typescript
// For passkey registration
await signUpWithPasskeys(
requestJson,
Platform.OS === 'android' ? true : false // preferImmediatelyAvailableCredentials
);

// For sign-in
await signIn(
[
'passkeys',
'password',
Platform.OS === 'android' ? 'google-signin' : 'apple-signin',
],
{
passkeys: passkeyParams,
...(Platform.OS === 'android'
? { googleSignIn: { serverClientId: 'your-client-id' } }
: { appleSignIn: { requestedScopes: ['fullName', 'email'] } }),
}
);
```

## iOS Setup Requirements

### 1. Associated Domains

Add the Associated Domains capability to your iOS app:

1. In Xcode, select your project
2. Go to Signing & Capabilities
3. Add "Associated Domains" capability
4. Add your domain with the `webcredentials` service: `webcredentials:yourdomain.com`

### 2. Apple App Site Association (AASA)

Ensure your domain has a proper AASA file at `https://yourdomain.com/.well-known/apple-app-site-association`:

```json
{
"webcredentials": {
"apps": ["TEAMID.com.yourcompany.yourapp"]
}
}
```

### 3. Apple Sign In Setup (Optional)

If using Apple Sign In, configure it in your Apple Developer account:

1. Enable "Sign In with Apple" capability in Xcode
2. Configure Sign In with Apple in your Apple Developer account
3. Add your app's bundle identifier to the Sign In with Apple configuration

## Quick Example

```typescript
import {
signUpWithPasskeys,
signUpWithPassword, // Android only - throws error on iOS
signUpWithGoogle, // Cross-platform: Google on Android, Apple on iOS
signUpWithApple, // iOS-specific function
signIn,
signOut,
type Credential,
type AppleCredential,
type GoogleCredential,
type AppleSignInParams,
type GoogleSignInParams,
} from 'react-native-credentials-manager';

// Sign up with passkey (works on both platforms)
const passkeyResult = await signUpWithPasskeys({
challenge: 'base64-challenge',
rp: { name: 'Your App', id: 'yourdomain.com' },
user: { id: 'user-id', name: 'username', displayName: 'User Name' },
// ... other WebAuthn options
});

// Password handling - platform differences
try {
// This will work on Android but throw an error on iOS
await signUpWithPassword({ username: 'user', password: 'pass' });
} catch (error) {
if (Platform.OS === 'ios') {
console.log(
'Manual password storage not supported on iOS. Use AutoFill instead.'
);
}
}

// Unified sign in (supports passkeys, AutoFill passwords, and platform sign-in)
const credential = await signIn(
['passkeys', 'password', 'google-signin'], // 'google-signin' becomes 'apple-signin' on iOS
{
passkeys: { challenge: 'base64-challenge', rpId: 'yourdomain.com' },
googleSignIn: { serverClientId: 'your-client-id' },
}
);

// Platform-specific sign-up methods
if (Platform.OS === 'ios') {
// Direct Apple Sign In (iOS only) - uses AuthenticationServices
const appleCredential = await signUpWithApple({
requestedScopes: ['fullName', 'email'],
});
} else {
// Google Sign In (Android only)
const googleCredential = await signUpWithGoogle({
serverClientId: 'your-client-id',
});
}

// Cross-platform sign-up (automatically uses the appropriate method)
const credential = await signUpWithGoogle({
serverClientId: 'your-client-id', // Used on Android, ignored on iOS
});
// Returns GoogleCredential on Android, AppleCredential on iOS

// Handle different credential types
if (credential.type === 'passkey') {
console.log('Passkey authentication:', credential.authenticationResponseJson);
} else if (credential.type === 'password') {
console.log('AutoFill Password:', credential.username, credential.password);
} else if (credential.type === 'google-signin') {
console.log('Google Sign In:', credential.idToken);
} else if (credential.type === 'apple-signin') {
console.log('Apple Sign In:', credential.idToken, credential.email);
}
```

## Cross-Platform Compatibility

The library provides excellent cross-platform compatibility while respecting platform limitations:

- **`signUpWithGoogle()`**: Cross-platform function
- Android: Uses Google Sign In
- iOS: Automatically uses Apple Sign In (AuthenticationServices)
- **`signUpWithApple()`**: iOS-specific function
- iOS: Uses Apple Sign In (AuthenticationServices)
- Android: Rejects with clear error message
- **`signUpWithPassword()`**: Platform-specific behavior
- Android: Uses Credential Manager API
- iOS: Rejects (manual storage not supported by AuthenticationServices)
- **AutoFill Passwords**: Available on both platforms through `signIn` method
- **Error handling**: Platform-specific errors are handled gracefully

## iOS AuthenticationServices Integration

The iOS implementation strictly follows Apple's Authentication Services framework:

- **Passkeys**: `ASAuthorizationPlatformPublicKeyCredentialProvider`
- **AutoFill Passwords**: `ASAuthorizationPasswordProvider`
- **Apple Sign In**: `ASAuthorizationAppleIDProvider`
- **No Custom Keychain**: Manual credential storage is handled by the system

This ensures compliance with Apple's security guidelines and provides the best user experience on iOS.
11 changes: 6 additions & 5 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.credentials:credentials:1.5.0-rc01"
implementation "androidx.credentials:credentials-play-services-auth:1.5.0-rc01"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation "com.google.android.libraries.identity.googleid:googleid:1.1.1"
implementation("androidx.credentials:credentials:1.6.0-alpha02")

implementation("androidx.credentials:credentials-play-services-auth:1.6.0-alpha02")
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation "com.google.android.libraries.identity.googleid:googleid:1.1.1"
}

if (isNewArchitectureEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,35 +36,49 @@ class CredentialHandler(
jsonString: String,
preferImmediatelyAvailableCredentials: Boolean,
): ReadableMap? {
val request =
CreatePublicKeyCredentialRequest(
requestJson = jsonString,
preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
)

val response =
credentialManager.createCredential(
context,
request,
) as CreatePublicKeyCredentialResponse

return response.data.getString("androidx.credentials.BUNDLE_KEY_REGISTRATION_RESPONSE_JSON")?.let { json ->
val jsonObject = Arguments.createMap()
val parsedObject = JSONObject(json)

parsedObject.keys().forEach { key ->
jsonObject.putString(key, parsedObject.getString(key))
Log.d("CredentialManager", "Creating passkey with request: $jsonString")

try {
val request =
CreatePublicKeyCredentialRequest(
requestJson = jsonString,
preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
)

val response =
credentialManager.createCredential(
context,
request,
) as CreatePublicKeyCredentialResponse

return response.data.getString("androidx.credentials.BUNDLE_KEY_REGISTRATION_RESPONSE_JSON")?.let { json ->
val jsonObject = Arguments.createMap()
val parsedObject = JSONObject(json)

parsedObject.keys().forEach { key ->
jsonObject.putString(key, parsedObject.getString(key))
}
jsonObject
}
jsonObject
} catch (e: Exception) {
Log.e("CredentialManager", "Error creating passkey", e)
throw e
}
}

suspend fun createPassword(
username: String,
password: String,
) {
val createPasswordRequest = CreatePasswordRequest(id = username, password = password)
credentialManager.createCredential(context, createPasswordRequest)
Log.d("CredentialManager", "Creating password credential for username: $username")

try {
val createPasswordRequest = CreatePasswordRequest(id = username, password = password)
credentialManager.createCredential(context, createPasswordRequest)
} catch (e: Exception) {
Log.e("CredentialManager", "Error creating password credential", e)
throw e
}
}

suspend fun signIn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,34 @@ class CredentialsManagerModule(
}
}

override fun signUpWithPassword(credObject: ReadableMap) {
override fun signUpWithPassword(credObject: ReadableMap, promise: Promise) {
val username = credObject.getString("username") ?: ""
val password = credObject.getString("password") ?: ""

if (username.isEmpty()) {
promise.reject("INVALID_USERNAME", "Username cannot be empty")
return
}

if (password.isEmpty()) {
promise.reject("INVALID_PASSWORD", "Password cannot be empty")
return
}

coroutineScope.launch {
try {
credentialHandler.createPassword(username, password)

// Create success response
val result = mapOf(
"type" to "password",
"username" to username,
"success" to true
)
promise.resolve(result)
} catch (e: CreateCredentialException) {
ErrorHandler.handleCredentialError(e)
promise.reject("CREDENTIAL_ERROR", e.message.toString())
}
}
}
Expand Down Expand Up @@ -140,4 +160,11 @@ class CredentialsManagerModule(
}
}
}

override fun signUpWithApple(params: ReadableMap, promise: Promise) {
promise.reject(
"PLATFORM_NOT_SUPPORTED",
"Sign up with Apple is only supported on iOS devices"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,34 @@ class CredentialsManagerModule(
}

@ReactMethod
fun signUpWithPassword(credObject: ReadableMap) {
fun signUpWithPassword(credObject: ReadableMap, promise: Promise) {
val username = credObject.getString("username") ?: ""
val password = credObject.getString("password") ?: ""

if (username.isEmpty()) {
promise.reject("INVALID_USERNAME", "Username cannot be empty")
return
}

if (password.isEmpty()) {
promise.reject("INVALID_PASSWORD", "Password cannot be empty")
return
}

coroutineScope.launch {
try {
credentialHandler.createPassword(username, password)

// Create success response
val result = mapOf(
"type" to "password",
"username" to username,
"success" to true
)
promise.resolve(result)
} catch (e: CreateCredentialException) {
ErrorHandler.handleCredentialError(e)
promise.reject("CREDENTIAL_ERROR", e.message.toString())
}
}
}
Expand Down Expand Up @@ -161,4 +181,13 @@ class CredentialsManagerModule(
}
}
}

@ReactMethod
fun signUpWithApple(params: ReadableMap, promise: Promise) {
// Since this is an iOS-specific function, we just reject with an appropriate message on Android
promise.reject(
"PLATFORM_NOT_SUPPORTED",
"Sign up with Apple is only supported on iOS devices"
)
}
}
2 changes: 1 addition & 1 deletion example/ios/.xcode.env.local
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export NODE_BINARY=/Users/mac/.nvm/versions/node/v20.14.0/bin/node
export NODE_BINARY=/opt/homebrew/Cellar/node/23.11.0/bin/node
Loading
Loading