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, oauth): support native oauth providers #7443

Merged
merged 19 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
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
mikehardy marked this conversation as resolved.
Show resolved Hide resolved
## 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
Loading