Of cookies, phones, and sealing wax [& solutions !]

Now that we have Envelop+Helix I went looking to the Envelop plugins repo - and there is nothing to help services manipulate cookies

How can my services read&write cookies

If not, why not?

I asked this question in the Envelop discussion area and they had some starting points: A Plugin To Manipulate Cookies? Ā· Discussion #1006 Ā· dotansimha/envelop Ā· GitHub

Based on that reply Iā€™ve tried adding a plugin to the extraPlugins part of the createGraphQLHandler

But I get no output to indicate that itā€™s workingā€¦

My useRequestHeadersPlugin

const path = require('path')
const __file = path.basename(__filename)

// https://www.npmjs.com/package/json-stringify-safe
const stringify = require('json-stringify-safe')

import { logger } from 'src/lib/logger'

export const useRequestHeadersPlugin = () => {
  return {
    onContextBuilding({ context, extendContext }) {
      logger.debug(
        `[${__file}] ~ onContextBuilding() context: ${stringify(
          context,
          null,
          2
        )}`
      )
      // extendContext({
      //   req: context.req,
      // })
    },
  }
}

Using the extraPlugins part of the createGraphQLHandler

import { createGraphQLHandler } from '@redwoodjs/graphql-server'

import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'

import { getCurrentUser } from 'src/lib/auth'
import { useRequestHeadersPlugin } from 'src/lib/envelop-request'

import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'

export const handler = createGraphQLHandler({
  getCurrentUser,
  loggerConfig: { logger, options: {} },
  extraPlugins: [useRequestHeadersPlugin],
  directives,
  sdls,
  services,
  onException: () => {
    // Disconnect from your database with an unhandled exception.
    db.$disconnect()
  },
})

Ah! I was not instantiating the plugin!

extraPlugins: [useRequestHeadersPlugin],

Should have been

extraPlugins: [useRequestHeadersPlugin()],

ok, so Iā€™ve added request to the context

but my getCurrentUser in ./api/src/lib/auth.ts is not being called within those steps, and itā€™s not being passed anythingā€¦

api | DEBUG [2021-11-21 17:01:57.465 +0000]: [auth.js] ~ getCurrentUser args:
api | DEBUG [2021-11-21 17:01:57.466 +0000] (graphql-server): GraphQL execution started: __REDWOOD__AUTH_GET_CURRENT_USER
api | DEBUG [2021-11-21 17:01:57.467 +0000] (graphql-server): GraphQL execution completed: __REDWOOD__AUTH_GET_CURRENT_USER
api | POST /graphql 200 4.704 ms - 42

Also, my services are not receiving the ā€˜contextā€™ as expected from the Envelop plugin docsā€¦

export const messaging = (args) => {
  logger.debug(`[${__file}] ~ messaging args: ${stringify(args, null, 2)}`)
  const { id } = args
  return db.messaging.findUnique({
    where: { id },
  })
}

outputs:

api | DEBUG [2021-11-21 17:23:16.070 +0000] (graphql-server): GraphQL execution started: MessagingQuery
api | DEBUG [2021-11-21 17:23:16.077 +0000]: [messagings.js] ~ messaging args: {
api |   "id": "to-register-phone"
api | }
api | DEBUG [2021-11-21 17:23:17.174 +0000] (graphql-server): GraphQL execution completed: MessagingQuery

Hi @ajoslin103 Iā€™ve read this and the issue on the Envelop repo but could you explain more about what you want to do and why instead of how.

Why do you need to manipulate a cookie? And why would you want to do that within the GraphQL lifecycle?

Is this a different cookie than the dbAuth session cookie?

Also what do you want to store in a cookie?

Hi @dthyresson

As always, thanks for your time & attention !

I created a different kind of auth, one that uses the phone # as the username and a code [I just texted to that phone] as the password.

I had created it in 0.35 before the dbAuth showed up and I have to bring that code forward because it (for no reason I can figure out) wonā€™t deploy to Netlify anymore.

I wanted it brought up to date anyway.

Iā€™ve been trying to shoehorn the two together for a few days (about 2 days longer than I should have.)

I was storing my validation token in browser local storage, and I got that into the cookie easily ā€“ but my getCurrentUser isnā€™t being given anything. But Iā€™m not using the .web side of the solutionā€¦ So how could it work?

I just realized, while out leaf blowing, that I should just deploy dbAuth into a clean project and manipulate it for my own nefarious purposes

Then the next time around I might be able to contribute it as another auth scheme

Here we go!

:slight_smile:
Al;

-- redwood rules !!

Actually, as I work my way thru this -

When I send a code to someoneā€™s phone Iā€™ll want to encrypt it and store it into the hashedPasword so that it will match what is encrypted and sent down from the frontEnd (when the user enters the code into the login box)

So I will be hunting for the calls to encrypt the code before poking it into the hashedPassword column

Flow: User enters their phone & clicks forgot, I send them a code & encrypt that into the hashedPasword and then redirect them to the login w/a toast saying to enter the code in as the password, User then enters the code and hits Login

Could you diagram this flow? The actors, the devices, the data, the info exchange.

BTW - Supabase offers username/password auth and also ā€œphoneā€ auth over SMS with a confirmation code.

Yes, Iā€™ve seen that ā€“ but Iā€™m not 100% happy w/the supabase (or firebase) experience to date ā€“ so they are now just my database.

This whole project has pivoted so many times itā€™s not funny.

Iā€™ll diagram it after it works ā€“ l think I can do it all with the current set of dbAuth code/functionality

Afterwards we can decide how much to save if any, assuming my insanity can be curedā€¦

Do you have the latest updates to dbAuth that include forgot/reset password functionality? In the forgotPassword.handler() you could create a random password and put it in the hashedPassword field, emailing it to them: Docs - Authentication : RedwoodJS Docs

Although with the default flow can you just use the built-in resetToken method and let them set their own password when they come back to the site? Iā€™m pretty sure that a security best-practice would be to make them change their password after you email them one in plain text, so if theyā€™re going to have to change it away, you could let dbAuth handle it all automatically.

Again all this is ā€œhowā€ and still need to know the ā€œwhyā€ and ā€œwhatā€. I ask only because I know some history here and there - if I recall - are two auth flows:

  • 1 - for the normal user
  • 2 - some invite code token sent over sms or email that then some *other * user activates to gain access to the app

So, thereā€™s this personal token sent to a third party that is used to look at (I think) Events to join.

Itā€™s this second with flow that is cookie based ā€“ not the genre use login. And thatā€™s why I am not sure any Auth here is really needed and why I want to know more about the what and why.

If the second user simply views and event of does 2-3 few things, then a secure serverless function could work as well depending on the complexity of this personā€™s interaction.

You could do a custom auth ā€“ just one auth provider ā€“ altogether and based on some header info, in the getCurrentUser decoded the token and either apply normal user auth flow or the custom flow where you verify the token.

Or you could add some audience claim the to JWT and one audience is app and audience is mobile and handle in getCurrentUser normally.

Lots of solutions - I could probably come up with 4-5 others to try and may or may not work - but first need to understand the purpose.

1 Like

Apologies, as I was AFTK (try not to fall out of your chair, dear reader)

ā€“ scene: a dim pub, many empties on the table - whence a plan was hatchedā€¦

In the beginning, a few developer ages agoā€¦ There was one app serving two populations, event planners and event attendeeā€™s

And, strange but true: tā€™was no UI for the attendeeā€™s, they were neither a source of data, nor a consumerā€¦ They were to opt in via text and that was pretty much that. It would cost them penniesā€¦

ā€“ scene: time passes, promises and negotiations, missed commitmentsā€¦

So I built it, and yet they were not insideā€¦ The whimsical winds of data availability had blown our dream away like so many cloudsā€¦ So we had to pivot - the datasource promises had failed to fulfill. ā€œOh Sorry, didnā€™t you get the memo?ā€

ā€“ scene: a brighter pub this time, fewer emptiesā€¦

And lo, the attendee was deemed Responsible (via the incantation of legal texts, hereafter known as the ā€œfine printā€) and we could Certainly start relying on the attendeeā€™s as their own datasource, which meant that they were to have logins and a UIā€¦

ā€“ scene: A quick devs year laterā€¦ The spawn does live and talk (mostly spouting le finĆ© printe) and provide & consume data ( note: search for TLDR here [solved] AWS S3 - File uploads - #17 by ajoslin103 )

ā€“ narrator: RW 0.38.3 and dbAuth are working great for the event planners, for whom an email address is 2nd nature - we resume with moreā€¦

Aaaaannd so then the attendees had to have auth - and in the spirit of the original plan, that auth was to be as minimal as possible ā€“ even an email address was too much

By this point the Event Planner and Attendee were sharing the same Supabase postgres database ā€“ ( search for ā€œrsyncā€ [Github PR in motion] Help, I need two apps with one database! )

So I decided to authenticate the attendees by texting them a passCode ā€“ they would use their phone number as their username & enter the passCode as their password

I got that working in 0.35 but it was a little cumbersome and the code was not as clean as I would have liked - and then the netlify deploy broke itselfā€¦ (wouldnā€™t deploy after not being touched for a 45 days, wtf, WtF !!)

ā€“ scene: a lone dev making another fateful decision, then typing furiously for a week of daysā€¦

I added hashedPassword, salt, resetToken & resetTokenExpiresAt to my validateByPhone table

I named ā€˜phoneā€™ as my username

So the attendee hits the Login page, and enters only their phone number

First I await forgotPassword(data.phone)

if that succeeds, then they exist and I can I kickoff the "sending of the code" in the ./api

If that fails then I await signUp({ ...data, password: nanoid() }) ā€“ note that the password is Irrelevant at this point, so I throw a guid at it for a fun obfuscation

That will succeed and I kickoff the "sending of the code" in the ./api

The attendee is then redirected to the Signup Page, where their [unmodifiable] phone number is displayed, and they enter the ā€œcode that they were sentā€

And I await logIn({ ...data, username: phone })

If the code they send matches what is stored in the hashedPassword then they are considered logged in and all proceeds as normal.

scene: User Happy Path Concludes, Happily

ā€“ epilogue: wherin lies the rub

ā€œthe sending of the codeā€

I generate a code 

I create the hash & salt values using code lifted from AuthProvider's ./api Brother [DbAuthHandler] -- and store them in the table `validateByPhone` using the given phone number as the unique key

Then I text the code to the phone number in question

But, (and yes dear reader, after 45 years - it is a big But : )

It doesnā€™t workā€¦ The salt and hashed values are somehow wrong?

What am I not understanding???

The Endā€¦

    (or is it?...

[thanks for your time and attention]

ā€“ appendix a:

the lifted codeā€¦

// hashes a password using either the given `salt` argument, or creates a new
// salt and hashes using that. Either way, returns an array with [hash, salt]
export const _hashPassword = (text: string, salt?: string) => {
  const useSalt = salt || CryptoJS.lib.WordArray.random(128 / 8).toString()

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

And so, it came to pass [the linter] and it was good

Ha Ha !!

Give it another whack and things DO WORK !!

Make that linter happy, finish the 0.38.3 updates ā€“ finish some code I missedā€¦

And weā€™re goldenā€¦

1 Like