Skip to content

Commit

Permalink
feat(auth, oauth): support native oauth providers (#7443)
Browse files Browse the repository at this point in the history
* (ANDROID ONLY) Add support for native OAuth Providers (Microsoft, Yahoo)
* (DRAFT) iOS Support
* (DRAFT) iOS Support - Fix typo
* (DRAFT) iOS Support - Fix premature memory deallocation by ARC

Methods such as getCredentialWithUIDelegate and linkWithCredential are asynchronous, meaning the local variable builder or user would be prematurely deallocated by ARC after the function has ended. Resolve this by using __block, ensuring that the variable is retained until the completion block has finished executing

* (DRAFT) iOS Support - Fix premature memory deallocation by ARC
* Update types of signInWithProvider, and linkWithProvider
* Fix typo in Android's linkWithProvider warning logs
* (iOS & Android) Add support for reauthentication with provider for completeness
* Fix typo in Microsoft documentation
* Fix TSC errors (Create new separate type OAuthProvider)
* Fix linting errors
* test(auth, oauth): patch up tests after rebase
* Fixed customeParameters in builder and updated doc for multi-tenant
* Removed typescript from js file
* chore: use Log.d for logging

.e and .w seem too high for things that can be expected
to happen based on user input

* test(auth): fix nesting too deep after conflict resolution

I think during the rebase where I merged main back into this
PR I got the nesting a little off here

* fix: reshape provider APIs to mirror firebase-js-sdk
* Update docs/auth/social-auth.md

Co-authored-by: Milutin <milutin.pesikan@gmail.com>

* style(lint): `yarn lint:markdown --write`

somehow this doesn't show up on macOS but it does on ubuntu
and ci checks are on ubuntu

---------

Co-authored-by: kielking <kielkingtop04@gmail.com>
Co-authored-by: Mike Hardy <github@mikehardy.net>
Co-authored-by: Milutin <milutin.pesikan@gmail.com>
  • Loading branch information
4 people authored Nov 27, 2023
1 parent 8673f31 commit 8461691
Show file tree
Hide file tree
Showing 15 changed files with 828 additions and 303 deletions.
62 changes: 46 additions & 16 deletions docs/auth/social-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,26 +327,56 @@ with the new authentication state of the user.

If you are testing this feature on an android emulator ensure that the emulate is either the Google APIs or Google Play flavor.

## Linking a Social Account with the Firebase Account
## Microsoft

If you want to provide users with an additional login method, you can link their social media account (or an email & password) with their Firebase account, which was created using any of the valid methods that `@react-native-firebase/auth` supports. The code is very similar to the login code (above.) You need to replace `auth().signInWithCredential()` in the scripts above with `auth().currentUser.linkWithCredential()`. An example of linking a Google account with a Firebase account follows.
Per the [documentation](https://firebase.google.com/docs/auth/android/microsoft-oauth#expandable-1), we cannot handle the Sign-In flow manually, by getting the access token from a library such as `react-native-app-auth`, and then calling `signInWithCredential`.
Instead, we must use the native's Sign-In flow from the Firebase SDK.

```js
import auth from '@react-native-firebase/auth';
import { GoogleSignin } from '@react-native-google-signin/google-signin';
To get started, please follow the prerequisites and setup instructions from the documentation: [Android](https://firebase.google.com/docs/auth/android/microsoft-oauth#before_you_begin), [iOS](https://firebase.google.com/docs/auth/ios/microsoft-oauth#before_you_begin).

async function onGoogleLinkButtonPress() {
// Check if your device supports Google Play
await GoogleSignin.hasPlayServices({ showPlayServicesUpdateDialog: true });
// Get the user ID token
const { idToken } = await GoogleSignin.signIn();
Additionally, for iOS, please follow step 1 of the "Handle sign-in flow" [section](https://firebase.google.com/docs/auth/ios/microsoft-oauth#handle_the_sign-in_flow_with_the_firebase_sdk), which is to add the custom URL scheme to your Xcode project

// Create a Google credential with the token
const googleCredential = auth.GoogleAuthProvider.credential(idToken);
Once completed, setup your application to trigger a sign-in request with Microsoft using either of the `signInWithPopup` or `signInWithRedirect` methods. The underlying implementation is the same and will not operate exactly as the firebase-js-sdk web-based implementations do, but will provide drop-in compatibility for a web implementation if your project has one.

// Link the user with the credential
const firebaseUserCredential = await auth().currentUser.linkWithCredential(googleCredential);
// You can store in your app that the account was linked.
return;
```jsx
import React from 'react';
import { Button } from 'react-native';

function MicrosoftSignIn() {
return (
<Button
title="Microsoft Sign-In"
onPress={() => onMicrosoftButtonPress().then(() => console.log('Signed in with Microsoft!'))}
/>
);
}
```

`onMicrosoftButtonPress` can be implemented as the following:

```js
import auth from '@react-native-firebase/auth';

const onMicrosoftButtonPress = async () => {
// Generate the provider object
const provider = new auth.OAuthProvider('microsoft.com');
// Optionally add scopes
provider.addScope('offline_access');
// Optionally add custom parameters
provider.setCustomParameters({
prompt: 'consent',
// Optional "tenant" parameter for optional use of Azure AD tenant.
// e.g., specific ID - 9aaa9999-9999-999a-a9aa-9999aa9aa99a or domain - example.com
// defaults to "common" for tenant-independent tokens.
tenant: 'tenant_name_or_id',
});

// Sign-in the user with the provider
return auth().signInWithRedirect(provider);
};
```

Additionally, the similar `linkWithRedirect` and `linkWithPopup` methods may be used in the same way to link an existing user account with the Microsoft account after it is authenticated.

Upon successful sign-in, any [`onAuthStateChanged`](/auth/usage#listening-to-authentication-state) listeners will trigger
with the new authentication state of the user.
1 change: 1 addition & 0 deletions docs/auth/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,5 @@ method:
- [Facebook Sign-In](/auth/social-auth#facebook).
- [Twitter Sign-In](/auth/social-auth#twitter).
- [Google Sign-In](/auth/social-auth#google).
- [Microsoft Sign-In](/auth/social-auth#microsoft).
- [Phone Number Sign-In](/auth/phone-auth).
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@
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;
Expand Down Expand Up @@ -877,6 +880,72 @@ private void signInWithCredential(
}
}

@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.d(TAG, "signInWithProvider:failure", e);
promiseRejectAuthException(promise, e);
});
} else {
firebaseAuth
.startActivityForSignInWithProvider(getCurrentActivity(), builder.build())
.addOnSuccessListener(
authResult -> {
Log.d(TAG, "signInWithProvider:success");
promiseWithAuthResult(authResult, promise);
})
.addOnFailureListener(
e -> {
Log.d(TAG, "signInWithProvider:failure", e);
promiseRejectAuthException(promise, e);
});
}
}

/**
* signInWithPhoneNumber
*
Expand Down Expand Up @@ -1527,6 +1596,85 @@ private void linkWithCredential(
}
}

/**
* 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.d(TAG, "linkWithProvider:failure", e);
promiseRejectAuthException(promise, e);
});
} else {
user.startActivityForLinkWithProvider(getCurrentActivity(), builder.build())
.addOnSuccessListener(
authResult -> {
Log.d(TAG, "linkWithProvider:success");
promiseWithAuthResult(authResult, promise);
})
.addOnFailureListener(
e -> {
Log.d(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);
Expand Down Expand Up @@ -1590,6 +1738,86 @@ private void reauthenticateWithCredential(
}
}

/**
* 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.d(TAG, "reauthenticateWithProvider:failure", e);
promiseRejectAuthException(promise, e);
});
} else {
user.startActivityForReauthenticateWithProvider(getCurrentActivity(), builder.build())
.addOnSuccessListener(
authResult -> {
Log.d(TAG, "reauthenticateWithProvider:success");
promiseWithAuthResult(authResult, promise);
})
.addOnFailureListener(
e -> {
Log.d(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) {
Expand Down
Loading

1 comment on commit 8461691

@vercel
Copy link

@vercel vercel bot commented on 8461691 Nov 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.