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 OpenID Connect provider #6574

Merged
merged 13 commits into from
Jan 27, 2023

Conversation

Babsvik
Copy link
Contributor

@Babsvik Babsvik commented Sep 24, 2022

Description

👷 I'm new to contributing to OSS, so please be nice 😅

An attempt at implementing support for OpenID Connect as provider.
I tested this using the Web SDK from Firebase first in React Native. After that worked I wanted to see if I could do the same using RNFirebase.
With some modifications that worked, so I decided to try to get that merge in to support Open ID Connect.
This is documented by Firebase for iOS here and Android here.

I also want to support Android also before merging this in, but for now I have only been focused on iOS.

One of the issues compared to other providers is that in order for Firebase to do the token exchange for you, the providerId can't be static. See screenshot below from the Firebase console.

Screenshot 2022-09-24 at 22 56 34

So it starts with oidc. but the rest is defined by the user. Since people can also have multiple OpenID Connect IdP's I think it makes sense to do this as a part of creating the credentials.

This is an example of how using it looked in my example:

// using react-native-app-auth to get oauth token from Azure AD
const config = {
  issuer: "https://login.microsoftonline.com/XXX/v2.0",
  clientId: "XXXX",
  redirectUrl: "msauth.your.bundle.id://auth/",
  scopes: ["openid", "profile", "email", "offline_access"],
  useNonce: false, // could not figure out how to get original nonce so ommiting that for now
};

// Log in to get an authentication token
const authState = await authorize(config);

const credential = auth.OIDCProvider.credential(
  "azure_test",
  authState.idToken
);

await auth().signInWithCredential(credential);

Related issues

Implementation of the idea @mikehardy suggested in #6305

Release Summary

Added support for OpenID Connect provider

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 do this later


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

Edit: Updated Android support to be checked (since it has been implemented)

@vercel
Copy link

vercel bot commented Sep 24, 2022

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

Name Status Preview Comments Updated
react-native-firebase ✅ Ready (Inspect) Visit Preview 💬 Add your feedback Jan 27, 2023 at 5:27PM (UTC)
1 Ignored Deployment
Name Status Preview Comments Updated
react-native-firebase-next ⬜️ Ignored (Inspect) Jan 27, 2023 at 5:27PM (UTC)

@Babsvik
Copy link
Contributor Author

Babsvik commented Sep 24, 2022

Hey @mikehardy started this PR draft as mentioned in #6305

I'm quite new to contributing to OSS, so feel free to give me any pointers. I would really like collaborate on how we could get support for OpenID Connect into RNFirebase.
I tried to explain why the providerId needs to be "dynamic" above. Please let me know if you need any clarifications.
Also I did not look at the Android side of things yet, just started with iOS (gotta start somewhere).

@codecov
Copy link

codecov bot commented Sep 25, 2022

Codecov Report

Merging #6574 (85fe346) into main (54f6012) will decrease coverage by 0.06%.
The diff coverage is 44.45%.

❗ Current head 85fe346 differs from pull request most recent head 658929d. Consider uploading reports for the commit 658929d to get more accurate results

@@             Coverage Diff              @@
##               main    #6574      +/-   ##
============================================
- Coverage     54.08%   54.02%   -0.05%     
+ Complexity      700      690      -10     
============================================
  Files           218      219       +1     
  Lines         10800    10809       +9     
  Branches       1700     1701       +1     
============================================
- Hits           5840     5839       -1     
- Misses         4644     4677      +33     
+ Partials        316      293      -23     

Copy link
Collaborator

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

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

Very interesting - I like how the change itself is nearly zero lines of code so it's very easy to see how oidc works (in combination with your screenshot). Certainly not a tough change, I could see it going in just like this. It will need documentation for users as part of the PR and my hunch is that will be the largest part of the diff based on just the idea of oidc.suffix and sending it in

@mikehardy
Copy link
Collaborator

This should fix up the objective-c formatting (where "fix" may actually still look ugly but at least uniformly ugly across whole project)

"lint:ios:fix": "clang-format -i --glob=\"packages/**/ios/**/*.{h,cpp,m,mm}\" --style=Google",

@Babsvik
Copy link
Contributor Author

Babsvik commented Sep 25, 2022

Thanks a lot @mikehardy really appreciate the help and the feedback!

I'l write up the docs when we have landed on an implementation that seems good, no problem 🙂

I'm not sure what is the best approach for RNFirebase. But I'l try to outline some of my thoughts on them.

When implementing OpenID Connect on the web, we can use signInWithRedirect or signInWithPopup. Since these are not supported in native signInWithCredential is the only option when comparing to the web sdk.

Adding OIDC provider (what I'm doing in this PR and explained above)
This can be achieved with the adding a OIDC provider like I am doing in this PR. This requires that people who wants to support OpenID Connect using RNFirebase need to implement the OpenID Connect sign-in flow themselves (like I did with react-native-app-auth above). I choose this because it seemed to be the most feasible.

I also want to add Android support here and I'm also going to try to include nonce into making the credential as an optional argument.

An implementation that uses Firebase for the OpenID Connect sign-in flow
Firebase also have built in methods in native to handle the OpenID Connect sign-in flow on iOS and Android. But that requires us to add a lot to the codebase and refactor some of the other pieces.

Doing this would give us an implementation that would not require the user to handle the OAuth flow themselves and would be a richer implementation (although I have not tested this myself).

The way I see it, we would then need to support making a new instance of the OAuthProvider.
We could and should also then support the other possible methods like settings custom parameteres and adding scopes.

Like I said, this would require more work and have a higher complexity compared to what I am suggesting in this PR.

If we did this, an example of how to use an implementation like that could look something like this:

const provider = new OAuthProvider('oidc.example-provider');

provider.setCustomParameters({
  // Target specific email with login hint.
  login_hint: 'user@example.com'
});

provider.setScopes(['mail.read', 'calendars.read']);

// The methods we would need to use are named "getCredentialWith" on iOS and "startActivityForSignInWithProvider" on Android
// so "getCredential" made the most sense to me for a unified implementation in RNFirebase
const credential = await provider.getCredential(); 

await auth().signInWithCredential(credential);

Would you be open to adding the OpenID Connect provider first and loop back in a future PR on an implementation like the one I explained above? The way I see it, one is not better then the other - but both have different use cases.

I would be motivated to work towards supporting OpenID Connect using only RNFirebase after that, but I would need more help to get there. I think I lack a little experience on the native side to be able to complete this by myself. But I think the benefit of not having to handle the OAuth flow to use OpenID Connect and having the OAuth configuration come from Firebase automatically is quite big.

Again, thank you for your feedback.
I'l try to whip together Android support tomorrow and possibly draft up some docs for the OIDC provider 🙂

@mikehardy
Copy link
Collaborator

I think a plan that involves incremental progress (that is, a PR like this) along with a future target that might get us most of the way to supporting generic OAuth even would be a dream really. Supporting general OAuth has been something I've wanted for a long time but did not have a personal use case important enough to support my time working on it above general maintenance + wrapping whole new modules here

…vider to OIDCAuthProvider to be more consistent with the naming of other providers.
@Babsvik
Copy link
Contributor Author

Babsvik commented Sep 27, 2022

That sounds like a good plan @mikehardy !

I will focus on this pr for now, and we can revisit the general support for OAuth in the future.

I implemented and tested on the Android side of things now. It works in my app now with both iOS and Android. I also added tests similar to the once that exists for the other providers.

I decided to return early in ReactNativeFirebaseAuthModule.java because a switch case was used before. I didn't see any better way to implement it.

This time I also linted all the code before commiting 🙌

I will try to draft up some docs next and I would love any feedback on that.
The general idea for me was to write up a chapter under "Authentication" called "OpenID Connect Auth" and write out all the steps required to make it work.

Do you think it's best compared to the rest of the docs to:

  1. explain that the OAuth flow needs to be handled outside of RNFirebase and suggest react-native-app-auth (it's also suggested in the RN docs)
  2. write up a complete example of how you would do it and include the code for react-native-app-auth?

Should I also go through the steps of the Firebase Console or is that generally a bad idea, since it's hard to stay up to date when the Firebase console changes in the future?

@mikehardy
Copy link
Collaborator

Fantastic - for the docs, skip the console screens but do your best to describe them, recommend react-native-app-auth, and if you have any code snippets that could be really useful.
Honestly the hardest part of me on the docs when adding a new page is adjusting all the prev/next links at the top 😆 - by the time you get to writing docs usually the subject matter is pretty well understood

It's normal to have to add a bunch of things to the spellcheck dictionary, for what it's worth, it'll tell you what's missing on a CI run

@mikehardy mikehardy added the Workflow: Waiting for User Response Blocked waiting for user response. label Oct 17, 2022
@Salakar
Copy link
Member

Salakar commented Dec 5, 2022

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

@Salakar Salakar added the Stale label Dec 5, 2022
@Babsvik
Copy link
Contributor Author

Babsvik commented Dec 6, 2022

Hey, sorry for the delay, have been swamped lately. I'l draft up the docs as soon as I have time, hopefully within a week or two 🙂

@github-actions github-actions bot removed the Stale label Dec 6, 2022
@paulrostorp
Copy link
Contributor

Lifesaver @Babsvik 🏆

@Babsvik
Copy link
Contributor Author

Babsvik commented Dec 20, 2022

@paulrostorp Did you try to use it? Appreciate the feedback, happy to help 🙂

@Babsvik
Copy link
Contributor Author

Babsvik commented Jan 3, 2023

Hey, is there anything I can do to help move this forward? 🙂

@mikehardy
Copy link
Collaborator

Hey there - thanks for your patience - no further external action is needed to move it forward, I just need my kid back in school from all the holidays (this happened yesterday 😄 ) and then to catch back up with all the repos I maintain (in progress now! 🏃 🏃 )

@happyfloat
Copy link

I just applied it with patch-package and it seems to work quite well! (tested it with microsoft active directory).
But there is a mistake in the documentation:
const credential = auth.OIDCProvider.credential(
should be
const credential = auth.OIDCAuthProvider.credential(

Thanks ❤️

@Babsvik
Copy link
Contributor Author

Babsvik commented Jan 18, 2023

Good catch @happyfloat! I refactored that to align more with the other auth classes and forgot to update the docs 🙂 Thanks! I did a new commit now and synced the fork with the main branch.

Edit: Updated the message because I commented before I was done writing 😅

@happyfloat
Copy link

@Babsvik Thanks 🙏 . Maybe off topic, but do you have any recommendation on how to handle the refresh token of the third party authentication or validity/change of the idToken relative to the firebase authentication?

Do you check the expiry yourself and then call firebase reauthenticateWithCredential or logout?

Normally I just call await auth().currentUser.getIdToken() and firebase will automatically refresh the token if expired and still valid. This still works, but does not take into account a change in the third party token.

I hope this question makes sense...

@mikehardy
Copy link
Collaborator

I am getting closer to "current" on my reviews and such, let's get this merged :-)
I reached into your branch and committed a change to spellcheck.dict that should clear that CI check, we'll see how the others go and this should get a real review+merge shortly

Thanks for your patience!

Copy link
Collaborator

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

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

code review looks great - pending merge - thank you!

@mikehardy mikehardy added Workflow: Pending Merge Waiting on CI or similar and removed Workflow: Waiting for User Response Blocked waiting for user response. labels Jan 27, 2023
@mikehardy mikehardy merged commit 469bf00 into invertase:main Jan 27, 2023
@mikehardy mikehardy removed the Workflow: Pending Merge Waiting on CI or similar label Jan 27, 2023
@rolfb
Copy link

rolfb commented Feb 9, 2023

This seems great!

Had a go at implementing it, but keep hitting an issue with the error message when setting Firebase's __/auth/handler as the authorizationEndpoint on iOS (haven't tested Android yet):

Unable to process request due to missing initial state.
This may happen if browser sessionStorage is inaccessible or accidentally cleared.

The only difference I can see from the example used is that I'm using Expo Auth Session instead of React Native App Auth. Any hints to what I should check? Using the OAuth Flow with Firebase JS with the OIDC provider works perfectly in a web-based prototype.

@Babsvik Babsvik deleted the adding-oidc-provider branch March 1, 2023 08:09
@Babsvik
Copy link
Contributor Author

Babsvik commented Mar 1, 2023

@Babsvik Thanks 🙏 . Maybe off topic, but do you have any recommendation on how to handle the refresh token of the third party authentication or validity/change of the idToken relative to the firebase authentication?

Do you check the expiry yourself and then call firebase reauthenticateWithCredential or logout?

Normally I just call await auth().currentUser.getIdToken() and firebase will automatically refresh the token if expired and still valid. This still works, but does not take into account a change in the third party token.

I hope this question makes sense...

Hey @happyfloat !
I wish there was a simple short answer to this, but the truth is it all depends on a magnitude of factors. I have choose in our case at my company to treat the exchange as a credential and once the user has exchanged it I don't really keep track of the original token (I do however make sure they are logged out when logging out of Firebase).

To make a simple comparison: when the user does a login you expect them to have their password, but they don't have to type their password again next time they open the app? But if they are going to do something like changing their personal information or something that has elevated privileges maybe you want them to type their password again? I think of the token from IdP as the password in this case. I don't know if this is best practice or even considered safe, but for our use-case this is sufficient.

Remember that Firebase uses JWT tokens, so they are also stateless. So unless you manually build something on top that denies specific tokens or use the built in tokenExpiry from firebase when verifying the token on the server it won't have any effect to revoke the tokens.

So to answer your question more specifically: you can build something yourself that tracks the original token and does reauthenticateWithCredential, if need to "keep up" with the IdP token. But that is something you need to judge for yourself based on the security requirements, architecture and probably a lot of other considerations for your application.

Hope this helps 🙂

@Babsvik
Copy link
Contributor Author

Babsvik commented Mar 1, 2023

This seems great!

Had a go at implementing it, but keep hitting an issue with the error message when setting Firebase's __/auth/handler as the authorizationEndpoint on iOS (haven't tested Android yet):

Unable to process request due to missing initial state.
This may happen if browser sessionStorage is inaccessible or accidentally cleared.

The only difference I can see from the example used is that I'm using Expo Auth Session instead of React Native App Auth. Any hints to what I should check? Using the OAuth Flow with Firebase JS with the OIDC provider works perfectly in a web-based prototype.

Hey @rolfb

This is just a wild guess from seeing a lot of different messages working with SSO over the years, but my guess would be that it has something to do with nonce or state. See this strack overflow answer for more details about what state and nonce is.

In the current implementation of OIDC that we just added, we currently don't do anything with the nonce. So if the nonce is present in the token, the authentication will fail because we did not send a raw nonce (but it's in the token). You could try to decrypt the token that is being sent to Firebase and check if it has a nonce in it to verify this. This could be fixed by passing the rawNonce to the FIROAuthProvider credentialWithProviderID. I will link to the code in the PR here. I would fix this if I had an environment I could test it in and time, but I have neither right now.

Hope you figure it out 🙂

@rolfb
Copy link

rolfb commented Mar 1, 2023

Hey @Babsvik,

Thank you for getting back to me. I'll look into the link you provided, but I have gotten a bit further since my last post. At the moment I seem to get to a page which looks blank but has a javacript call to fireauth.oauthhelper.widget.initialize() which doesn't seem to do anything on iOS (haven't tried Android yet).

Another thing I'm having some issues to understand is how you exchange the code for a token in your example. react-native-app-auth doesn't seem to do this automatically with Firebase which is sort of the point by storing the client_secret in Firebase Authentication. Having it in the app is considered insecure and Firebase Authentication recommends pointing the authorizationEndpoint towards __/auth/handler which doesn't seem to work properly if you do?

Do you know of a more custom way to exchange a code for a token with Firebase Authentication with React Native Firebase after getting back from the third party oidc provider?

@Babsvik
Copy link
Contributor Author

Babsvik commented Mar 1, 2023

No idea about your first paragraph.

My understanding is that the secret is not stored in the app. But it is stored internally in the Firebase infrastructure (when you add it in the console) and when you do credential and signInWithCredential it's actually a call to the Firebase backend to verify and create the Firebase token for you.

I can't say for sure, but it seems like you are confusing differences between web/ios/android and some parts of the authentication process.

What we are doing in RN Firebase is largely this.
My understand of how everything works, very roughly explained:

  • auth with OpenID Connect IdP manually
  • Take idToken, send it to Firebase, Firebase make a backend request so we can auth with Firebase (using the data stored in the console, like secret)
  • Firebase on the serverside verifies the idToken (against the data added in console) if ok, creates a Firebase token for the user

Best of luck to you 🙂

@happyfloat
Copy link

@Babsvik thanks for your response :). To clarify my use case: I want to logout/deactivate the user in my app, if he/she left the parent company or the account got deactivated at their company identity provider. One of the main benefits for SSO?
Anyway, I will figure it out later. Keeping track of the token myself and calling reauthenticateWithCredential. Thanks :)

@rolfb
Copy link

rolfb commented Mar 2, 2023

Thanks!

My understanding is something like this:

  1. Authenticate with OpenID Connect Provider, get code back (code, state, scope params)
  2. Exchange code for ID Token (this is where the client_secret is required)
  3. Send ID Token to Firebase to sign in

Seems to stop at 2. when authorization step returns to the Firebase Auth Handler and I'm not sure why.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants