Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

getCurrentUser given both decoded & access tokens #779

Merged
merged 22 commits into from
Jul 9, 2020

Conversation

dthyresson
Copy link
Contributor

Overview and Why

When implementing a custom getCurrentUser() in a Redwood app's auth.js, there's a need to access not just the decoded JWT, but the access token itself.

This is especially helpful when implementing authentication with Auth0 because an access token (https://auth0.com/docs/tokens/concepts/access-tokens) is required by Auth0 in order to fetch the userProfile from the /userinfo endpoint (https://auth0.com/docs/api/authentication#get-user-info).

One may then persist the profile information (such as email) in a User or update the currentUser in the context to present email, name, or other profile info subsequently on the web side.

Changes Made

This PR makes the following changes:

  • In api/src/auth/authHeaders.ts, moves parsing the access token from the event headers (ie, the "Bearer ") into its own function to be used both independently and also when decoding the token. This is used when creating the context handler in api/src/functions/graphql.ts.
export const accessToken = async (event: {
  event: APIGatewayProxyEvent
}): Promise<AccessToken> => {
  let accessToken: AccessToken = null

  accessToken = event.headers?.authorization?.split(' ')?.[1]

  if (!accessToken && accessToken.length === 0) {
    throw new Error('Empty authorization token')
  }

  return accessToken
}
  • In api/src/functions/graphql.ts, the createContextHandler now provides decoded token as well as optional authType and access token to getCurrentUser().
// Get the authorization information from the request headers and request context.
    const type = getAuthProviderType(event)
    if (typeof type !== 'undefined') {
      const decoded = await decodeAuthToken({ type, event, context })
      const token = await accessToken(event)
      context.currentUser =
        typeof getCurrentUser == 'function'
          ? await getCurrentUser(decoded, {
              token,
              authType: type,
            })
          : decoded
    }

Example Use

The following is an example of how having the access token is helpful when using Auth0 as an authentication provider.

  • Install "auth0" package via yarn workspace api add auth0
  • The decoded token provides the user's id in the JWT's sub (aka subject) claim.
  • The token is the access token required by Auth0 to call the /userinfo endpoint
  • Use the sub claim to see if a User exists in the db; if not ...
  • Fetch userProfile from Auth0
  • Create new User
  • Update the currentUser in context so that a db call to find the User is not made upon each request
// api/src/lib/auth.js

import { AuthenticationClient } from 'auth0'

...

const auth0 = new AuthenticationClient({
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
})

export const getCurrentUser = async (decoded, { token, authType }) => {
  // shortcut if we have a user profile in context
  // note: if something else changes the User record
  // can't rely on this shortcut but will reduce db calls
  if (context.currentUser?.userId) {
    return context.currentUser
  }

  // do we have an accessToken from auth0 and an userId from the decoded JWT?
  if (!token || authType != 'auth0' || !decoded?.sub) {
    return decoded
  }

  // find the existing User ...
  // or create a new User with its userProfile info
  try {
    const user = await db.user.findOne({
      where: {
        userId: decoded.sub,
      },
    })

    if (!user && token) {
      const auth0User = await auth0.getProfile(token)
      const userProfile = {
        email: auth0User.email,
        emailVerified: auth0User.emailVerified,
        lastIp: auth0User.lastIp,
        lastLogin: auth0User.lastLogin,
        loginsCount: auth0User.loginsCount,
        name: auth0User.name,
        nickname: auth0User.nickname,
        picture: auth0User.picture,
        userId: auth0User.sub,
      }

      const userWithProfile = await db.user.create({
        data: userProfile,
      })

      // set the currentUser in context to include its userProfile info
      const currentUser = context.currentUser
      context.currentUser = { currentUser, ...userProfile }

      return userWithProfile
    }

    return user
  } catch (error) {
    console.log(error)
    return decoded
  }
}

Now in a page (or better yet, a User component) one can present the user profile:

  const { currentUser, isAuthenticated } = useAuth()

...

{isAuthenticated && currentUser && (
  <div>
   <p>You need to be logged in to see your profile:</p>
   <p>{currentUser.email}</p>
   <img src={currentUser.picture} />
   <p>{currentUser.email}</p>
  </div>
)}

Considerations

  • { token, authType } should be optional so as to not break existing getCurrentUser() implementations?
  • authHeaders did not have tests, so testcases not updated (but happy to write some)

@dthyresson
Copy link
Contributor Author

I also just realized that if this gets merged, the generated auth.js template -- see: https://github.com/redwoodjs/redwood/tree/main/packages/cli/src/commands/generate/auth/templates -- should be updated for the new getCurrentUser = async (decoded, { token, authType }) signature.

https://github.com/redwoodjs/redwood/tree/main/packages/cli/src/commands/generate/auth/templates

@peterp
Copy link
Contributor

peterp commented Jul 1, 2020

This is great, thanks for this @dthyresson !

@peterp peterp self-assigned this Jul 1, 2020
Copy link
Contributor

@peterp peterp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey!

I've left some feedback, this is starting to look really good - let me know if there are things that aren't clear and I'll be happy to jump on a call to clarify them.

packages/api/src/auth/accessToken.ts Outdated Show resolved Hide resolved
packages/api/src/auth/accessToken.ts Outdated Show resolved Hide resolved
packages/api/src/auth/accessToken.ts Outdated Show resolved Hide resolved
packages/api/src/functions/graphql.ts Outdated Show resolved Hide resolved
packages/api/src/auth/authDecoders/goTrue.ts Outdated Show resolved Hide resolved
packages/api/src/auth/authDecoders/magicLink.ts Outdated Show resolved Hide resolved
@dthyresson
Copy link
Contributor Author

Hey!

I've left some feedback, this is starting to look really good - let me know if there are things that aren't clear and I'll be happy to jump on a call to clarify them.

Thanks @peterp appreciate that! Starting to get a feel for how you and the project are designing and implementing the framework -- it has to be designed for extensibility from the start (well from an early iteration) when solving a problem... not just solving the problem if you know what I mean.

I think I have a handle on your suggestions but will reach out otherwise.

@peterp peterp self-requested a review July 6, 2020 15:31
Copy link
Contributor

@peterp peterp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great, I just want a few stylistic changes - Feel free to merge them in batches, or by yourself. After that I'll run it locally and merge it.

I might change a few things post deployment - but over all it's way better than it was before, thanks for much for this!

packages/api/src/auth/authDecoders/auth0.ts Outdated Show resolved Hide resolved
packages/api/src/auth/authHeaders.ts Outdated Show resolved Hide resolved
packages/api/src/auth/authHeaders.ts Outdated Show resolved Hide resolved
packages/api/src/auth/authHeaders.ts Outdated Show resolved Hide resolved
packages/api/src/functions/graphql.ts Outdated Show resolved Hide resolved
@dthyresson
Copy link
Contributor Author

@peterp All request style changes are in. Thanks for noting them -- I'll remember for future.

After that I'll run it locally and merge it.

Thanks.

I might change a few things post deployment -

I could see revisiting some naming: auth vs authentication vs authorization. Client vs Provider. Decoder vs Authorization. Decoded vs token vs "bearer token in header".

I imagine when roles and permissions are added, these terms might get more used and possible source of confusion.

but over all it's way better than it was before, thanks for much for this!

Glad to help.

@dthyresson
Copy link
Contributor Author

@peter one last item -- but this could be a separate issue/task is to update the auth generators/templates:

https://github.com/redwoodjs/redwood/tree/main/packages/cli/src/commands/generate/auth/templates

export const getCurrentUser = async (jwt) => {
  return jwt
}

to use new

export const getCurrentUser = async (decoded, { authType, token }) => {

and even add a new Auth0 template that has an example of fetching the full userProfile from API

import { AuthenticationClient } from 'auth0'
...

const auth0 = new AuthenticationClient({
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
})

...

export const getCurrentUser = async (decoded, { authType, token }) => {

...

  // find the existing User ...
  // or create a new User with its userProfile info
  try {
    const user = await db.user.findOne({
      where: {
        userId: decoded.sub,
      },
    })

    if (!user && token) {
      const auth0User = await auth0.getProfile(token)
      const userProfile = {
        email: auth0User.email,
        emailVerified: auth0User.emailVerified,
        lastIp: auth0User.lastIp,
        lastLogin: auth0User.lastLogin,
        loginsCount: auth0User.loginsCount,
        name: auth0User.name,
        nickname: auth0User.nickname,
        picture: auth0User.picture,
        userId: auth0User.sub,
      }

...

@@ -147,7 +147,7 @@ export const handler = async ({ provider, force }) => {
const tasks = new Listr(
[
{
title: 'Adding required packages...',
title: 'Adding required web packages...',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now can add packages to either the web or api side, since Auth0's api side benefits from having the its AuthenticationClient to implement the getCurrentUser.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@peterp ok with ☝️ to add both web/api packages?

@@ -0,0 +1,101 @@
// Define what you want `currentUser` to return throughout your app.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be way too much but included as an example of how to fetch userProfile now that have the access token.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I had problems testing my changes to the cli.

While I built the cli package via yarn build:js && yarn build:clean-dist and I see the new/updated files:

Screen Shot 2020-07-07 at 19 26 04

and when I do a

yarn rwt copy ../../redwoodjs/redwood

from there and I see the files move

when i try to test with a

yarn rw g auth auth0

the files in dist seem to revert to the old files and I get a file not found error for the new auth0 template

  ✔ Adding required web packages...
  ✔ Adding required api packages...
  ✔ Installing packages...
  ✖ Generating auth lib...
    → ENOENT: no such file or directory, open '<MYDIRPATH>/projects/redwoodjs/auth0-auth-test-app
…
    Adding auth config to web...
    Adding auth config to GraphQL API...
    One more thing...
ENOENT: no such file or directory, open <MYDIRPATH>/projects/redwoodjs/auth0-auth-test-app/node_modules/@redwoodjs/cli/dist/commands/generate/auth/templates/auth0.auth.js.template'

So, unfortunately these template changes haven't been tested with a new rw app. Sorry. I'll keep trying to figure out why the files revert.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange, I'm having the same issue

@peterp peterp self-requested a review July 8, 2020 10:23
@peterp
Copy link
Contributor

peterp commented Jul 9, 2020

TODO

  • Test that authentication still works.
  • Update docs.

@peterp peterp self-requested a review July 9, 2020 15:46
@peterp peterp merged commit 7b0fb8f into redwoodjs:main Jul 9, 2020
@peterp peterp deleted the dt-auth-access-token branch July 9, 2020 16:35
@peterp peterp added this to the next release milestone Jul 9, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants