Custom github JWT Auth with Redwood Auth

@morganmspencer Someone just posted a very thorough walkthrough for using a Local JWT Auth Implementation that may be of interest to you.

@ajcwebdev I just saw that and it helped a ton, thanks for sending it along!

1 Like

Hey, I would say the best method still to implement custom auth is to use the “custom” auth provider. The only thing you will need to implement really are functions to retrieve the user (getCurrentUser) in src/lib/auth, and some way to sign in to linkedin/github/facebook/your own system.

While the solution linked above looks like a lot of work was put into it, and top marks for sharing it with the community, there are a few things I don’t quite agree with there that I can see such as, use of cookies, some potential security risks and not using the existing auth pattern that Redwood has built in - which means you don’t get a tonne of the new features that RW offers e.g. RBAC.

I wrote up a little bit more about implementing custom auth on this post:

1 Like

Has anyone ever tried to pen-test a Redwood app? Would be interesting to actually test some of these different methods.

Looks like we’re not able to generate custom providers anymore?

2 Likes

I believe you have to start by generating one of the default ones so it sets up the necessary auth files first. Then, you change the AuthProvider type to custom and pass your own custom client.

I also ran into this but there is no custom generator.

1 Like

Hi, @edjiang, as @morganmspencer noted, there isn’t a custom generator even though the RedwoodJS docs unfortunately do seem to indicate that there is one:

I looked at the auth generator code and it checks a directory of auth templates (auth0, supabase, etc … ie, the ones you note in your command line message) and even looking at the history of the template directory, I do not see that custom every existed.

So, perhaps the generator never could actually generate the custom setup at all – and the docs need to be updated.

You can as @morganmspencer said, setup with another provider and that will get you the updates to:

  • index.js

You need something like:

const customClient = <your client>
...

    <AuthProvider client={customClient} type="custom">
      <RedwoodProvider>
        <Routes />
      </RedwoodProvider>
    </AuthProvider>

and then the changes to graphql and then the auth.js lib.

I’ll raise an issue since the generator really should work with “custom” as while it is somewhat easy to setup the web side, the api side needs the modifications done.

2 Likes

@morganmspencer and @edjiang FYI I’ve opened an issue:

and referenced this post.

Thanks again for pointing this out!

2 Likes

Thanks for the help! Was code reading and it seems like the interface for a Client is straightforward especially due to the type annotations. But definitely was confused at that step :slight_smile:

1 Like

Got a basic prototype working. Only feedback on documentation was that it was not intuitive that you had to add an auth-provider: custom header in the HTTP request in order for Redwood’s stack to even check the access token and call my handlers. Such a requirement is unintuitive for non-Redwood callers like mobile apps (although not a huge deal).

1 Like

@edjiang Great to hear! Also, would you be up for improving the documentation based on your experience? And/or creating a related issue with your notes and suggestions for improvement would be great as well.

No pressure at all but 100% welcome:

There’s a certain amount of polish needed to write docs on GH, but what I can do to help is write a summary here and add a link in the GH

Build auth endpoint into the api project:

Write the SDL

In my case I was building phone auth stored in the local database. Design your SDL and put it in /api/src/graphql/auth.sdl.js

# Passwordless example
type Mutation {
  authChallenge(input: AuthChallengeInput!): AuthChallengeResult
  authVerify(input: AuthVerifyInput!): AuthVerifyResult # Should return a token
}

# Username/password example
type Mutation {
  authRegister(input: AuthRegisterInput!): AuthRegisterResult
  authLogin(input: AuthLoginInput!): AuthLoginResult # Should return a token
  authVerify(input: AuthVerifyInput!): AuthVerifyResult
  authForgotPassword(input: AuthForgotPasswordInput!): AuthForgotPasswordResult
}

Write the Service

In /api/src/services/auth/auth.js

export const authChallenge = async (input) => { return { success: true } }
// ...etc

Write a token validator and user resolver

In /api/src/lib/auth.js:

import { AuthenticationError } from '@redwoodjs/api'

export const getCurrentUser = async (token) => {
  // Resolve and return user record
}

export const requireAuth = () => {
  if (!context.currentUser) {
    throw new AuthenticationError("You don't have permission to do that.")
  }
}

Add the user resolver to the GraphQL endpoint

In /api/src/functions/graphql.js:

import { getCurrentUser } from 'src/lib/auth' // Add this line

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

Authenticate via HTTP Headers!

Add Authorization: Bearer TOKEN and replace TOKEN with your token value
Add Auth-Provider: custom

Viola! You should be able to authenticate using a custom provider now.

Implement Frontend Client

I haven’t implemented this, but you’ll also need to implement the UI to hit your backend endpoints.

3 Likes

Thanks for this auth blueprint @edjiang

Just so readers know why this is – that the authProvider type is sent in the headers along with the Bearer token – is that different authentication providers have different ways of decoding (or not decoding) the access token.

While many will decode and verify the JWT, some (like Netlify Identity) has the token already decoded in its own context

and verified and some like Auth0 need to ]get a signing key in order to verify the token](redwood/packages/api/src/auth/decoders/auth0.ts at 1d7973d8315eb940b9504ca5eccb0567ce235eed · redwoodjs/redwood · GitHub):

So, the set of decoders needs to know which one applies – hence passed in the headers.

While I understand that in this implementation it’s necessary, most likely it would make sense to have two bits of configuration, the getCurrentUser and tokenMiddleware or something instead of declaratively using the auth-provider.

For most apps you won’t be using multiple providers, and declaring a tokenMiddleware that decodes the token will allow developers to flexibly handle incoming authorization and types.

True – while “apps” may not, multiple different “sides” could.

One may one day implement a cli side that needs to interact differently than the web side to authenticate. Ie - it might send an API_TOKEN that is connected to a user/team/project account to authenticate.

But, I see your point. Redwood always welcomes PRs or issues to make the framework better, more useful, and a cleaner DX, so if you’d like to submit an approach I’m sure the community there can definitely discuss and see if that can happen.

FYI - this example is from 2020 and there is in RedwoodJS v2+ a new way.

See the docs here about using auth in serverless functions: Serverless Functions | RedwoodJS Docs

import type { APIGatewayEvent, Context } from 'aws-lambda'

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

import { getCurrentUser, isAuthenticated } from 'src/lib/auth'
import { logger } from 'src/lib/logger'

const myHandler = async (event: APIGatewayEvent, context: Context) => {
  logger.info('Invoked myHandler')

  if (isAuthenticated()) {
    logger.info('Access myHandler as authenticated user')

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        data: 'myHandler function',
      }),
    }
  } else {
    logger.error('Access to myHandler was denied')

    return {
      statusCode: 401,
    }
  }
}

export const handler = useRequireAuth({
  handlerFn: myHandler,
  getCurrentUser,
})

Hey, I’ve been trying to implement @edjiang 's solution, as I’d prefer all authentication to be in the same lambda function. I’m trying to use JWT for sessions, as we have to use a custom auth service. Basically, link our users with the old db’s accounts, and have this new API be a gateway. So I need to access what’s stored in the JWT token, and login without validating in my db.

I’m familiar on how to do this on a more barebones framework, but redwood has been giving me issues - I can’t seem to get getCurrentUser to receive the token unless I access the raw request. I’m considering I’ll have to split the authentication into a separate function, like @3nvy does in their example, but I’m unsure if it would still work on the latest version :confused:

I’d appreciate any help you could give :slight_smile:

@JIGutierrez RedwoodJS v4 (a release candidate should be out by end of 2022) will have a refactored auth provider architecture and will make it much much easier to implemented your own auth.

Stay tuned. While there is no JWT flavor of dbAuth at moment, this will be possible in v4.

Thanks.

@JIGutierrez Actually, the RW canary releases will have this feature.

Here is an example of implementing custom auth (but a contrived demo example).

Sorry for the delay - busy weekend. I think I’ll go ahead and switch to canary and get it working, and change to RC when it comes out. We have plenty of time til production and I’d like to get this working. Thanks!