OAuth 2 Authentication flow

Hey friends, I’m building an authentication flow and currently have my front and back end working well independently. I just need to be able to hook them up together and I am having a bit of trouble, so wondering if anyone can help me spot what I’m doing wrong.

In my front end I have:

In my page:

const AuthenticationPage = () => {
  return (
    <>
      <Toaster />
      <MetaTags title="Authentication" description="Authentication page" />

      <h1>Authentication Page</h1>
      <GoogleOAuthProvider clientId={`${process.env.GOOGLE_CLIENT_ID}`}>
        <GoogleAuth />
      </GoogleOAuthProvider>
    </>
  )
}

The component:

export const AUTHENTICATE_USER_MUTATION = gql`
  mutation AuthenticateUserMutation($code: String!) {
    authenticate: authenticate(code: $code) {
      accessToken
    }
  }
`

const GoogleAuth = ({ code }) => {
  const [authenticate, { loading, error }] = useMutation(
    AUTHENTICATE_USER_MUTATION,
    {
      onCompleted: () => {
        console.log(authenticate.access_token)
      },
      onError: (error) => {
        console.log(error.message)
      },
    }
  )

  const login = useGoogleLogin({
    onSuccess: (codeResponse) => console.log(codeResponse),
    flow: 'auth-code',
    scope: 'https://www.googleapis.com/auth/gmail.settings.basic',
  })
  return (
    <GoogleLogin
      onSuccess={(credentialResponse) => {
        login(credentialResponse)
      }}
      onError={() => {
        console.log('Login Failed')
      }}
    />
  )
}

And then in the backend I just need to receive a code which I will then pass on to Google’s server and generate my access and refresh tokens

export const authenticate = async ({ code }) => {
  const oAuth2Client = new OAuth2Client(
    process.env.GOOGLE_CLIENT_ID,
    process.env.GOOGLE_CLIENT_SECRET,
    'http://localhost:8910'
  )

  console.log(oAuth2Client)
  const { tokens } = await oAuth2Client.getToken(code)

  logger.info('output tokens')
  console.log(tokens)

  return {
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiryDate: tokens.expiry_date,
  }
}

I then have a mutation in my sdl file

type Mutation {
    authenticate(code: String!): Authentication @skipAuth
  }

Like I said, they both work well independently, I can get a code in the front end, and then if I use that code in Redwood GraphQL PLayground like this I’m able to get my tokens:

mutation AUTHENTICATE($code: String!) {
  authenticate(code: $code) {
    accessToken
    refreshToken
    expiryDate
  }
}

I just can’t get my front and back end to talk to each other which I know is a 100% down to how I’m trying to do that in my component, I just don;lt know how to move forward from here.

Thanks in advance

Hi @mplacona - can I ask why you’re not taking the easy route of implementing one of the many auth providers that will give you this out of the box?

We don’t have an official guide to custom auth, but sounds like the missing pieces you need to put together are:

  1. Configure custom auth provider
    The auth client is setup in your App.tsx - so the first thing I would do is - which will just add the boilerplate. In your App.tsx you need to do something like this
                          // 👇           // 👇
  <AuthProvider client={customClient} type="custom">

The “type” just sets the auth-provider header for each graphql request that is made. customClient is something you’ll need to define in (2)

  1. Create custom client
    You need to create a “redwood auth client” i.e. the thing that will be used by the framework to determine whether you’re logged in or not, and send the token with each header. It needs to conform to this interface:
export interface AuthClient {
  restoreAuthState?(): void | Promise<any>
  login(options?: any): Promise<any>
  logout(options?: any): void | Promise<any>
  signup(options?: any): void | Promise<any>
  getToken(options?: any): Promise<null | string>
  forgotPassword?(username: string): void | Promise<any>
  resetPassword?(options?: any): void | Promise<any>
  validateResetToken?(token: string | null): void | Promise<any>
  /** The user's data from the AuthProvider */
  getUserMetadata(): Promise<null | SupportedUserMetadata>
  client: null
  type: String
}

I think at this point you should have everything you need - but not something I’ve tried in a very long time!

Hey Danny, thanks for this. Yeah, this is a good question and I should have clarified that. In my app I give the user the ability to authorize access to some features from their account. In this case just basic settings (https://www.googleapis.com/auth/gmail.settings.basic). So I’m not using Google for user authentication, just asking for some permissions in the app.

Hope this clarifies it.

Hey friends, just following up here to see if anyone can help me out with this. Thanks

WAG, but what if the alias is conflicting?? Have you tried removing it?

const AUTHENTICATE_USER_MUTATION = gql`
  mutation AuthenticateUserMutation($code: String!) {
    authenticate(code: $code) {
      accessToken
    }
  }
`

Just a thought, but I will still look through and think some more.

1 Like

One issue in the specific code you posted is that the onSuccess has console.log instead of GoogleAuth(codeResponse).

  const login = useGoogleLogin({
    onSuccess: (codeResponse) => console.log(codeResponse),
    flow: 'auth-code',
    scope: 'https://www.googleapis.com/auth/gmail.settings.basic',
  })
1 Like

Are you using the “custom” provider and client interface pattern and that Danny mentioned? That’s how all the other auth clients are supported.

If so, could you share repo we can test this on?

Heya, I’m not as I’m not using this to authenticate the user in my application, but to authorise access to their account so I can use the API on their behalf.

Like I said, I have these both working on the front end and back end, but can’t seem to get them to talk to each other.

I do think it’s something I need to do here in onSuccess

const login = useGoogleLogin({
    onSuccess: (codeResponse) => console.log(codeResponse),
    flow: 'auth-code',
    scope: 'https://www.googleapis.com/auth/gmail.settings.basic',
  })

I did try to change from console.log to GoogleAuth to GoogleAuth(codeResponse) as suggested by @PantheRedEye but that didn’t really make any difference.

I think I’m just not really understanding how to get my front end to talk to my backend.

Could you write up an example of what you mean by getting “my front end to talk to my backend”?

Do you mean using that auth to then auth ticket GraphQL requests in cells?

A code repo example would help greatly – or some few sentences describing the use case with realistic data flow.

Otherwise if you are trying to have “some other way of have the web side call a function api-side” then you have access to the api urls in the config to use to make requests.

But is sounds like this isn’t a RedwoodJS issue per se, but some way of storing and then later using that token provided by Google to then access their api.

And that depends on how you store it and where and if you want you make that Google api call on the web or api side. And that’s up to your implementation.

Sorry, it took me so long here, but I put together an example of what I’m trying to do and how I’m coding it.

The application uses DBAuth for authentication, so nothing is out of the ordinary here. In the application, you have an option to authorize it to interact with your Google account and update your settings.

I’ve added my button to the homepage of this sample app just to make it easier. You will need to add a Google Client ID to it so it runs. You can get one by following the steps here.

The idea here is that the front end gives you an authorization code that you can then use in the backend (sdl) to generate an auth token and a refresh token. I want to retrieve those and store them in a database so I can use the refresh token later if necessary.

Please let me know if this makes sense now, but basically, this application will run some cron jobs to update your account at certain times hence why it’s important I store the refresh token as I will need that to reauthorize your account and get an access token when running those cron jobs.

thanks

Forked and taking a look.

1 Like

This might be very janky but its close to what I did when I essentially used local DB auth ontop of netlify. Just replace the mutation and the user tokens object with the required tokens.

import { useGoogleLogin } from '@react-oauth/google'

import { navigate, routes } from '@redwoodjs/router'
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
const CREATE_AUTHENTICATION_MUTATION = gql`
  mutation CreateAuthenticationMutation($input: CreateAuthenticationInput!) {
    createAuthentication(input: $input) {
      id
    }
  }
`

const GoogleAuth = () => {
  const [createAuthentication, { loading, error }] = useMutation(
    CREATE_AUTHENTICATION_MUTATION,
    {
      onCompleted: () => {
        toast.success('Authentication created')
        navigate(routes.authentications())
      },
      onError: (error) => {
        toast.error(error.message)
      },
    }
  )
  let userTokens = {}
  const login = useGoogleLogin({
    onSuccess: (tokenResponse) => {
      userTokens = tokenResponse
      createAuthentication({ variables: { userTokens } })
    },
  })
  return (
    <button onClick={() => login()} loading={loading} error={error} >
      BUTTONBUTTOONBUTTONTBUTBONTOBUTNBOTNB
    </button>
  )
}

export default GoogleAuth

This does feel gross to me for some reason.

You’d need to restructure the googles return object and then use a custom mutation to put it in a table/graph? and make a 2nd table/graph that matches the 1st table/graphs unique ID with your user tables unique ID. Gotta say my database game is still in the read the documentation and break things faze. Does prisma need join tables? I also used an if statement in the wrapper to determine what I load or show to the user. You could make a section that shows a button to link and unlink their google account with the logic for it on the buttons component.

model User {
  id      Int     @id @default(autoincrement())
  email   String?  @unique
  name    String?
  glinked Boolean @default(false)
  gID     Gaccount[]
}

model Gaccount {
  id           Int     @id @default(autoincrement())
  accesstoken  String?
  refreshtoken String?
  expirydate   String?
  gmail        String
  goath        User    @relation(fields: [accountID], references: [id])
  accountID
}

I believe this Prisma schema does the job for your backend. I don’t think you need a join table explicitly said, I am under the current idea that Prisma infers the need on creation of the DB?