Skip to content
Draft
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
12 changes: 11 additions & 1 deletion .github/scripts/compare-types/configs/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,23 @@ const config: PackageConfig = {
reason:
'RN Firebase exported MultiFactor interface type. firebase-js-sdk exposes multi-factor enrollment through the multiFactor() helper without exporting this interface at the package root.',
},
{
name: 'AppleFullPersonName',
reason:
'RN Firebase exported type for the fullName option on Sign in with Apple credentials. iOS: forwarded to FIROAuthProvider.appleCredentialWithIDToken:rawNonce:fullName: so Firebase stores it as the displayName on first sign-in. Android: Firebase Android SDK has no equivalent credential-level API (https://firebase.google.com/docs/auth/android/apple#handle-sign-in); ignored, use updateProfile() after sign-in instead. firebase-js-sdk OAuthCredentialOptions has no fullName field on any platform.',
},
],

differentShape: [
{
name: 'OAuthCredential',
reason:
'RN Firebase OAuthCredential exposes rawNonce for Apple / limited-login flows. OAuth 1.0 token secrets use the inherited AuthCredential.secret field (firebase-js-sdk optional secret on OAuthCredential).',
'RN Firebase OAuthCredential exposes rawNonce for Apple / limited-login flows, plus fullName for Sign in with Apple (iOS only; see AppleFullPersonName). OAuth 1.0 token secrets use the inherited AuthCredential.secret field (firebase-js-sdk optional secret on OAuthCredential).',
},
{
name: 'OAuthCredentialOptions',
reason:
'RN Firebase adds an optional fullName field (see AppleFullPersonName) so OAuthProvider("apple.com").credential({ idToken, rawNonce, fullName }) can forward Sign in with Apple full-name data on iOS. firebase-js-sdk OAuthCredentialOptions has no fullName field on any platform.',
},
{
name: 'FacebookAuthProvider',
Expand Down
86 changes: 86 additions & 0 deletions packages/auth/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,92 @@ describe('Auth', function () {
expect(credential?.token).toBe('vid');
expect(credential?.secret).toBe('123456');
});

it('AppleAuthProvider.credential maps fullName alongside the native bridge fields', function () {
const fullName = { givenName: 'Jonny', familyName: 'Appleseed' };
const credential: any = AppleAuthProvider.credential(
'apple-id-token',
'apple-raw-nonce',
fullName,
);
expect(credential.providerId).toBe('apple.com');
expect(credential.signInMethod).toBe('apple.com');
expect(credential.idToken).toBe('apple-id-token');
expect(credential.rawNonce).toBe('apple-raw-nonce');
expect(credential.fullName).toEqual(fullName);
// Native bridge slots are unaffected by fullName (backwards-compatible with pre-fullName behavior).
expect(credential.token).toBe('apple-id-token');
expect(credential.secret).toBe('apple-raw-nonce');
});

it('AppleAuthProvider.credential works without fullName', function () {
const credential: any = AppleAuthProvider.credential('apple-id-token', 'apple-raw-nonce');
expect(credential.fullName).toBeUndefined();
expect(credential.token).toBe('apple-id-token');
expect(credential.secret).toBe('apple-raw-nonce');
});

it('OAuthProvider(apple.com).credential carries fullName', function () {
const fullName = { givenName: 'Jonny', familyName: 'Appleseed' };
const credential = new OAuthProvider('apple.com').credential({
idToken: 'apple-id-token',
rawNonce: 'apple-raw-nonce',
fullName,
});
expect(credential.fullName).toEqual(fullName);
});

it('OAuthCredential.fromJSON round-trips fullName', function () {
const fullName = { givenName: 'Jonny', familyName: 'Appleseed' };
const credential = AppleAuthProvider.credential(
'apple-id-token',
'apple-raw-nonce',
fullName,
) as OAuthCredential;
const roundTripped = OAuthCredential.fromJSON(credential.toJSON());
expect(roundTripped?.fullName).toEqual(fullName);
});
});

describe('Sign in with Apple fullName', function () {
it('signInWithCredential forwards fullName to the native bridge', async function () {
const { TurboModuleRegistry } = require('react-native');
const nativeAuth = TurboModuleRegistry.getEnforcing('NativeRNFBTurboAuth');
nativeAuth.signInWithCredential.mockClear();

const fullName = { givenName: 'Jonny', familyName: 'Appleseed' };
const credential = AppleAuthProvider.credential(
'apple-id-token',
'apple-raw-nonce',
fullName,
);
await signInWithCredential(getAuth(), credential);

expect(nativeAuth.signInWithCredential).toHaveBeenCalledWith(
'[DEFAULT]',
'apple.com',
'apple-id-token',
'apple-raw-nonce',
fullName,
);
});

it('signInWithCredential forwards null when fullName is absent', async function () {
const { TurboModuleRegistry } = require('react-native');
const nativeAuth = TurboModuleRegistry.getEnforcing('NativeRNFBTurboAuth');
nativeAuth.signInWithCredential.mockClear();

const credential = GoogleAuthProvider.credential('google-id-token');
await signInWithCredential(getAuth(), credential);

expect(nativeAuth.signInWithCredential).toHaveBeenCalledWith(
'[DEFAULT]',
'google.com',
'google-id-token',
'',
null,
);
});
});

describe('User.reauthenticateWithRedirect', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,15 @@ public void updateProfile(String appName, ReadableMap props, final Promise promi

@Override
public void signInWithCredential(
String appName, String provider, String authToken, String authSecret, final Promise promise) {
String appName,
String provider,
String authToken,
String authSecret,
@Nullable ReadableMap fullName,
final Promise promise) {
// Sign in with Apple fullName has no equivalent credential-level API on the Firebase Android
// SDK (see AppleFullPersonName in lib/types/auth.ts); intentionally ignored here for spec
// parity with iOS. Callers should use updateProfile() after sign-in on Android.
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ public NativeRNFBTurboAuthSpec(ReactApplicationContext reactContext) {

@ReactMethod
@DoNotStrip
public abstract void signInWithCredential(String appName, String provider, String authToken, String authSecret, Promise promise);
public abstract void signInWithCredential(String appName, String provider, String authToken, String authSecret, @Nullable ReadableMap fullName, Promise promise);

@ReactMethod
@DoNotStrip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_getIdToken

static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithCredential(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
static jmethodID cachedMethodId = nullptr;
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "signInWithCredential", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "signInWithCredential", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
}

static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithProvider(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
Expand Down Expand Up @@ -350,7 +350,7 @@ NativeRNFBTurboAuthSpecJSI::NativeRNFBTurboAuthSpecJSI(const JavaTurboModule::In
methodMap_["updateProfile"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthSpecJSI_updateProfile};
methodMap_["getIdToken"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthSpecJSI_getIdToken};
methodMap_["getIdTokenResult"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthSpecJSI_getIdTokenResult};
methodMap_["signInWithCredential"] = MethodMetadata {4, __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithCredential};
methodMap_["signInWithCredential"] = MethodMetadata {5, __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithCredential};
methodMap_["signInWithProvider"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithProvider};
methodMap_["signInWithPhoneNumber"] = MethodMetadata {3, __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithPhoneNumber};
methodMap_["verifyPhoneNumberWithMultiFactorInfo"] = MethodMetadata {3, __hostFunction_NativeRNFBTurboAuthSpecJSI_verifyPhoneNumberWithMultiFactorInfo};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ static jsi::Value __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_signInWithCredent
count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt),
count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt),
count <= 2 ? throw jsi::JSError(rt, "Expected argument in position 2 to be passed") : args[2].asString(rt),
count <= 3 ? throw jsi::JSError(rt, "Expected argument in position 3 to be passed") : args[3].asString(rt)
count <= 3 ? throw jsi::JSError(rt, "Expected argument in position 3 to be passed") : args[3].asString(rt),
count <= 4 || args[4].isNull() || args[4].isUndefined() ? std::nullopt : std::make_optional(args[4].asObject(rt))
);
}
static jsi::Value __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_signInWithProvider(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
Expand Down Expand Up @@ -502,7 +503,7 @@ NativeRNFBTurboAuthCxxSpecJSI::NativeRNFBTurboAuthCxxSpecJSI(std::shared_ptr<Cal
methodMap_["updateProfile"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_updateProfile};
methodMap_["getIdToken"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_getIdToken};
methodMap_["getIdTokenResult"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_getIdTokenResult};
methodMap_["signInWithCredential"] = MethodMetadata {4, __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_signInWithCredential};
methodMap_["signInWithCredential"] = MethodMetadata {5, __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_signInWithCredential};
methodMap_["signInWithProvider"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_signInWithProvider};
methodMap_["signInWithPhoneNumber"] = MethodMetadata {3, __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_signInWithPhoneNumber};
methodMap_["verifyPhoneNumberWithMultiFactorInfo"] = MethodMetadata {3, __hostFunction_NativeRNFBTurboAuthCxxSpecJSI_verifyPhoneNumberWithMultiFactorInfo};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ namespace facebook::react {
virtual jsi::Value updateProfile(jsi::Runtime &rt, jsi::String appName, jsi::Object props) = 0;
virtual jsi::Value getIdToken(jsi::Runtime &rt, jsi::String appName, bool forceRefresh) = 0;
virtual jsi::Value getIdTokenResult(jsi::Runtime &rt, jsi::String appName, bool forceRefresh) = 0;
virtual jsi::Value signInWithCredential(jsi::Runtime &rt, jsi::String appName, jsi::String provider, jsi::String authToken, jsi::String authSecret) = 0;
virtual jsi::Value signInWithCredential(jsi::Runtime &rt, jsi::String appName, jsi::String provider, jsi::String authToken, jsi::String authSecret, std::optional<jsi::Object> fullName) = 0;
virtual jsi::Value signInWithProvider(jsi::Runtime &rt, jsi::String appName, jsi::Object provider) = 0;
virtual jsi::Value signInWithPhoneNumber(jsi::Runtime &rt, jsi::String appName, jsi::String phoneNumber, bool forceResend) = 0;
virtual jsi::Value verifyPhoneNumberWithMultiFactorInfo(jsi::Runtime &rt, jsi::String appName, jsi::String hintUid, jsi::String sessionKey) = 0;
Expand Down Expand Up @@ -359,13 +359,13 @@ class JSI_EXPORT NativeRNFBTurboAuthCxxSpec : public TurboModule {
return bridging::callFromJs<jsi::Value>(
rt, &T::getIdTokenResult, jsInvoker_, instance_, std::move(appName), std::move(forceRefresh));
}
jsi::Value signInWithCredential(jsi::Runtime &rt, jsi::String appName, jsi::String provider, jsi::String authToken, jsi::String authSecret) override {
jsi::Value signInWithCredential(jsi::Runtime &rt, jsi::String appName, jsi::String provider, jsi::String authToken, jsi::String authSecret, std::optional<jsi::Object> fullName) override {
static_assert(
bridging::getParameterCount(&T::signInWithCredential) == 5,
"Expected signInWithCredential(...) to have 5 parameters");
bridging::getParameterCount(&T::signInWithCredential) == 6,
"Expected signInWithCredential(...) to have 6 parameters");

return bridging::callFromJs<jsi::Value>(
rt, &T::signInWithCredential, jsInvoker_, instance_, std::move(appName), std::move(provider), std::move(authToken), std::move(authSecret));
rt, &T::signInWithCredential, jsInvoker_, instance_, std::move(appName), std::move(provider), std::move(authToken), std::move(authSecret), std::move(fullName));
}
jsi::Value signInWithProvider(jsi::Runtime &rt, jsi::String appName, jsi::Object provider) override {
static_assert(
Expand Down
53 changes: 53 additions & 0 deletions packages/auth/e2e/provider.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,59 @@ describe('auth() -> Providers', function () {
credential.secret.should.equal(accessToken);
credential.accessToken.should.equal(accessToken);
});

it('should reach iOS native signInWithCredential for apple.com credentials with fullName', async function () {
if (!Platform.ios) {
this.skip();
}
const { getApp } = modular;
const { OAuthProvider, signInWithCredential, getAuth } = authModular;
const defaultAuth = getAuth(getApp());
const provider = new OAuthProvider('apple.com');
const credential = provider.credential({
idToken: 'apple-id-token-fullname-e2e',
rawNonce: 'apple-raw-nonce-fullname-e2e',
fullName: {
givenName: 'Ada',
familyName: 'Lovelace',
},
});

try {
await signInWithCredential(defaultAuth, credential);
throw new Error('Did not error.');
} catch (error) {
error.code.should.be.a.String();
error.code.should.not.equal('auth/unknown');
String(error.message).should.not.match(/fullName/i);
}
});

it('should reach iOS native signInWithCredential for apple.com credentials with an empty fullName', async function () {
if (!Platform.ios) {
this.skip();
}
const { getApp } = modular;
const { OAuthProvider, signInWithCredential, getAuth } = authModular;
const defaultAuth = getAuth(getApp());
const provider = new OAuthProvider('apple.com');
const credential = provider.credential({
idToken: 'apple-id-token-empty-fullname-e2e',
rawNonce: 'apple-raw-nonce-empty-fullname-e2e',
// No recognized NSPersonNameComponents fields; native should fall back to the
// credential built without fullName rather than an empty name components object.
fullName: {},
});

try {
await signInWithCredential(defaultAuth, credential);
throw new Error('Did not error.');
} catch (error) {
error.code.should.be.a.String();
error.code.should.not.equal('auth/unknown');
String(error.message).should.not.match(/fullName/i);
}
});
});

describe('PROVIDER_ID', function () {
Expand Down
49 changes: 49 additions & 0 deletions packages/auth/ios/RNFBAuth/RNFBAuthModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ - (void)signInWithCredential:(NSString *)appName
provider:(NSString *)provider
authToken:(NSString *)authToken
authSecret:(NSString *)authSecret
fullName:(NSDictionary *)fullName
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
FIRApp *firebaseApp = [RCTConvert firAppFromString:appName];
Expand All @@ -649,6 +650,20 @@ - (void)signInWithCredential:(NSString *)appName
token:authToken
secret:authSecret
firebaseApp:firebaseApp];

// Sign in with Apple: if the caller supplied fullName (only available on the user's first
// authorization), rebuild the credential via appleCredentialWithIDToken:rawNonce:fullName: so
// Firebase can store it as the account's displayName. See AppleFullPersonName in auth.ts.
if ([provider compare:@"apple.com" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
NSPersonNameComponents *personNameComponents =
[self personNameComponentsFromDictionary:fullName];
if (personNameComponents != nil) {
credential = [FIROAuthProvider appleCredentialWithIDToken:authToken
rawNonce:authSecret
fullName:personNameComponents];
}
}
Comment thread
russellwheatley marked this conversation as resolved.

if (credential == nil) {
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
userInfo:(NSMutableDictionary *)@{
Expand Down Expand Up @@ -1605,6 +1620,40 @@ - (void)useEmulator:(NSString *)appName host:(nonnull NSString *)host port:(doub
}
}

- (NSPersonNameComponents *)personNameComponentsFromDictionary:(NSDictionary *)dictionary {
if (dictionary == nil || (id)dictionary == [NSNull null]) {
return nil;
}

NSPersonNameComponents *components = [[NSPersonNameComponents alloc] init];
BOOL hasAnyComponent = NO;
if ([dictionary[@"namePrefix"] isKindOfClass:[NSString class]]) {
components.namePrefix = dictionary[@"namePrefix"];
hasAnyComponent = YES;
}
if ([dictionary[@"givenName"] isKindOfClass:[NSString class]]) {
components.givenName = dictionary[@"givenName"];
hasAnyComponent = YES;
}
if ([dictionary[@"middleName"] isKindOfClass:[NSString class]]) {
components.middleName = dictionary[@"middleName"];
hasAnyComponent = YES;
}
if ([dictionary[@"familyName"] isKindOfClass:[NSString class]]) {
components.familyName = dictionary[@"familyName"];
hasAnyComponent = YES;
}
if ([dictionary[@"nameSuffix"] isKindOfClass:[NSString class]]) {
components.nameSuffix = dictionary[@"nameSuffix"];
hasAnyComponent = YES;
}
if ([dictionary[@"nickname"] isKindOfClass:[NSString class]]) {
components.nickname = dictionary[@"nickname"];
hasAnyComponent = YES;
}
return hasAnyComponent ? components : nil;
}
Comment thread
russellwheatley marked this conversation as resolved.

- (FIRAuthCredential *)getCredentialForProvider:(NSString *)provider
token:(NSString *)authToken
secret:(NSString *)authTokenSecret
Expand Down
Loading
Loading