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!

2 Likes