Combining dbAuth + OAuth2

After a lengthy discussion with @rob and @dthyresson (thanks both!) I managed to finish my auth service.

TL;DR - cookie sessions from dbAuth, but instead of username/password I do an OAuth exchange with a 3rd party OAuth provider. Wanted to share here in case it helps anyone trying to massage dbAuth to their specific needs.

Redwood v0.37.1

Step one is replacing the login function from dbAuth with a redirect to the OAuth authority

import { AuthProvider } from '@redwoodjs/auth'
import { login } from 'src/auth/client'
// web/src/App.js

const dbAuth = new AuthProvider({ type: 'dbAuth' })
dbAuth.logIn = login // Replace with our OAuth redirect
const CustomDbAuth = () => dbAuth
CustomDbAuth.prototype = React.Component.prototype

const App = () => (
  <FatalErrorBoundary page={FatalErrorPage}>
    <RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
      <CustomDbAuth type="dbAuth">
      // ...
    )

Now the redwoodjs useAuth logIn function will fetch the url and redirect the user

// web/src/auth/client.js
export const login = async ({ redirectTo }) => {
  redirectTo && saveRedirectTo(redirectTo)
  const response = await global.fetch(
    `${global.__REDWOOD__API_PROXY_PATH}/graphql`,
    {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        query: LOGIN_URL_QUERY,
      }),
    }
  )
  if (response.ok) {
    // Redirect user
    const { data } = await response.json()
    window.location = data.loginUrl.url
  }
}

When the user returns, we trigger the dbAuth login. Instead of username/password, we provide code and state to complete the OAuth flow.

// web/src/components/Redirect.js

const submitOauthCodeGrant = async () => {
  // Call the dbAuth login function
  const response = await fetch(`${global.__REDWOOD__API_PROXY_PATH}/auth`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code, state, method: 'login' }),
  }).then((res) => res.json())
  if (response.error) return setErrorMessage(response.error)
  if (response.id) window.location.href = getRedirectTo() || '/'
}

React.useEffect(() => {
  checkValidParams()
  submitOauthCodeGrant()
}, [])

Now in the api, we must replace the login function in our DbAuthHandler. Instead of consuming username/password, it performs the OAuth code grant.

// api/src/functions/auth.js

import { handleOauthCodeGrant } from 'src/lib/oAuth/oAuth'

const authHandler = new DbAuthHandler(event, context, {
    const loginOptions = {
      // Normal stuff here....
    }
})

// Replace the login with our own OAuth logic
authHandler.login = async () => {
  const { type, code, state } = this.params

  // Do Oauth and create a new user in our database
  const user = await handleOauthCodeGrant({ state, code })

  const sessionData = { id: user[this.options.authFields.id] }

  // TODO: this needs to go into graphql somewhere so that each request makes
  // a new CSRF token and sets it in both the encrypted session and the
  // csrf-token header
  const csrfToken = DbAuthHandler.CSRF_TOKEN

  return [
    sessionData,
    {
      'csrf-token': csrfToken,
      ...this._createSessionHeader(sessionData, csrfToken),
    },
  ]
}

return await authHandler.invoke()

Success! We’ve saved a session cookie, which gets passed in the graphql requests.

Discussion

We need more :brain: on Implement CSRF checking with dbAuth · Issue #3075 · redwoodjs/redwood · GitHub . Happy to put a :moneybag: bounty on this if someone wants to work on it!

5 Likes

Update: here’s a better way to extend the client, without getting weird react warnings. Courtesy of @olance

import { AuthProvider } from '@redwoodjs/auth'

class AuthProviderExtended extends AuthProvider {
  constructor(props) {
    super(props)
    this.rwClient.login = logIn // Your custom login stuff goes here
  }
}

const App = () => (
  <FatalErrorBoundary page={FatalErrorPage}>
    <RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
      <AuthProviderExtended type="dbAuth">
2 Likes

This is very handy, I wanted to see what something built into dbAuth would look like and this is what I have so far.

1 Like

This is amazing work @Irev-Dev keep it going!

1 Like

@pi0neerpat hi, does your snippet still works ? And can you provide a full working example/sample, I think some part are missign like details about where/how use Rediretc component or the LOGIN_URL_QUERY magik part

Thank you !

PS: and what is { handleOauthCodeGrant } ???

Haven’t had time to create a full demo, but when I do I’ll add it here GitHub - pi0neerpat/redwood-devops-example

handleOauthCodeGrant is your internal service to do the token exchange with the OAuth provider and get the user’s access token.

The redirect page is just a page with some loading animation. You can see a working example here: https://treasure.chess.com

It would be really nice to have an updated, complete version of this.

1 Like

I’ve ended up rolling my own now Oauth implementation via GitLab

Do we have any better solution now there isn’t much resource online on how to properly implement this

Ask and you shall receive I made PassportJS for Redwood