Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): Add support for native federated OAuth Providers (Microsoft, Yahoo) #7187

Closed

Conversation

KielKing
Copy link
Contributor

Description

react-native-firebase doesn't currently support Microsoft authentication (without OpenID). Given that I already utilize Microsoft authentication on my web application, I wanted to do the same for mobile. I found a PR (#6574: Add support for OpenID Connect provider) that resolved the issue by supporting OpenID Connect provider. As per the docs I used react-native-app-auth to retrieve the user's Microsoft access token.

Although working, web and mobile users who want to sign in using Microsoft rely on separate providers, even though both are associated with the same Microsoft App Registration. E.g. users who signed up via Microsoft on the web would not have the same provider on mobile, meaning to use Microsoft sign in on mobile, users need to link their account to the same Microsoft account (but now with OpenID).

I saw that manual handling of the sign-in flow was not possible.

Unlike other OAuth providers supported by Firebase such as Google, Facebook, and Twitter, where sign-in can directly be achieved with OAuth access token based credentials, Firebase Auth does not support the same capability for providers such as Microsoft due to the inability of the Firebase Auth server to verify the audience of Microsoft OAuth access tokens. This is a critical security requirement and could expose applications and websites to replay attacks where a Microsoft OAuth access token obtained for one project (attacker) can be used to sign in to another project (victim). Instead, Firebase Auth offers the ability to handle the entire OAuth flow and the authorization code exchange using the OAuth client ID and secret configured in the Firebase Console. As the authorization code can only be used in conjunction with a specific client ID/secret, an authorization code obtained for one project cannot be used with another.

I'm not that familiar with the native side, so please feel free to adjust any code 😄

Pending Feature

Need to store access token received from the OAuth provider. This would allow to perform API requests on the respective providers, such as the Microsoft Graph API or Yahoo API.

Related issues

Does not fix these issues: #6305, #4731, but gives an opportunity to use the native Microsoft Sign-In flow instead of creating an OpenID provider.

Release Summary

Added support for native federated OAuth Providers (Microsoft, Yahoo)

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
    • Yes
  • My change supports the following platforms;
    • Android
    • iOS
  • My change includes tests;
    • e2e tests added or updated in packages/\*\*/e2e
    • jest tests added or updated in packages/\*\*/__tests__
  • I have updated TypeScript types that are affected by my change.
  • This is a breaking change;
    • Yes
    • No

Test Plan

Will develop this at a later time


Think react-native-firebase is great? Please consider supporting the project with any of the below:

🔥

@vercel
Copy link

vercel bot commented Jun 19, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-native-firebase ✅ Ready (Inspect) Visit Preview 💬 Add feedback Nov 6, 2023 7:17pm
react-native-firebase-next ❌ Failed (Inspect) Nov 6, 2023 7:17pm

@CLAassistant
Copy link

CLAassistant commented Jun 19, 2023

CLA assistant check
All committers have signed the CLA.

@github-actions
Copy link

Hello 👋, this PR has been opened for more than 2 months with no activity on it.

If you think this is a mistake please comment and ping a maintainer to get this merged ASAP! Thanks for contributing!

You have 15 days until this gets closed automatically

@KielKing
Copy link
Contributor Author

Hi @mikehardy, sorry I was informed to ping a maintainer

@github-actions github-actions bot removed the Stale label Jul 17, 2023
@mikehardy
Copy link
Collaborator

No worries! There is steady progress in the repo and not that many open PRs at the moment, hopefully get a chance to review soon

@github-actions
Copy link

Hello 👋, this PR has been opened for more than 2 months with no activity on it.

If you think this is a mistake please comment and ping a maintainer to get this merged ASAP! Thanks for contributing!

You have 15 days until this gets closed automatically

@github-actions github-actions bot added the Stale label Aug 14, 2023
@github-actions github-actions bot closed this Aug 29, 2023
@mnahkies
Copy link
Contributor

Any chance this could be revived? It looks like it might be very helpful for a project we're working on

@mikehardy
Copy link
Collaborator

mikehardy commented Oct 25, 2023

Yes, it was open on purpose but our stale bot is ignoring pins at the moment. Still working through full repo infrastructure at the moment but hopefully someone has time to finish this or I do in the future

@mikehardy mikehardy reopened this Oct 25, 2023
@mikehardy mikehardy added Keep Open avoids the stale bot and removed Stale labels Oct 25, 2023
@lpfrenette
Copy link
Contributor

For those interested, i've made a patch to use with path-package from this pull request.
I'm currently investigating why i have The supplied auth credential is malformed or has expired error when i try to sign-in, but if can help anyone while waiting on the official release.

@react-native-firebase+auth+18.6.1.patch

diff --git a/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java b/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
index 771f3f5..ea5c10b 100644
--- a/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
+++ b/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
@@ -26,10 +26,13 @@ import com.facebook.react.bridge.Arguments;
 import com.facebook.react.bridge.Promise;
 import com.facebook.react.bridge.ReactApplicationContext;
 import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableArray;
 import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.ReadableMapKeySetIterator;
 import com.facebook.react.bridge.WritableArray;
 import com.facebook.react.bridge.WritableMap;
 import com.google.android.gms.tasks.OnCompleteListener;
+import com.google.android.gms.tasks.Task;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseException;
 import com.google.firebase.FirebaseNetworkException;
@@ -877,6 +880,72 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     }
   }
 
+  @ReactMethod
+  private void signInWithProvider(String appName, ReadableMap provider, final Promise promise) {
+    FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
+    FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
+
+    if (provider.getString("providerId") == null) {
+      rejectPromiseWithCodeAndMessage(
+          promise,
+          "invalid-credential",
+          "The supplied auth credential is malformed, has expired or is not currently supported.");
+      return;
+    }
+
+    OAuthProvider.Builder builder = OAuthProvider.newBuilder(provider.getString("providerId"));
+    // Add scopes if present
+    if (provider.hasKey("scopes")) {
+      ReadableArray scopes = provider.getArray("scopes");
+      if (scopes != null) {
+        List<String> scopeList = new ArrayList<>();
+        for (int i = 0; i < scopes.size(); i++) {
+          String scope = scopes.getString(i);
+          scopeList.add(scope);
+        }
+        builder.setScopes(scopeList);
+      }
+    }
+    // Add custom parameters if present
+    if (provider.hasKey("customParameters")) {
+      ReadableMap customParameters = provider.getMap("customParameters");
+      if (customParameters != null) {
+        ReadableMapKeySetIterator iterator = customParameters.keySetIterator();
+        while (iterator.hasNextKey()) {
+          String key = iterator.nextKey();
+          builder.addCustomParameter(key, customParameters.getString(key));
+        }
+      }
+    }
+    Task<AuthResult> pendingResultTask = firebaseAuth.getPendingAuthResult();
+    if (pendingResultTask != null) {
+      pendingResultTask
+          .addOnSuccessListener(
+              authResult -> {
+                Log.d(TAG, "signInWithProvider:success");
+                promiseWithAuthResult(authResult, promise);
+              })
+          .addOnFailureListener(
+              e -> {
+                Log.w(TAG, "signInWithProvider:failure", e);
+                promiseRejectAuthException(promise, e);
+              });
+    } else {
+      firebaseAuth
+          .startActivityForSignInWithProvider(getCurrentActivity(), builder.build())
+          .addOnSuccessListener(
+              authResult -> {
+                Log.w(TAG, "signInWithProvider:success");
+                promiseWithAuthResult(authResult, promise);
+              })
+          .addOnFailureListener(
+              e -> {
+                Log.w(TAG, "signInWithProvider:failure", e);
+                promiseRejectAuthException(promise, e);
+              });
+    }
+  }
+
   /**
    * signInWithPhoneNumber
    *
@@ -1527,6 +1596,85 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     }
   }
 
+  /**
+   * linkWithProvider
+   *
+   * @param provider
+   * @param promise
+   */
+  @ReactMethod
+  private void linkWithProvider(String appName, ReadableMap provider, final Promise promise) {
+    FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
+    FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
+
+    if (provider.getString("providerId") == null) {
+      rejectPromiseWithCodeAndMessage(
+          promise,
+          "invalid-credential",
+          "The supplied auth credential is malformed, has expired or is not currently supported.");
+      return;
+    }
+
+    FirebaseUser user = firebaseAuth.getCurrentUser();
+    Log.d(TAG, "linkWithProvider");
+
+    if (user == null) {
+      promiseNoUser(promise, true);
+      return;
+    }
+
+    OAuthProvider.Builder builder = OAuthProvider.newBuilder(provider.getString("providerId"));
+    // Add scopes if present
+    if (provider.hasKey("scopes")) {
+      ReadableArray scopes = provider.getArray("scopes");
+      if (scopes != null) {
+        List<String> scopeList = new ArrayList<>();
+        for (int i = 0; i < scopes.size(); i++) {
+          String scope = scopes.getString(i);
+          scopeList.add(scope);
+        }
+        builder.setScopes(scopeList);
+      }
+    }
+    // Add custom parameters if present
+    if (provider.hasKey("customParameters")) {
+      ReadableMap customParameters = provider.getMap("customParameters");
+      if (customParameters != null) {
+        ReadableMapKeySetIterator iterator = customParameters.keySetIterator();
+        while (iterator.hasNextKey()) {
+          String key = iterator.nextKey();
+          builder.addCustomParameter(key, customParameters.getString(key));
+        }
+      }
+    }
+    Task<AuthResult> pendingResultTask = firebaseAuth.getPendingAuthResult();
+    if (pendingResultTask != null) {
+      pendingResultTask
+          .addOnSuccessListener(
+              authResult -> {
+                Log.d(TAG, "linkWithProvider:success");
+                promiseWithAuthResult(authResult, promise);
+              })
+          .addOnFailureListener(
+              e -> {
+                Log.w(TAG, "linkWithProvider:failure", e);
+                promiseRejectAuthException(promise, e);
+              });
+    } else {
+      user.startActivityForLinkWithProvider(getCurrentActivity(), builder.build())
+          .addOnSuccessListener(
+              authResult -> {
+                Log.w(TAG, "linkWithProvider:success");
+                promiseWithAuthResult(authResult, promise);
+              })
+          .addOnFailureListener(
+              e -> {
+                Log.w(TAG, "linkWithProvider:failure", e);
+                promiseRejectAuthException(promise, e);
+              });
+    }
+  }
+
   @ReactMethod
   public void unlink(final String appName, final String providerId, final Promise promise) {
     FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
@@ -1590,6 +1738,86 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     }
   }
 
+  /**
+   * reauthenticateWithProvider
+   *
+   * @param provider
+   * @param promise
+   */
+  @ReactMethod
+  private void reauthenticateWithProvider(
+      String appName, ReadableMap provider, final Promise promise) {
+    FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
+    FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
+
+    if (provider.getString("providerId") == null) {
+      rejectPromiseWithCodeAndMessage(
+          promise,
+          "invalid-credential",
+          "The supplied auth credential is malformed, has expired or is not currently supported.");
+      return;
+    }
+
+    FirebaseUser user = firebaseAuth.getCurrentUser();
+    Log.d(TAG, "reauthenticateWithProvider");
+
+    if (user == null) {
+      promiseNoUser(promise, true);
+      return;
+    }
+
+    OAuthProvider.Builder builder = OAuthProvider.newBuilder(provider.getString("providerId"));
+    // Add scopes if present
+    if (provider.hasKey("scopes")) {
+      ReadableArray scopes = provider.getArray("scopes");
+      if (scopes != null) {
+        List<String> scopeList = new ArrayList<>();
+        for (int i = 0; i < scopes.size(); i++) {
+          String scope = scopes.getString(i);
+          scopeList.add(scope);
+        }
+        builder.setScopes(scopeList);
+      }
+    }
+    // Add custom parameters if present
+    if (provider.hasKey("customParameters")) {
+      ReadableMap customParameters = provider.getMap("customParameters");
+      if (customParameters != null) {
+        ReadableMapKeySetIterator iterator = customParameters.keySetIterator();
+        while (iterator.hasNextKey()) {
+          String key = iterator.nextKey();
+          builder.addCustomParameter(key, customParameters.getString(key));
+        }
+      }
+    }
+    Task<AuthResult> pendingResultTask = firebaseAuth.getPendingAuthResult();
+    if (pendingResultTask != null) {
+      pendingResultTask
+          .addOnSuccessListener(
+              authResult -> {
+                Log.d(TAG, "reauthenticateWithProvider:success");
+                promiseWithAuthResult(authResult, promise);
+              })
+          .addOnFailureListener(
+              e -> {
+                Log.w(TAG, "reauthenticateWithProvider:failure", e);
+                promiseRejectAuthException(promise, e);
+              });
+    } else {
+      user.startActivityForReauthenticateWithProvider(getCurrentActivity(), builder.build())
+          .addOnSuccessListener(
+              authResult -> {
+                Log.w(TAG, "reauthenticateWithProvider:success");
+                promiseWithAuthResult(authResult, promise);
+              })
+          .addOnFailureListener(
+              e -> {
+                Log.w(TAG, "reauthenticateWithProvider:failure", e);
+                promiseRejectAuthException(promise, e);
+              });
+    }
+  }
+
   /** Returns an instance of AuthCredential for the specified provider */
   private AuthCredential getCredentialForProvider(
       String provider, String authToken, String authSecret) {
diff --git a/node_modules/@react-native-firebase/auth/ios/RNFBAuth/RNFBAuthModule.m b/node_modules/@react-native-firebase/auth/ios/RNFBAuth/RNFBAuthModule.m
index 90adc1c..68d59db 100644
--- a/node_modules/@react-native-firebase/auth/ios/RNFBAuth/RNFBAuthModule.m
+++ b/node_modules/@react-native-firebase/auth/ios/RNFBAuth/RNFBAuthModule.m
@@ -571,6 +571,63 @@ - (void)invalidate {
                 }];
 }
 
+RCT_EXPORT_METHOD(signInWithProvider
+                  : (FIRApp *)firebaseApp
+                  : (NSDictionary *)provider
+                  : (RCTPromiseResolveBlock)resolve
+                  : (RCTPromiseRejectBlock)reject) {
+  NSString *providerId = provider[@"providerId"];
+  if (providerId == nil) {
+    [RNFBSharedUtils rejectPromiseWithUserInfo:reject
+                                      userInfo:(NSMutableDictionary *)@{
+                                        @"code" : @"invalid-credential",
+                                        @"message" : @"The supplied auth credential is malformed, "
+                                                     @"has expired or is not currently supported.",
+                                      }];
+  }
+
+  __block FIROAuthProvider *builder = [FIROAuthProvider providerWithProviderID:providerId];
+  // Add scopes if present
+  if (provider[@"scopes"]) {
+    [builder setScopes:provider[@"scopes"]];
+  }
+  // Add custom parameters if present
+  if (provider[@"parameters"]) {
+    [builder setCustomParameters:provider[@"parameters"]];
+  }
+
+  [builder getCredentialWithUIDelegate:nil
+                            completion:^(FIRAuthCredential *_Nullable credential,
+                                         NSError *_Nullable error) {
+                              if (error) {
+                                [self promiseRejectAuthException:reject error:error];
+                                return;
+                              }
+                              if (credential) {
+                                [[FIRAuth auth]
+                                    signInWithCredential:credential
+                                              completion:^(FIRAuthDataResult *_Nullable authResult,
+                                                           NSError *_Nullable error) {
+                                                if (error) {
+                                                  [self promiseRejectAuthException:reject
+                                                                             error:error];
+                                                  return;
+                                                }
+
+                                                // NOTE: This variable has NO PURPOSE AT ALL, it is
+                                                // only to keep a strong reference to the builder
+                                                // variable so it is not deallocated prematurely by
+                                                // ARC.
+                                                NSString *providerID = builder.providerID;
+
+                                                [self promiseWithAuthResult:resolve
+                                                                   rejecter:reject
+                                                                 authResult:authResult];
+                                              }];
+                              }
+                            }];
+}
+
 RCT_EXPORT_METHOD(confirmPasswordReset
                   : (FIRApp *)firebaseApp
                   : (NSString *)code
@@ -983,6 +1040,68 @@ - (void)invalidate {
   }
 }
 
+RCT_EXPORT_METHOD(linkWithProvider
+                  : (FIRApp *)firebaseApp
+                  : (NSDictionary *)provider
+                  : (RCTPromiseResolveBlock)resolve
+                  : (RCTPromiseRejectBlock)reject) {
+  NSString *providerId = provider[@"providerId"];
+  if (providerId == nil) {
+    [RNFBSharedUtils rejectPromiseWithUserInfo:reject
+                                      userInfo:(NSMutableDictionary *)@{
+                                        @"code" : @"invalid-credential",
+                                        @"message" : @"The supplied auth credential is malformed, "
+                                                     @"has expired or is not currently supported.",
+                                      }];
+  }
+
+  __block FIRUser *user = [FIRAuth authWithApp:firebaseApp].currentUser;
+  if (user == nil) {
+    [self promiseNoUser:resolve rejecter:reject isError:YES];
+    return;
+  }
+
+  __block FIROAuthProvider *builder = [FIROAuthProvider providerWithProviderID:providerId];
+  // Add scopes if present
+  if (provider[@"scopes"]) {
+    [builder setScopes:provider[@"scopes"]];
+  }
+  // Add custom parameters if present
+  if (provider[@"parameters"]) {
+    [builder setCustomParameters:provider[@"parameters"]];
+  }
+
+  [builder getCredentialWithUIDelegate:nil
+                            completion:^(FIRAuthCredential *_Nullable credential,
+                                         NSError *_Nullable error) {
+                              if (error) {
+                                [self promiseRejectAuthException:reject error:error];
+                                return;
+                              }
+                              if (credential) {
+                                [user linkWithCredential:credential
+                                              completion:^(FIRAuthDataResult *_Nullable authResult,
+                                                           NSError *_Nullable error) {
+                                                if (error) {
+                                                  [self promiseRejectAuthException:reject
+                                                                             error:error];
+                                                  return;
+                                                }
+
+                                                // NOTE: This variable has NO PURPOSE AT ALL, it is
+                                                // only to keep a strong reference to the builder
+                                                // variable so it is not deallocated prematurely by
+                                                // ARC.
+                                                NSString *providerID = builder.providerID;
+
+                                                [self promiseWithAuthResult:resolve
+                                                                   rejecter:reject
+                                                                 authResult:authResult];
+                                              }];
+                              }
+                            }];
+}
+
 RCT_EXPORT_METHOD(unlink
                   : (FIRApp *)firebaseApp
                   : (NSString *)providerId
@@ -1043,6 +1162,69 @@ - (void)invalidate {
   }
 }
 
+RCT_EXPORT_METHOD(reauthenticateWithProvider
+                  : (FIRApp *)firebaseApp
+                  : (NSDictionary *)provider
+                  : (RCTPromiseResolveBlock)resolve
+                  : (RCTPromiseRejectBlock)reject) {
+  NSString *providerId = provider[@"providerId"];
+  if (providerId == nil) {
+    [RNFBSharedUtils rejectPromiseWithUserInfo:reject
+                                      userInfo:(NSMutableDictionary *)@{
+                                        @"code" : @"invalid-credential",
+                                        @"message" : @"The supplied auth credential is malformed, "
+                                                     @"has expired or is not currently supported.",
+                                      }];
+  }
+
+  __block FIRUser *user = [FIRAuth authWithApp:firebaseApp].currentUser;
+  if (user == nil) {
+    [self promiseNoUser:resolve rejecter:reject isError:YES];
+    return;
+  }
+
+  __block FIROAuthProvider *builder = [FIROAuthProvider providerWithProviderID:providerId];
+  // Add scopes if present
+  if (provider[@"scopes"]) {
+    [builder setScopes:provider[@"scopes"]];
+  }
+  // Add custom parameters if present
+  if (provider[@"parameters"]) {
+    [builder setCustomParameters:provider[@"parameters"]];
+  }
+
+  [builder getCredentialWithUIDelegate:nil
+                            completion:^(FIRAuthCredential *_Nullable credential,
+                                         NSError *_Nullable error) {
+                              if (error) {
+                                [self promiseRejectAuthException:reject error:error];
+                                return;
+                              }
+                              if (credential) {
+                                [user reauthenticateWithCredential:credential
+                                                        completion:^(
+                                                            FIRAuthDataResult *_Nullable authResult,
+                                                            NSError *_Nullable error) {
+                                                          if (error) {
+                                                            [self promiseRejectAuthException:reject
+                                                                                       error:error];
+                                                            return;
+                                                          }
+
+                                                          // NOTE: This variable has NO PURPOSE AT
+                                                          // ALL, it is only to keep a strong
+                                                          // reference to the builder variable so it
+                                                          // is not deallocated prematurely by ARC.
+                                                          NSString *providerID = builder.providerID;
+
+                                                          [self promiseWithAuthResult:resolve
+                                                                             rejecter:reject
+                                                                           authResult:authResult];
+                                                        }];
+                              }
+                            }];
+}
+
 RCT_EXPORT_METHOD(fetchSignInMethodsForEmail
                   : (FIRApp *)firebaseApp
                   : (NSString *)email
diff --git a/node_modules/@react-native-firebase/auth/lib/User.js b/node_modules/@react-native-firebase/auth/lib/User.js
index 8ac80ae..b7bc29b 100644
--- a/node_modules/@react-native-firebase/auth/lib/User.js
+++ b/node_modules/@react-native-firebase/auth/lib/User.js
@@ -96,6 +96,12 @@ export default class User {
       .then(userCredential => this._auth._setUserCredential(userCredential));
   }
 
+  linkWithProvider(provider) {
+    return this._auth.native
+      .linkWithProvider(provider.toObject())
+      .then(userCredential => this._auth._setUserCredential(userCredential));
+  }
+
   reauthenticateWithCredential(credential) {
     return this._auth.native
       .reauthenticateWithCredential(credential.providerId, credential.token, credential.secret)
diff --git a/node_modules/@react-native-firebase/auth/lib/index.d.ts b/node_modules/@react-native-firebase/auth/lib/index.d.ts
index 26daaf8..5b4d90d 100644
--- a/node_modules/@react-native-firebase/auth/lib/index.d.ts
+++ b/node_modules/@react-native-firebase/auth/lib/index.d.ts
@@ -109,6 +109,52 @@ export namespace FirebaseAuthTypes {
     credential: (token: string | null, secret?: string) => AuthCredential;
   }
 
+  /**
+   * Interface that represents an OAuth provider. Implemented by other providers.
+   */
+  export interface OAuthProvider {
+    /**
+     * The provider ID of the provider.
+     * @param providerId
+     */
+    // eslint-disable-next-line @typescript-eslint/no-misused-new
+    new (providerId: string): AuthProvider;
+    /**
+     * Creates a new `AuthCredential`.
+     *
+     * @returns {@link auth.AuthCredential}.
+     * @param token A provider token.
+     * @param secret A provider secret.
+     */
+    credential: (token: string | null, secret?: string) => AuthCredential;
+    /**
+     * Sets the OAuth custom parameters to pass in an OAuth request for sign-in
+     * operations.
+     *
+     * @remarks
+     * For a detailed list, check the reserved required OAuth 2.0 parameters such as `client_id`,
+     * `redirect_uri`, `scope`, `response_type`, and `state` are not allowed and will be ignored.
+     *
+     * @param customOAuthParameters - The custom OAuth parameters to pass in the OAuth request.
+     */
+    setCustomParameters: (customOAuthParameters: Record<string, string>) => AuthProvider;
+    /**
+     * Retrieve the current list of custom parameters.
+     * @returns The current map of OAuth custom parameters.
+     */
+    getCustomParameters: () => Record<string, string>;
+    /**
+     * Add an OAuth scope to the credential.
+     *
+     * @param scope - Provider OAuth scope to add.
+     */
+    addScope: (scope: string) => AuthProvider;
+    /**
+     * Retrieve the current list of OAuth scopes.
+     */
+    getScopes: () => string[];
+  }
+
   /**
    * Interface that represents an Open ID Connect auth provider. Implemented by other providers.
    */
@@ -315,7 +361,7 @@ export namespace FirebaseAuthTypes {
      * firebase.auth.OAuthProvider;
      * ```
      */
-    OAuthProvider: AuthProvider;
+    OAuthProvider: OAuthProvider;
     /**
      * Custom Open ID connect auth provider implementation.
      *
@@ -1212,6 +1258,30 @@ export namespace FirebaseAuthTypes {
      */
     linkWithCredential(credential: AuthCredential): Promise<UserCredential>;
 
+    /**
+     * Link the user with a federated 3rd party credential provider (Microsoft, Yahoo).
+     *
+     * #### Example
+     *
+     * ```js
+     * const provider = new firebase.auth.OAuthProvider('microsoft.com');
+     * const userCredential = await firebase.auth().currentUser.linkWithProvider(provider);
+     * ```
+     *
+     * @error auth/provider-already-linked Thrown if the provider has already been linked to the user. This error is thrown even if this is not the same provider's account that is currently linked to the user.
+     * @error auth/invalid-credential Thrown if the provider's credential is not valid. This can happen if it has already expired when calling link, or if it used invalid token(s). See the Firebase documentation for your provider, and make sure you pass in the correct parameters to the credential method.
+     * @error auth/credential-already-in-use Thrown if the account corresponding to the credential already exists among your users, or is already linked to a Firebase User.
+     * @error auth/email-already-in-use Thrown if the email corresponding to the credential already exists among your users.
+     * @error auth/operation-not-allowed Thrown if you have not enabled the provider in the Firebase Console. Go to the Firebase Console for your project, in the Auth section and the Sign in Method tab and configure the provider.
+     * @error auth/invalid-email Thrown if the email used in a auth.EmailAuthProvider.credential is invalid.
+     * @error auth/wrong-password Thrown if the password used in a auth.EmailAuthProvider.credential is not correct or when the user associated with the email does not have a password.
+     * @error auth/invalid-verification-code Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification code of the credential is not valid.
+     * @error auth/invalid-verification-id Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid.
+     * @throws on iOS {@link auth.NativeFirebaseAuthError}, on Android {@link auth.NativeFirebaseError}
+     * @param provider A created {@link auth.AuthProvider}.
+     */
+    linkWithProvider(provider: AuthProvider): Promise<UserCredential>;
+
     /**
      * Re-authenticate a user with a third-party authentication provider.
      *
@@ -1233,6 +1303,27 @@ export namespace FirebaseAuthTypes {
      */
     reauthenticateWithCredential(credential: AuthCredential): Promise<UserCredential>;
 
+    /**
+     * Re-authenticate a user with a federated authentication provider (Microsoft, Yahoo)
+     *
+     * #### Example
+     *
+     * ```js
+     * const provider = new firebase.auth.OAuthProvider('microsoft.com');
+     * const userCredential = await firebase.auth().currentUser.reauthenticateWithProvider(provider);
+     * ```
+     *
+     * @error auth/user-mismatch Thrown if the credential given does not correspond to the user.
+     * @error auth/user-not-found Thrown if the credential given does not correspond to any existing user.
+     * @error auth/invalid-credential Thrown if the provider's credential is not valid. This can happen if it has already expired when calling link, or if it used invalid token(s). See the Firebase documentation for your provider, and make sure you pass in the correct parameters to the credential method.
+     * @error auth/invalid-email Thrown if the email used in a auth.EmailAuthProvider.credential is invalid.
+     * @error auth/wrong-password Thrown if the password used in a auth.EmailAuthProvider.credential is not correct or when the user associated with the email does not have a password.
+     * @error auth/invalid-verification-code Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification code of the credential is not valid.
+     * @error auth/invalid-verification-id Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid.
+     * @param provider A created {@link auth.AuthProvider}.
+     */
+    reauthenticateWithProvider(provider: AuthProvider): Promise<UserCredential>;
+
     /**
      * Refreshes the current user.
      *
@@ -1722,6 +1813,41 @@ export namespace FirebaseAuthTypes {
      */
     signInWithCredential(credential: AuthCredential): Promise<UserCredential>;
 
+    /**
+     * Signs the user in with a federated OAuth provider supported by Firebase (Microsoft, Yahoo).
+     *
+     * From Firebase Docs:
+     * Unlike other OAuth providers supported by Firebase such as Google, Facebook, and Twitter, where
+     * sign-in can directly be achieved with OAuth access token based credentials, Firebase Auth does not
+     * support the same capability for providers such as Microsoft due to the inability of the Firebase Auth
+     * server to verify the audience of Microsoft OAuth access tokens.
+     *
+     * #### Example
+     * ```js
+     * // Generate an OAuth instance
+     * const provider = new firebase.auth.OAuthProvider('microsoft.com');
+     * // Optionally add scopes to the OAuth instance
+     * provider.addScope('mail.read');
+     * // Optionally add custom parameters to the OAuth instance
+     * provider.setCustomParameters({
+     *  prompt: 'consent',
+     * });
+     * // Sign in using the OAuth provider
+     * const userCredential = await firebase.auth().signInWithProvider(provider);
+     * ```
+     *
+     * @error auth/account-exists-with-different-credential Thrown if there already exists an account with the email address asserted by the credential.
+     * @error auth/invalid-credential Thrown if the credential is malformed or has expired.
+     * @error auth/operation-not-allowed Thrown if the type of account corresponding to the credential is not enabled. Enable the account type in the Firebase Console, under the Auth tab.
+     * @error auth/user-disabled Thrown if the user corresponding to the given credential has been disabled.
+     * @error auth/user-not-found Thrown if signing in with a credential from firebase.auth.EmailAuthProvider.credential and there is no user corresponding to the given email.
+     * @error auth/wrong-password Thrown if signing in with a credential from firebase.auth.EmailAuthProvider.credential and the password is invalid for the given email, or if the account corresponding to the email does not have a password set.
+     * @error auth/invalid-verification-code Thrown if the credential is a firebase.auth.PhoneAuthProvider.credential and the verification code of the credential is not valid.
+     * @error auth/invalid-verification-id Thrown if the credential is a firebase.auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid.
+     * @param provider A generated `AuthProvider`, for example from social auth.
+     */
+    signInWithProvider(provider: AuthProvider): Promise<UserCredential>;
+
     /**
      * Revokes a user's Sign in with Apple token.
      *
diff --git a/node_modules/@react-native-firebase/auth/lib/index.js b/node_modules/@react-native-firebase/auth/lib/index.js
index fd7cf89..0e73d20 100644
--- a/node_modules/@react-native-firebase/auth/lib/index.js
+++ b/node_modules/@react-native-firebase/auth/lib/index.js
@@ -372,6 +372,12 @@ class FirebaseAuthModule extends FirebaseModule {
     return this.native.revokeToken(authorizationCode);
   }
 
+  signInWithProvider(provider) {
+    return this.native
+      .signInWithProvider(provider.toObject())
+      .then(userCredential => this._setUserCredential(userCredential));
+  }
+
   sendPasswordResetEmail(email, actionCodeSettings = null) {
     return this.native.sendPasswordResetEmail(email, actionCodeSettings);
   }
diff --git a/node_modules/@react-native-firebase/auth/lib/providers/OAuthProvider.js b/node_modules/@react-native-firebase/auth/lib/providers/OAuthProvider.js
index 7ef56c6..3d8eabd 100644
--- a/node_modules/@react-native-firebase/auth/lib/providers/OAuthProvider.js
+++ b/node_modules/@react-native-firebase/auth/lib/providers/OAuthProvider.js
@@ -15,22 +15,56 @@
  *
  */
 
-const providerId = 'oauth';
-
 export default class OAuthProvider {
-  constructor() {
-    throw new Error('`new OAuthProvider()` is not supported on the native Firebase SDKs.');
-  }
+  /** @internal */
+  #providerId = null;
+  /** @internal */
+  #customParameters = {};
+  /** @internal */
+  #scopes: string[] = [];
 
-  static get PROVIDER_ID() {
-    return providerId;
+  constructor(providerId) {
+    this.#providerId = providerId;
   }
 
   static credential(idToken, accessToken) {
     return {
       token: idToken,
       secret: accessToken,
-      providerId,
+      providerId: 'oauth',
+    };
+  }
+
+  get PROVIDER_ID() {
+    return this.#providerId;
+  }
+
+  setCustomParameters(customOAuthParameters) {
+    this.#customParameters = customOAuthParameters;
+    return this;
+  }
+
+  getCustomParameters() {
+    return this.#customParameters;
+  }
+
+  addScope(scope) {
+    if (!this.#scopes.includes(scope)) {
+      this.#scopes.push(scope);
+    }
+    return this;
+  }
+
+  getScopes() {
+    return [...this.#scopes];
+  }
+
+  /** @internal */
+  toObject() {
+    return {
+      providerId: this.#providerId,
+      scopes: this.#scopes,
+      customParameters: this.#customParameters,
     };
   }
 }

@mikehardy
Copy link
Collaborator

@lpfrenette - I've just rebased this one against current main / 18.6.1 and solved the various conflicts etc - that will also cause it to generate a fresh set of patch-package patches available here https://github.com/invertase/react-native-firebase/actions/runs/6775452367?pr=7187

I'd appreciate any feedback you have on the PR, knowing that someone is using it gives me a lot more confidence merging it. Hoping to get this in sometime soon 🤞

@lpfrenette
Copy link
Contributor

lpfrenette commented Nov 7, 2023

@mikehardy I've applied the new patch. I managed to make it work with android, but with IOS i still get the invalid credential error. I will keep on investigating. But maybe in the doc you could add :

For azure mutli-tenant

provider.setCustomParameters({
              prompt: 'consent',
              tenant: '[tenant_name_or_id]',  //i.e. example.com or 9aaa9999-9999-999a-a9aa-9999aa9aa99a

            });

Not sure if i did it right, but I've created a PR for my changes in IOS and in the docs. I don't want to take the credit for the rest of the work that was done by @KielKing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Keep Open avoids the stale bot
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants