Skip to content

Commit 8461691

Browse files
lpfrenetteKielKingmikehardyMilutin-P
authored
feat(auth, oauth): support native oauth providers (#7443)
* (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 <[email protected]> * 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 <[email protected]> Co-authored-by: Mike Hardy <[email protected]> Co-authored-by: Milutin <[email protected]>
1 parent 8673f31 commit 8461691

File tree

15 files changed

+828
-303
lines changed

15 files changed

+828
-303
lines changed

docs/auth/social-auth.md

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -327,26 +327,56 @@ with the new authentication state of the user.
327327

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

330-
## Linking a Social Account with the Firebase Account
330+
## Microsoft
331331

332-
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.
332+
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`.
333+
Instead, we must use the native's Sign-In flow from the Firebase SDK.
333334

334-
```js
335-
import auth from '@react-native-firebase/auth';
336-
import { GoogleSignin } from '@react-native-google-signin/google-signin';
335+
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).
337336

338-
async function onGoogleLinkButtonPress() {
339-
// Check if your device supports Google Play
340-
await GoogleSignin.hasPlayServices({ showPlayServicesUpdateDialog: true });
341-
// Get the user ID token
342-
const { idToken } = await GoogleSignin.signIn();
337+
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
343338

344-
// Create a Google credential with the token
345-
const googleCredential = auth.GoogleAuthProvider.credential(idToken);
339+
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.
346340

347-
// Link the user with the credential
348-
const firebaseUserCredential = await auth().currentUser.linkWithCredential(googleCredential);
349-
// You can store in your app that the account was linked.
350-
return;
341+
```jsx
342+
import React from 'react';
343+
import { Button } from 'react-native';
344+
345+
function MicrosoftSignIn() {
346+
return (
347+
<Button
348+
title="Microsoft Sign-In"
349+
onPress={() => onMicrosoftButtonPress().then(() => console.log('Signed in with Microsoft!'))}
350+
/>
351+
);
351352
}
352353
```
354+
355+
`onMicrosoftButtonPress` can be implemented as the following:
356+
357+
```js
358+
import auth from '@react-native-firebase/auth';
359+
360+
const onMicrosoftButtonPress = async () => {
361+
// Generate the provider object
362+
const provider = new auth.OAuthProvider('microsoft.com');
363+
// Optionally add scopes
364+
provider.addScope('offline_access');
365+
// Optionally add custom parameters
366+
provider.setCustomParameters({
367+
prompt: 'consent',
368+
// Optional "tenant" parameter for optional use of Azure AD tenant.
369+
// e.g., specific ID - 9aaa9999-9999-999a-a9aa-9999aa9aa99a or domain - example.com
370+
// defaults to "common" for tenant-independent tokens.
371+
tenant: 'tenant_name_or_id',
372+
});
373+
374+
// Sign-in the user with the provider
375+
return auth().signInWithRedirect(provider);
376+
};
377+
```
378+
379+
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.
380+
381+
Upon successful sign-in, any [`onAuthStateChanged`](/auth/usage#listening-to-authentication-state) listeners will trigger
382+
with the new authentication state of the user.

docs/auth/usage/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,5 @@ method:
200200
- [Facebook Sign-In](/auth/social-auth#facebook).
201201
- [Twitter Sign-In](/auth/social-auth#twitter).
202202
- [Google Sign-In](/auth/social-auth#google).
203+
- [Microsoft Sign-In](/auth/social-auth#microsoft).
203204
- [Phone Number Sign-In](/auth/phone-auth).

packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@
2626
import com.facebook.react.bridge.Promise;
2727
import com.facebook.react.bridge.ReactApplicationContext;
2828
import com.facebook.react.bridge.ReactMethod;
29+
import com.facebook.react.bridge.ReadableArray;
2930
import com.facebook.react.bridge.ReadableMap;
31+
import com.facebook.react.bridge.ReadableMapKeySetIterator;
3032
import com.facebook.react.bridge.WritableArray;
3133
import com.facebook.react.bridge.WritableMap;
3234
import com.google.android.gms.tasks.OnCompleteListener;
35+
import com.google.android.gms.tasks.Task;
3336
import com.google.firebase.FirebaseApp;
3437
import com.google.firebase.FirebaseException;
3538
import com.google.firebase.FirebaseNetworkException;
@@ -877,6 +880,72 @@ private void signInWithCredential(
877880
}
878881
}
879882

883+
@ReactMethod
884+
private void signInWithProvider(String appName, ReadableMap provider, final Promise promise) {
885+
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
886+
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
887+
888+
if (provider.getString("providerId") == null) {
889+
rejectPromiseWithCodeAndMessage(
890+
promise,
891+
"invalid-credential",
892+
"The supplied auth credential is malformed, has expired or is not currently supported.");
893+
return;
894+
}
895+
896+
OAuthProvider.Builder builder = OAuthProvider.newBuilder(provider.getString("providerId"));
897+
// Add scopes if present
898+
if (provider.hasKey("scopes")) {
899+
ReadableArray scopes = provider.getArray("scopes");
900+
if (scopes != null) {
901+
List<String> scopeList = new ArrayList<>();
902+
for (int i = 0; i < scopes.size(); i++) {
903+
String scope = scopes.getString(i);
904+
scopeList.add(scope);
905+
}
906+
builder.setScopes(scopeList);
907+
}
908+
}
909+
// Add custom parameters if present
910+
if (provider.hasKey("customParameters")) {
911+
ReadableMap customParameters = provider.getMap("customParameters");
912+
if (customParameters != null) {
913+
ReadableMapKeySetIterator iterator = customParameters.keySetIterator();
914+
while (iterator.hasNextKey()) {
915+
String key = iterator.nextKey();
916+
builder.addCustomParameter(key, customParameters.getString(key));
917+
}
918+
}
919+
}
920+
Task<AuthResult> pendingResultTask = firebaseAuth.getPendingAuthResult();
921+
if (pendingResultTask != null) {
922+
pendingResultTask
923+
.addOnSuccessListener(
924+
authResult -> {
925+
Log.d(TAG, "signInWithProvider:success");
926+
promiseWithAuthResult(authResult, promise);
927+
})
928+
.addOnFailureListener(
929+
e -> {
930+
Log.d(TAG, "signInWithProvider:failure", e);
931+
promiseRejectAuthException(promise, e);
932+
});
933+
} else {
934+
firebaseAuth
935+
.startActivityForSignInWithProvider(getCurrentActivity(), builder.build())
936+
.addOnSuccessListener(
937+
authResult -> {
938+
Log.d(TAG, "signInWithProvider:success");
939+
promiseWithAuthResult(authResult, promise);
940+
})
941+
.addOnFailureListener(
942+
e -> {
943+
Log.d(TAG, "signInWithProvider:failure", e);
944+
promiseRejectAuthException(promise, e);
945+
});
946+
}
947+
}
948+
880949
/**
881950
* signInWithPhoneNumber
882951
*
@@ -1527,6 +1596,85 @@ private void linkWithCredential(
15271596
}
15281597
}
15291598

1599+
/**
1600+
* linkWithProvider
1601+
*
1602+
* @param provider
1603+
* @param promise
1604+
*/
1605+
@ReactMethod
1606+
private void linkWithProvider(String appName, ReadableMap provider, final Promise promise) {
1607+
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
1608+
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
1609+
1610+
if (provider.getString("providerId") == null) {
1611+
rejectPromiseWithCodeAndMessage(
1612+
promise,
1613+
"invalid-credential",
1614+
"The supplied auth credential is malformed, has expired or is not currently supported.");
1615+
return;
1616+
}
1617+
1618+
FirebaseUser user = firebaseAuth.getCurrentUser();
1619+
Log.d(TAG, "linkWithProvider");
1620+
1621+
if (user == null) {
1622+
promiseNoUser(promise, true);
1623+
return;
1624+
}
1625+
1626+
OAuthProvider.Builder builder = OAuthProvider.newBuilder(provider.getString("providerId"));
1627+
// Add scopes if present
1628+
if (provider.hasKey("scopes")) {
1629+
ReadableArray scopes = provider.getArray("scopes");
1630+
if (scopes != null) {
1631+
List<String> scopeList = new ArrayList<>();
1632+
for (int i = 0; i < scopes.size(); i++) {
1633+
String scope = scopes.getString(i);
1634+
scopeList.add(scope);
1635+
}
1636+
builder.setScopes(scopeList);
1637+
}
1638+
}
1639+
// Add custom parameters if present
1640+
if (provider.hasKey("customParameters")) {
1641+
ReadableMap customParameters = provider.getMap("customParameters");
1642+
if (customParameters != null) {
1643+
ReadableMapKeySetIterator iterator = customParameters.keySetIterator();
1644+
while (iterator.hasNextKey()) {
1645+
String key = iterator.nextKey();
1646+
builder.addCustomParameter(key, customParameters.getString(key));
1647+
}
1648+
}
1649+
}
1650+
Task<AuthResult> pendingResultTask = firebaseAuth.getPendingAuthResult();
1651+
if (pendingResultTask != null) {
1652+
pendingResultTask
1653+
.addOnSuccessListener(
1654+
authResult -> {
1655+
Log.d(TAG, "linkWithProvider:success");
1656+
promiseWithAuthResult(authResult, promise);
1657+
})
1658+
.addOnFailureListener(
1659+
e -> {
1660+
Log.d(TAG, "linkWithProvider:failure", e);
1661+
promiseRejectAuthException(promise, e);
1662+
});
1663+
} else {
1664+
user.startActivityForLinkWithProvider(getCurrentActivity(), builder.build())
1665+
.addOnSuccessListener(
1666+
authResult -> {
1667+
Log.d(TAG, "linkWithProvider:success");
1668+
promiseWithAuthResult(authResult, promise);
1669+
})
1670+
.addOnFailureListener(
1671+
e -> {
1672+
Log.d(TAG, "linkWithProvider:failure", e);
1673+
promiseRejectAuthException(promise, e);
1674+
});
1675+
}
1676+
}
1677+
15301678
@ReactMethod
15311679
public void unlink(final String appName, final String providerId, final Promise promise) {
15321680
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
@@ -1590,6 +1738,86 @@ private void reauthenticateWithCredential(
15901738
}
15911739
}
15921740

1741+
/**
1742+
* reauthenticateWithProvider
1743+
*
1744+
* @param provider
1745+
* @param promise
1746+
*/
1747+
@ReactMethod
1748+
private void reauthenticateWithProvider(
1749+
String appName, ReadableMap provider, final Promise promise) {
1750+
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
1751+
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
1752+
1753+
if (provider.getString("providerId") == null) {
1754+
rejectPromiseWithCodeAndMessage(
1755+
promise,
1756+
"invalid-credential",
1757+
"The supplied auth credential is malformed, has expired or is not currently supported.");
1758+
return;
1759+
}
1760+
1761+
FirebaseUser user = firebaseAuth.getCurrentUser();
1762+
Log.d(TAG, "reauthenticateWithProvider");
1763+
1764+
if (user == null) {
1765+
promiseNoUser(promise, true);
1766+
return;
1767+
}
1768+
1769+
OAuthProvider.Builder builder = OAuthProvider.newBuilder(provider.getString("providerId"));
1770+
// Add scopes if present
1771+
if (provider.hasKey("scopes")) {
1772+
ReadableArray scopes = provider.getArray("scopes");
1773+
if (scopes != null) {
1774+
List<String> scopeList = new ArrayList<>();
1775+
for (int i = 0; i < scopes.size(); i++) {
1776+
String scope = scopes.getString(i);
1777+
scopeList.add(scope);
1778+
}
1779+
builder.setScopes(scopeList);
1780+
}
1781+
}
1782+
// Add custom parameters if present
1783+
if (provider.hasKey("customParameters")) {
1784+
ReadableMap customParameters = provider.getMap("customParameters");
1785+
if (customParameters != null) {
1786+
ReadableMapKeySetIterator iterator = customParameters.keySetIterator();
1787+
while (iterator.hasNextKey()) {
1788+
String key = iterator.nextKey();
1789+
builder.addCustomParameter(key, customParameters.getString(key));
1790+
}
1791+
}
1792+
}
1793+
Task<AuthResult> pendingResultTask = firebaseAuth.getPendingAuthResult();
1794+
if (pendingResultTask != null) {
1795+
pendingResultTask
1796+
.addOnSuccessListener(
1797+
authResult -> {
1798+
Log.d(TAG, "reauthenticateWithProvider:success");
1799+
promiseWithAuthResult(authResult, promise);
1800+
})
1801+
.addOnFailureListener(
1802+
e -> {
1803+
Log.d(TAG, "reauthenticateWithProvider:failure", e);
1804+
promiseRejectAuthException(promise, e);
1805+
});
1806+
} else {
1807+
user.startActivityForReauthenticateWithProvider(getCurrentActivity(), builder.build())
1808+
.addOnSuccessListener(
1809+
authResult -> {
1810+
Log.d(TAG, "reauthenticateWithProvider:success");
1811+
promiseWithAuthResult(authResult, promise);
1812+
})
1813+
.addOnFailureListener(
1814+
e -> {
1815+
Log.d(TAG, "reauthenticateWithProvider:failure", e);
1816+
promiseRejectAuthException(promise, e);
1817+
});
1818+
}
1819+
}
1820+
15931821
/** Returns an instance of AuthCredential for the specified provider */
15941822
private AuthCredential getCredentialForProvider(
15951823
String provider, String authToken, String authSecret) {

0 commit comments

Comments
 (0)