I'm using NextAuth's EmailProvider and FaunaAdapter with a FaunaDB instance to authenticate users in my NextJS application. I'm going down the "passwordless" route with this site, and the magic links are setup nicely after finishing the bulk of this tutorial.
There are some changes I've made to it, however, as I don't want a Sign Up page, and instead want only whitelisted emails that exist in my DB to be sent links to authenticate. I've done this by hijacking the signIn callback like so:
async signIn ({ user, account, profile, email, credentials }) {
// As "signIn" gets called twice when using magic links (once for sending and once for authenticating),
// we need to check here if this is a request for a magic link, as opposed to authenticating a token.
if (email.verificationRequest) {
// This is a helper function which does a lookup for the email in the "accounts"
// table of my Fauna DB.
return checkUserEmailExistsInAccounts(account.userId)
}
return true
}
This works nicely, but I wonder if it could be improved. I want any page that is behind the authentication wall to effectively have a session or JWT token that describes the user in more detail. If, say, I want the user name to be added to the session data, I then need to somehow modify the session or jwt callbacks to add this data. But that will involve yet another call to the database, where I have already made a perfectly useful call in the signIn process. Whats more, these callbacks are made frequently - whereas the signIn callback is only made a couple of times during the authentication process.
To summarise: how can I leverage a magic link sign-in system with NextAuth where I look up allowed emails from a pre-existing Fauna accounts collection, furnish a user object with the account data and have the account data become available to every page without needing further lookups when already-authenticated?
Edit: Just a note to say that when I first started playing with the library, I was surprised that simply putting the data into my accounts collection in Fauna DB didn't somehow magically make it appear in the session info. Perhaps I am not using the Fauna Adapter correctly, and this is actually the better way to go?
Related
I'm managing authentication state in a readable store which is grouped with a promise that resolves when the auth state is known (either signed in or out). The store is set internally via onAuthStateChange.
I'm trying to access this auth state from the server (+layout.server.ts and +page.server.ts) so that I can redirect the user to a sign-in page if they aren't authenticated, or load data from the database if (and only if) they are. No matter what I try, whenever I access this store from the server, its value is always null. I think this is because Firebase is only supposed to run on the client, although I'm not sure. Is there any way I can access this store from the server, or change my implementation so that Firebase runs in the server and passes auth state to the client? This blog post explains pretty much exactly what I want to do, but the solution here seems more complicated than it needs to be.
I've tried moving Firebase initialization code to the server (in both hooks.server.ts and +layout.server.ts), but there's no way for me to pass the auth object to the client because it can't be serialized (I get an error explaining this when I try to return it from a load function in +page.server.ts). I've also tried to handle authentication only using client-side code, but the server is responsible for loading data from the database, so in this case there's no way for me to verify a valid authentication state from the server.
author of the blog post you linked here.
You are correct in that the firebase client is only supposed to run on the client. If you want to access the Firebase services server-side you're supposed to use Firebase admin.
In my post I'm working around these limitations that firebase sets and unfortunately you do need that much code.
You are forced to do all the authentication in the frontend (firebase-client) and only afterwards you can inform the backend of the user info. (through the cookie)
Before I wrote this the ideal solution I had in my head was this:
Send the user to a login page that lists all the possible login providers.
The user chooses one and logs in.
the user gets redirected to a callback page where we can do something with the auth data in the backend.
I do think that something like this flow is possible if you're using custom tokens. But I'm sure that will be a whole load of even more complicated code.
TLDR; Can multiple different users be authenticated and retain authentication via a generated custom token IF that custom token for each of those users is being generated always by the same UID? That is, User1 gets custom token generated by UID1 (via createCustomToken(UID1)) and then signed-in with signInWithCustomToken(), THEN User2 gets and signs-in with custom token generated using UID1, then User3 same thing etc etc, can ALL these users happily remained logged-in and experience no interruptions despite these other users being authenticated in this identical manner?
Long Version:
Ok, so I am trying to create a link-sharing system wherein a user who navs to this link can access a specific subset of my project's Firebase resources.
I have already tried using Firebase's signInAnonymously() to do this, but I dont like the way that Firebase does this for a whole host of reasons I dont want to get into.
The way i want to accomplish this is by:
generating a unique link (really a Firestore unique doc ID with some access data stored in that doc)
having the unauthenticated user navigate to some landing page, calling the cloud function and passing that unique link (lets call it a UID now)
cloud function, upon recieving this UID, will createCustomToken(UID), returning the token back to calling user
and the user will authenticate themselves with signInWithCustomToken(returnedToken) and access provisioned resources
Now, that is all well and good, but my question is:
If two (or any amount more people) people navigate to that same link and therefore pass and create token with the same UID, will they all be ok to continue happily using Firebase resources? Or is it because they got tokens created for them which utilised the same UID a sort of token-conflict is made, and therefore any next user who authenticates in this manner will revoke the previous user's auth token.
I havent been able to try this, and it seems like every question asked about these custom tokens relates to the generation and expiry time of them, which I understand. I wish the Auth docs were more clear on the mechnics and pitfalls of using Custom Tokens. I also havent been able to try it myself as it would be quite alot of refactoring, and was hoping someone could give me a straight answer to this.
Yes, a user can login on multiple devices without affecting other sessions at the same time irrespective of which auth method you use.
I'm not sure what the unique links are but it's not a good idea to pass the UID itself around if you function just takes a UID and returns a custom token as UIDs are pretty short and just a random string. It might be best to add a custom signed JWT in the links that contain the UID in payload so you can verify them before creating Firebase Custom Tokens.
I want to use Firebase Auth for my user login/registration process. Everything else should be handled by my own backend (spring boot app + postgres db).
Now I'm asking myself how I can synchronize a new created user to my user table in postgres. I thought about the following:
REST call through client - Everytime I get a success event from the firebase sdk I call an additional request to my backend which sends uid, username etc.
Problem: What if my backend call fails but the register process was successful ? That would lead to an inconsistent state since (at least thats what I understanded) I can't easily rollback. That would lead to situations where a user can login into my app without my backend knowing the user. This would crash/ invalidate all my following queries (e.g. search after user xyz would lead to no result even though he/she exists)
Check the existence of the user in the postgres database
Here I would query the uid from the database (which I got from the jwt) and create a new user if it doesn't exists in every incoming request.
Problem: The user query is a unnessecary overhead for every incoming request.
Trigger with cloud functions - When I understood it right firebase auth is firing events when a new user is created in cloud functions. This could be used to make the external api call.
Problem: I dont know what happens when my external rest call fails at this point. Can I rollback the registration ? Will I be ever catch this event again ? I also proably would have an eventual consistency situation, since I dont know when the cloud function triggers. Furthermore I would prefer not to include cloud functions to my stack
Is there any way how I could do this in a transactional manner ? Did anyone else tried is using sth simular ?
Thanks for every help!
The easiest way is actually to not synchronize auth data, but instead decode and verify the ID token of the user in your backend code.
This operation is (by design) stateless, although Firebase's own backend services often implement a cache of recently decoded tokens to speed up future calls with the same ID token.
Apparently, I finally came up with a different solution:
Register user per Firebase SDK (e.g. with email + pw method)
Make a post-call to my own registration api including the resulting uid from the previous step and some metadata
API creates a new user including a column with the UID + Fetches the firebase token of the user and adds an internal claim that references to the internal Postgres UUID via Admin SDK.
Frontend gets the created user and hard refreshes (very important, since the previously fetched token won't contain the newly added claim !) the firebase token and verifies that it contains the token. If it does -> everything is cool, if not some oopsie happened :) That will require a request retry.
Later when you start your app you can just check if the passed token contains the custom claim, if not open the sign up/sign in page.
Every endpoint except the one for registration should check if the claim is set. If not just forbid the request.
How to set custom claims:
https://firebase.google.com/docs/auth/admin/custom-claims#set_and_validate_custom_user_claims_via_the_admin_sdk
You can use the Firebase Admin SDK to create the user account from your back-end instead of from the client.
So first you create the user in your database, then grab the ID and use it to create a user with the same ID in Firebase.
If all goes well, send a confirmation to the client and sign it in using the same credentials they entered.
Why not creating an endpoint in your backend service and call this endpoint when a client side authentication succeeds?
This method should do 2 things:
decode token to get access to Firebase user object (Firebase Admin)
Compare Firebase user with your internal user table. if it doesn't exist you can create it using firebase user object, otherwise do nothing.
This solution allows you to do other nice things as well (Syncing user info between Firebase and your internal db, providing a way to let a frontend know if this user is new or not, ...) at a relative small cost (1 get call per sign in)
For my website, I want to build my own login form for email/password based authentication using Firebase authentication instead of using FirebaseUI Web. I'll be using createUserWithEmailAndPassword JS function to create new user accounts. But how can I prevent spam registrations? Usually for web based forms, I would use Google Recaptcha and validate the recaptcha on my server. But here, I'm not using my server for creating the user accounts. I'm making a call on the client side to create the user accounts.
Of course, I'll be using email verification in the flow, but how would I prevent bots from creating the accounts in the first place?
I also understand that Firebase has some sort of limit for the number of requests per min from a single IP, but I would like to go further and try to prevent those registrations.
firebase.auth().createUserWithEmailAndPassword(email, password).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// ...
});
Thanks.
After 2 years, this question is still valid and as far as I see, it is not possible. You probably do not need an answer to this question anymore but it may help others. Even if you succeed in doing something manually, those js functions will stay there and can be called manually by any user who knows how to do it.
If there are no hidden, top secret options which are not available in the documentation, this is not possible. There is a recaptcha option but it is only for Phone Authentication. So, it seems like you have 2 options.
Ignore and delete users who do not verify their email address.
Disable email option from Firebase console and implement your own
email authentication. Generate your own token and log user into
Firebase with that custom token. https://firebase.google.com/docs/auth/web/custom-auth
I'm following up on frankish's answer. He is totally correct, and I agree I think it's strange that Firebase automatically integrates ReCaptcha when doing phone authentication (and now when doing Phone MFA), but does not provide support in createUserWithEmailAndPassword for passing a recaptcha verifier. Thus, the only way to really get around this is to do something like the following:
Set up ReCaptcha (either V2 or V3) manually on your signup page. Do NOT use firebase. auth. RecaptchaVerifier, that is only for integration with phone authentication.
Immediately after calling createUserWithEmailAndPassword, you need to make a call to your own server that passes up the recaptcha token. There is a Firebase blog post here about how to do that with a Firebase Function: https://firebase.googleblog.com/2017/08/guard-your-web-content-from-abuse-with.html. Note I think it's a bit strange that Firebase documented how to do this with server-side functions but didn't directly integrate this with account creation.
The final point is that in your server-side code, after you make the call to validate the recaptcha token, you need to set a custom claim on the Firebase user with the Firebase Admin API. That claim can be something like recaptchaPassed: true (or false). For details on custom claims see https://firebase.google.com/docs/auth/admin/custom-claims.
After that, you can then do things based on the value of that custom claim. For example you could read that custom claim in other server-side calls, or you can use it in Firestore security rules (good blog post on this, https://medium.com/google-developers/controlling-data-access-using-firebase-auth-custom-claims-88b3c2c9352a). You could also choose to immediately delete the user server-side (using the admin API), if recaptcha verification fails.
Note it's important to understand that there is nothing that guarantees that some malicious script will call your server-side token verification function after the code on the client calls createUserWithEmailAndPassword. Thus, the only way the rest of your code can guarantee that a particular Firebase user passed recaptcha verification is by looking for your custom claim that you set on the user server-side.
is it possible to get the user profile pic after the user has signedup and then signed in using meteor accounts. I tried querying
Meteor.user().services.google
or
Meteor.user().services.facebook
but I Meteor.user().services comes out as undefined. So I'm not quite sure how to access the profile pic.
Note I don't want to rewrite the signup process and Accounts.onCreateUser it would be much easier to simply add in user details after a user logs in.
There are two options but the best would be to write Accounts.onLogin because there is a security reason you can't access it with Meteor.user().services.
The stuff inside Meteor.user().services is sensitive, it contains the OAuth keys, your Meteor access tokens and your password hashes, this is why its blocked from access to the client. Its a very short CSF attack to stealing your account (and possibly get access to your facebook, google+, twitter, etc)
You can make it 'open'
Server side code
Meteor.publish("userdata", function() {
return Meteor.users.find({_id: this.userId});
});
Client side code
Meteor.subscribe("userdata")
More Secure Way
The more secure way would be to create the onLogin/onCreate callback, grab their profile picture url/other profile data and put it in profile and not have any publish functions like the one above
In your onLogin callback
var avatar = .. get your avatar url for whatever service you're using
//Assuming you've called the onLogin callback parameter 'info'
Meteor.users.update({_id: info.user._id}, {$set:{'profile.avatar':avatar}});
Then you can just do this when you need their profile pic
<img src="{{currentUser.profile.avatar}}"/>
This way you could do the same for the other data too! And standardize it in a way that works accross multiple services (facebook, google+, twitter, etc)