Auth: what about signing up?

I’ve just caught up with all the auth goodness that’s been introduced in the last few weeks, and was looking at maybe integrating a new provider… but in the larger scheme of things, something struck me: nowhere is there any discussion about how to handle signups?

There is an hint in the lib/auth.js generated file about how you’d map an authenticated user to a DB user record:

// Define what you want `currentUser` to return throughout your app. For example,
// to return a real user from your database, you could do something like:
//
//   export const getCurrentUser = async ({ email }) => {
//     return await db.user.findOne({ where: { email } })
//   }

And that makes sense… but how would you get that email into the DB in the first place?

Given the following scenario:

  1. New user comes to my webapp for the first time
  2. Clicks “sign in with Google”, for instance
  3. Comes back from Google sign-in with a token (in the sample code, I guess the intent was to show how to handle the response from a Custom provider that uses email/password credentials)
  4. A User is created if it’s not present already

I don’t see where Redwood is helping me do that fourth step?
Maybe I’m stuck in the 2000-web and I missed the point when having a User model became irrelevant? :sweat_smile:

But I do think there are still use cases requiring you to have that model, so that a User can be enriched with app-specific attributes, and associated to different objects in the DB with proper relations…

Should I…

… add an API endpoint to create the User model upon signup?

  • Implies detecting signup vs. signin on the Web side, which might not be practical?
    • unless there is a separate signup button, but that UX feels outdated

… use db.user.upsert({ ... }) instead of findOne?

  • I guess it best follows the idea that a user would just have to “sign in” on my app, and I’d seamlessly create whatever my app needs to work on the fly
  • but what if I want a new user to go through an onboarding process upon signing up?
    i.e. redirect to onboarding route on the Web side when user is signing-in for the first time, which is detected on the API side with this approach…

In the spirit of Redwood providing sane defaults for fullstack web app development, I feel we should at least be given directions/best practices to handle signups in the documentation, if not a solution suitable for a majority of cases.

How do you all handle this today?

3 Likes

I’m working on a Redwood app and do this in api/src/lib/auth.js:

import { AuthenticationError } from '@redwoodjs/api'
import { db } from './db'

export const getCurrentUser = async ({ name, email }) => {
  const user =
    (await db.user.findOne({
      where: { email },
    })) || (await createUser(name, email))
  return user
}

const createUser = (name, email) => {
  return db.user.create({
    data: { name, email }
  })
}

I’m using the Netlify Identity widget for my login. So when you log in and the system attempts to retrieve the details for that user (looked up with the email address that Netlify is returning) it will return that user (if found) or create a new user, and then return that one.

This may be handled a little cleaner with Prisma’s upsert call, but I don’t love the syntax (you need to provide both update and create parameters, even if you don’t care about updating and just want to do the create if the record doesn’t exist). I really wish they had an equivalent of Rails first_or_create which either returns the first record, or creates it if it doesn’t exist. Maybe Redwood can add our own version of that…

I’m not sure if Netlify has webhooks when new users sign up, but if so that could be another way to handle it—create a custom function that creates a new user and have that function’s URL be the webhook callback URL.

1 Like

I just made myself an Issue to add this to the Cookbook: https://github.com/redwoodjs/redwoodjs.com/issues/212

Looked into it and Netlify does have webhooks for user signup:

I’m not 100% comfortable with my current solution where the user creation logic lives right next to my auth logic…webhooks may be a cleaner way to handle it. But that becomes one more integration with your app and a third party service that you have to make sure to setup/maintain in future installs. Hmmm…

I can 100% sympathise with this feeling - at the end of the day I couldn’t really figure out a nice way to handle that for all the providers that we want to support.

One way to think about it is that the user is already logged in or signed up (via the provider) and we’re mapping their representation of the user onto our local representation, but maybe there’s a way that we can abstract that into separate functions, instead of having branch logic… :man_shrugging:

This topic made me hesitant a lot.

The reason is because RW supports and encourages providers for auth - which is not in discussion.
What I would debate is the actual need of handling any user information on the app side. How much should we really host?

I’d argue that the only reasonable option would be to map, as per @peterp suggestion. Duplicating the user’s personal information when they come from a provider is rarely a good option ( as you’d need to keep track of it ) and at the end of the day, if it has been a real messy day and you got intruded… you’d be glad to only give away emails. Identity is best kept separated from the rest of an app.

What I would favor would be the implementation of mappers for each providers, but on the RW database we should only store one email and one id, the keys to match all needed data together while never exposing too much.
Eventually the model would be expanded to have some configuration fields or date fields, but that’s something else.

I like where you coming from, and I think in a GDPR world it’s a great idea to ignore personally identifiable information, and instead settle on a “stable uuid” that represents a user. It could be an email address, but it could also just be a sub

So maybe we could provide a lean User model, as you suggested, and then a Profile model where you could associate additional information.

I don’t think duplicating information to some extent can be avoided. E.g. transactional emails require an email address.

There are best practices for sure. But a large majority of applications are going to encounter this.

1 Like

When I was saying I wasn’t 100% comfortable with my solution, I wasn’t thinking Redwood should be doing it for me, I just felt a little weird with what I came up with (creating a user in the same block of code that’s looking up an authenticated user). At the time I couldn’t think of a more elegant way to do it, but I bet there is one! Creating the user as the result of a webhook feels like a better separation of concerns to me right now…

Devs are always going to have unique needs when it comes to storing user info, I don’t think we can include standard User models or try to dictate what you can and can’t saved about a user, it’s always going to be unique situation for every app created. Even the concept of signing up, some apps may flip the process 180°—have you create your user locally and then make an API request to the auth system to create that user over there.

We can come up with a list of best practices where we recommend “you should really only store the ID from the 3rd party identity system and let them worry about everything else, for these security and GDPR reasons…”

But even then, there going to be processes like sending email, or background jobs, where it’s not going to be realistic to make hundreds or thousands of requests to Netlify Identity (or whoever) to get the name and email of everyone registered for your site, versus a single query against your own database and a loop.

Yeah, what Rob said :point_up:

@noire.munich Curious if you have regulation/privacy issues that make this especially challenging in your region?

Wow, I wasn’t expecting to spark such a long discussion :sweat_smile:

I’m not a huge fan of this, I think most of the time when you do need a User model for your website to function normally, you need the User creation to be synchronous.
In my experience, webhooks are some of the least trustable mechanisms of the Web :stuck_out_tongue:
You never know when they’ll finally come in, and you do know there will be some misfires/calls that will never come. I can’t figure any good UX where you’d make a new user wait for the webhook to come before actually signing them in.

So I’d much rather have a synchronous solution, and I’m actually quite OK with the upsert approach, although it’s not straightforward how I would redirect a new user to an onboarding page when they sign in for the first time.

Definitely in agreement with that. But it’s highly dependent on the use case and would probably just be done by each dev to the extent they’re comfortable with?

GDPR does not prevent you from collecting data such as the email address or name of someone signing up for your service :slight_smile:
Plus, I don’t think Redwood should ever position itself on legal matters, that’s a very slippery slope.

Indeed, but don’t you think we could come up with some ways to help developers figure out ways to make it happen easily? Not just about what data to copy or not, but more on the flow one can implement to get both sign up and sign in working together.

1 Like

Absolutely, I was more responding to what sounded like a suggestion that Redwood ship with User and Profile models with certain properties, which I don’t think is something we want to be in the business of doing.

Ah, yes I agree with that ^^

We should probably find ways to provide “opportunities” to create a new model, whatever it is, from the metadata of a user who signed in for the first time, and then let developers build up from that!

1 Like

Can we support something like a parameter in redirect_uri? For example, http://localhost:8910/?loginResult=success. If this parameter is available in the redirect_uri, redwoodjs can provide a handler (e.g. newSession) to create user. newSession is called when there is loginResult=success param.

2 Likes

@noire.munich Curious if you have regulation/privacy issues that make this especially challenging in your region?

I’d have more of a concrete use case :). My last job was CTO in a small company in the e-learning business in France. We were dealing with clients from all over the world, as much from the private sector than from the public one. We were required to satisfy the laws of each different countries and the preferences of each different customers ( some were harsher than their own countries on these matters ) when it comes to RGPD. Not only were we bound to our local laws, but we were meant to help our international customers abide by their own. South American universities and companies were more worried than our other customers, for instance. Apps can deal with requirements from different regions at the same time, it really depends on the audience.

On the tech side it meant being able to connect to various auth providers with rules specific to each client, so end users would access our app. They’d use the same access point and would never know the variety of providers we were actually working with.
Anyway, that is the most extreme case I’ve had to work with related to auth, RGPD and signing up, it would be irrelevant to make it count too much in RW’s design.

Back to the issue at hand.

I appreciate a lot the flexibility RedwoodJS gives us and having some auth providers supported out of the box is awesome. In this regard I would somehow expect to find sign up support for said providers. I implemented firebase with the help of the team and would see it very serviceable if in the mean time full signup was provided using firebase’s createUser methods. This would delegate the opinion on where to store some data :), and if I wanted some info which couldn’t be stored on firebase I could still develop my own Profile model to my own discretion. That could be an idea to play with.

@rob I’m not sure I properly understand what you would expect from Redwood in this matter, could you elaborate? Also about the need to store some duplicated data vs the cost and risks of making calls - there’s no debate, we agree on that point :). I was more thinking about the kind of details that an app doesn’t need to know about in order to let the user use it efficiently on most screens/features.

1 Like

Ahhhh interesting…

It looked to me like the conversation was steering towards making canonical User and/or Profile models in any newly created Redwood app and I don’t think that’s an approach we should pursue—Redwood (the codebase) should not take any stance on what data you can/should store about a user in your local DB. We could totally talk about best practices and give recommendations in our documentation/tutorials, but I would never try to enforce those requirements in the framework itself.

3 Likes

Thanks, it’s clearer to me now :).
I wouldn’t see offering a basis as taking a stance, specially if the basis is kept at its bare modest and unopiniated minimum, but I get the logic and recos + docs are very valid solutions.

I’ve created a PR that handles a createUser with Firebase: https://github.com/redwoodjs/redwood/pull/783

Firebase returns a promise on a successful creation of a user, it could be enough to trigger a graphql mutation that records said newcomer to the app’s database - and it would be entirely implemented by the app’s developer(s).

@olance would it help you?

Don’t want to derail the thread but strong agree.

Sanity.io’s API has mutations called createOrReplace and createIfNotExists which are self explanatory, and are great for just throwing one piece of data at and letting the API sort it out.

Prisma’s upsert Syntax is a bit strange in comparison.

1 Like