dbAuth is here: host your own authentication, the old fashioned way

Looks like ethAuth and dbAuth are very similar. Only major difference is instead of email/password, they are signing a message with their wallet (Metamask). Both rely on the db/api to validate and store user auth data

I’m really happy to have a solution for self-host auth!
I tried to find a way to reset the password, maybe I can write some code inside functions/auth.ts to handle this, but I’m scare to create security issues. Do you have recommendations for that ?

2 Likes

Yeah, a forgot password flow didn’t make the cut, but we do want one!

What you could do today is something along these lines:

  • add a resetToken field to the user table that holds a UUID
  • create a “Forgot Password” page, that accepts an email address
  • when the user fills it out, assuming their email is found, set the resetToken to a UUID and email them a URL with that token in a query string variable
  • create a “Reset Your Password” page that checks that the reset token is found, if so then let them enter a plain text password (and nullify the resetToken field in the DB so it can’t be re-used)
  • salt and hash the password and save to the user record (take a look at the code here and follow the function calls for how we’re hashing/salting, we might move that to a shared location so you can just import and use it in your own code): redwood/DbAuthHandler.ts at main · redwoodjs/redwood · GitHub
  • send user back to the login screen and they should be good to go!

I was thinking about abstracting most of this and maybe just exposing a forgotPasswordHandler handler, similar to the signupHandler we have now in the auth.js function, where you define what you want to do when someone says their password wasn’t found. You would put your logic for however you want to notify the user (send an email, probably) and we’d handle the rest. We could generate the Forgot Password and Reset Your Password pages, and you could use the default, or re-style them to match your site.

3 Likes

Thank you ! :grinning_face_with_smiling_eyes: That is what I was looking for

1 Like

Does someone try to implement dbAuth on they current project ?
I try to find out why I have this weird error from apollo :

"TypeError: Converting circular structure to JSON"
1: "    --> starting at object with constructor 'Array'"
2: "    |     index 2 -> object with constructor 'Object'"
3: "    |     property 'context' -> object with constructor 'Object'"
4: "    --- property 'currentUser' closes the circle"
5: "    at JSON.stringify (<anonymous>)"
6: "    at prettyJSONStringify (/Users/simongagnon/Projects/raccoon/node_modules/apollo-server-core/src/runHttpQuery.ts:455:15)"

With a code 500 to /graphql when it’s call this query

query __REDWOOD__AUTH_GET_CURRENT_USER { redwood { currentUser } }

The __REDWOOD__AUTH_GET_CURRENT_USER query runs the getCurrentUser() function you have defined in api/src/lib/auth.js It sounds like your user user record is referencing something that then references user again, making a circular dependency.

We’ve also seen weird errors after upgrading from a previous version of Redwood, and the only fix seems to be deleting all of node_modules and reinstalling everything from scratch again. You could also try that.

If you’re still getting the same error, would you mind sharing the SDL file for your user, as well as the content of api/src/lib/auth.js?

1 Like

Yes, I know that query run getCurrentUser() so I put a console.log() to see what’s happen like this :

export const getCurrentUser = async (session: { id: string }) => {
  console.log('getCurrentUser', session)
  return await db.agent.findUnique({ where: { id: session.id } })
}

And… I see nothing from my console :sweat_smile: So I was like yeah, I think the problem is before it execute the function. I tried to debug from the framework and I did’t find anything that can cause this circular structure.

My sdl for agent look like this :

  type Agent {
    email: String!
    id: String
    createdAt: DateTime!
    firstName: String!
    lastName: String!
    role: String!
    Partner: Partner!
    partnerId: String!
    notes: [Note]
    kanban: JSON
  }

And I set authModelAccessor: 'agent', for /auth.ts

Thank you for your help and also thanks for dbAuth :grinning_face_with_smiling_eyes:

Don’t thank me yet, it’s not working! :sweat_smile:

Okay, I don’t see a hashedPassword or salt field on your Agent model. You’ll need to add those so that dbAuth can store the hashed password for the user! There were instructions for adding those after the yarn rw setup auth dbAuth command, but you can see them again here: Docs - Authentication : RedwoodJS Docs

Did you update authFields and tell it that you’re using email as your username field? You’ll see that mentioned in the instructions as well.

You won’t have any users that have a valid password in your database yet, so you’ll want to sign up to create the first one. Then, see if you can log in as that user!

Oh yes I have it in the prisma.schema, I was thinking I should not put this on the GraphQL.
It don’t seem to change something now that is done.
Is also all good for authFields.

So yes I have a user with a valid password and it’s work, I also receive the id in the network when it call /auth?method=getToken

I wrote a little function in my seed to create a god user in dev ( all credit to you )

import * as CryptoJS from 'crypto-js'

function _hashPassword(text: string) {
  const useSalt = CryptoJS.lib.WordArray.random(128 / 8).toString()

  return [
    CryptoJS.PBKDF2(text, useSalt, { keySize: 256 / 32 }).toString(),
    useSalt,
  ]
}

const [hashedPassword, salt] = _hashPassword(process.env['MYPASSWORD'])

export default [
  {
    email: 'god@raccoon.trade',
    firstName: 'Mr',
    lastName: 'Raccoon',
    role: 'admin',
    partnerId: '00000000-0000-0000-0000-000000000000',
    kanban: '{"lanes":[]}',
    hashedPassword: hashedPassword,
    salt: salt,
  },
]

HMMMM It seems like everything is in place then…

This line in the error is interesting: starting at object with constructor 'Array' since we’re only selecting a single user I’m not sure where an Array would be coming from. Do you have a unique constraint on Agent.email?

I also noticed that id is not required in the SDL…is it required and being set properly in the database though?

1 Like

Yes

  id             String   @id @default(uuid())
  email          String   @unique

But that give me a good path to continue my research, I will keep you update !
Also is it normal that the Bearer token is the id of the user ?

I was able to create a new app from scratching using Agent as the user model and it’s working fine for me. Let me know if you want to see the code and I can put it up on GitHub!

Finally found out ! :star_struck:
After learn about how all the auth in redwood work, I discover that I’m missing something for my GraphQL, and I will explain why.

So when I was doing the Code Modifications for V0.35, I copy-past completely the handler like this :

export const handler = createGraphQLHandler({
  loggerConfig: { logger, options: {} },
  schema: makeMergedSchema({
    schemas,
    services: makeServices({ services }),
  }),
  onException: () => {
    // Disconnect from your database with an unhandled exception.
    db.$disconnect()
  },
})

So, by out of sheer ignorance, I remove the import { getCurrentUser } from 'src/lib/auth' telling me why this is here!

I finally get it by reading this code from the framework :

// packages/api/src/functions/graphql.ts
...
    // If the request contains authorization headers, we'll decode the providers that we support,
    // and pass those to the `currentUser`.
    const authContext = await getAuthenticationContext({ event, context })
    if (authContext) {
      context.currentUser = getCurrentUser
        ? await getCurrentUser(authContext[0], authContext[1], authContext[2])
        : authContext
    }

This ternary operator check if it can get getCurrentUser from createGraphQLHandler. Guest what I remove it, so it gives me authContext.

I don’t know why we give authContext if getCurrentUser is not available, but now I know that this thing give a circular structure and also why I have import { getCurrentUser } from 'src/lib/auth'

1 Like

Glad you figured it out! :slight_smile:

1 Like

Please consider supporting https://next-auth.js.org/

Loving it so far @rob!

Is a loginHandler on the horizon? Feature-wise I’m thinking of a confirmation step between signup and any attempted login, where an email gets sent as part of signup handling and that login handler would reject on error, similar to beforeResolver.

forgotPasswordHandler seems like a good idea, +1 to it.

2 Likes

Good idea!

Should a loginHandler always have to be defined, or do you think loginHandler() is optional, and if you don’t define it, it falls back to the current behavior (returning the ID of the user that needs to be saved to the cookie)?

If you do define it, you can include any logic you want, you just need to make sure to return the user ID just like the default behavior? Maybe loginHandler() is passed the user record that matches the username/password, and then you can inspect that data (or even pull additional data from the DB if necessary) and determine whether to return the ID, or throw an error. You can then respond to that error in your LoginPage

Created an issue: dbAuth: Add a `loginHandler` similar to the `signupHandler` · Issue #3064 · redwoodjs/redwood · GitHub

@rob Support jwt?

There’s no JWTs needed in this implementation—it relies on an encrypted cookie that stores the ID of the user that’s logged in. You get that ID in your getCurrentUser() function, so that you can lookup the user however you want (similar to using the sub property of a JWT in other auth implementations).

1 Like

Gonna move my thoughts to that issue to keep this post lean!

1 Like