If a service needs to know the currentUser's access token, how best to fetch it?

There are cases where a service will have to call a third-party api on behalf of the currentUser.

One case is Netlify Identity.

Consider this service that wants to update a user’s user_metadata (where profile info and preferences can be store). It makes PUT request to Netlify Identity to update that user_metadata.

import { context } from '@redwoodjs/api/dist/globalContext'
import got from 'got'
import { requireAuth } from 'src/lib/auth'

export const userMetadata = () => {
  requireAuth()
  return context.currentUser.user_metadata
}

export const updateUserMetadata = async ({ input }) => {
  requireAuth()

  const { body } = await got.put(
    'https://MYAPP.netlify.app/.netlify/identity/user',
    {
      responseType: 'json',

      json: {
        data: {
          ...input,
        },
      },
      headers: {
        authorization: `Bearer ${NEED_ACCESS_TOKEN_HERE}`,
      },
    }
  )

  context.currentUser.user_metadata = body.user_metadata

  return context.currentUser.user_metadata
}

But right now there is no great way to get NEED_ACCESS_TOKEN_HERE.

It is not on the context. In getCurrentUser grpahql passes along the event which has the headers and the type, schema, and token for is extracted:

/**
 * Split the `Authorization` header into a schema and token part.
 */
export const parseAuthorizationHeader = (
  event: APIGatewayProxyEvent
): AuthorizationHeader => {
  const [schema, token] = event.headers?.authorization?.split(' ')
  if (!schema.length || !token.length) {
    throw new Error('The `Authorization` header is not valid.')
  }
  // @ts-expect-error
  return { schema, token }
}

Looking for ideas where best to keep the accessToken.

My current workaround is to store it on the currentUser:

export const getCurrentUser = async (decoded, { token, type }) => {
  const authorization = { token, type }
  return { ...decoded, roles: parseJWT({ decoded }).roles, authorization }
}

But would rather it be available perhaps on the context as done in graphql:

    // 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
    }

    let customUserContext = userContext
    if (typeof userContext === 'function') {
      // if userContext is a function, run that and return just the result
      customUserContext = await userContext({ event, context })
    }

@peterp I have not seen customUserContext implemented before, might the token be stored there?

Anyone have ideas?

Cheers.

As a more generalised version of the question - how can we get access to the request context in a service?

Note: FYI - the request context @danny needs is different than then context I use above. The “global context” has the currentUser and other info, but not the request (headers and all the other goodies).

1 Like

Hello :wave:,

This is possible via a custom context function.


import { getAuthenticationContext }  from '@redwoodjs/api'

export const handler = createGraphQLHandler({
    context: ({ event, context }) => {

        const authContext = getAuthenticationContext({ event })
        // returns AuthContextPayload:
        // export type AuthContextPayload = [
        //   string | object | null,
        //   { type: SupportedAuthTypes } & AuthorizationHeader,
        //   { event: APIGatewayProxyEvent; context: GlobalContext & LambdaContext }
        // ]
        return {
            authContext,
        }
    }
})

Now authContext should be available via the global context, or in { context } as the second argument of a service.

You can do the same things for event or context, but I think it may be a good practice to only put what you want into the context object.

This is great.

I had to make a few changes.

  1. getAuthenticationContext isn’t exported from api

So, in redwood/packages/api/src/index.ts

  • add export * from './auth'
import './global.api-auto-imports'

export * from 'apollo-server-lambda'
export * from './makeServices'
export * from './makeMergedSchema/makeMergedSchema'
export * from './functions/graphql'
export * from './globalContext'
export * from './parseJWT'
export * from './auth'
  1. needs async
export const handler = createGraphQLHandler({
  context: async ({ event, context }) => {
    const authContext = await getAuthenticationContext({ event, context })
    return authContext
  },
  getCurrentUser,
  schema: makeMergedSchema({
    schemas,
    services: makeServices({ services }),
  }),
  db,
})
  1. In order to access context in a service, I had to do something like:
export const userMetadata = ({ _args }, { context }) => {
  requireAuth()
  return context.currentUser.user_metadata
}

export const updateUserMetadata = async ({ _args }, { input, context }) => {
  requireAuth()
  const accessToken = context[1].token
...

Not sure why I had to include the {args}. If i didn’t, context was undefined.

But … works great. Edit: strike that.

I think I will PR the

export * from './auth'

Edit: Note: the args comes from makeMergedSchema

 // Does a function exist in the service?
    if (services?.[name]) {
      return {
        ...resolvers,
        // Map the arguments from GraphQL to an ordinary function a service would
        // expect.
        [name]: (
          root: unknown,
          args: unknown,
          context: unknown,
          info: unknown
        ) => services[name](args, { root, context, info }),
      }

Having an issue getting both input and context in

export const updateUserMetadata = async ({ input, context }) => {

but can get context in

export const userMetadata = (_args, { context }) => {

:man_shrugging:

You could import directly: import { getAuthenticationContext } from '@redwoodjs/api/dist/auth'

Where does input come from?

1 Like

Oh, of course. I forget about the “dist” being available.

It’s from a typical edit mutation, here for a Post

export const createPost = ({ input }) => {
  requireAuth()

  return db.post.create({
    data: input,
  })
}

In my case it is the

  input UpdateUserMetadataInput {
    full_name: String
    favorite_color: String
  }

Note the snake case because that’s how attributes are stored in the token and info coming back from Netlify.

ah, ok, then your service would look something like this:

export const updateUserMetadata = async ({ input }) => {
  requireAuth()
  // context is a global, so we don't need to destructure it. 
  // I also would have assumed that this would've been 
  `context.authContext[1].token`
  const accessToken = context[1].token
...

Global context - of course.

Ok, so here is the code if you want to put something into your context (like auth accessToken) and then update Netlify Identity userMetadata:

  1. In graphql, import getAuthenticationContext from dist api
  2. Add custom context function to set authContext
// api/src/functions/graphql.js
import {
  createGraphQLHandler,
  makeMergedSchema,
  makeServices,
} from '@redwoodjs/api'
import { getAuthenticationContext } from '@redwoodjs/api/dist/auth'
import schemas from 'src/graphql/**/*.{js,ts}'
import services from 'src/services/**/*.{js,ts}'

import { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'

export const handler = createGraphQLHandler({
  context: async ({ event, context }) => {
    const authContext = await getAuthenticationContext({ event, context })
    return authContext
  },
  getCurrentUser,
  schema: makeMergedSchema({
    schemas,
    services: makeServices({ services }),
  }),
  db,
})
  1. In your service, context is global so can get the accessToken as context[1].token
export const updateUserMetadata = async ({ input }) => {
  requireAuth()

  const { body } = await got.put(
    'https://YOUR_APP.netlify.app/.netlify/identity/user',
    {
      responseType: 'json',

      json: {
        data: {
          ...input,
        },
      },
      headers: {
        authorization: `Bearer ${context[1].token}`,
      },
    }
  )

  return (context.currentUser.user_metadata = body.user_metadata)
}
  1. in your getCurrentUser, since you have updated the user metadata but the decoded token is still the old (it has not refreshed even though Netlify has new data)
export const getCurrentUser = async (decoded) => {
  return (
    context.currentUser || { ...decoded, roles: parseJWT({ decoded }).roles }
  )
}

Might need to refresh token from web side that can access client using

    getToken: async () => {
      const user = await client.currentUser()
      return user?.jwt() || null
    },

?

2 Likes