Accessing the target url from the unauthenticated redirect

Would it be possible to add the ability to access the url the user was trying to reach if they are blocked by a Private route? It would be nice if there was an easy way to redirect the user back to the url they were trying to access once they successfully authenticate. Even if we just had access to the name of the original target url after being redirected to the unauthenticated route I think that would be enough.

Thanks for considering!

3 Likes

I definitely think this is an interesting idea and could see myself finding it useful. Is it something you’d want to take a swing at (in the form of a PR)?

1 Like

The way I’m doing this currently is using a custom React hook, and by placing the specific page outside the <Private> block so that redwood router doesn’t automatically redirect for us.

web/src/utils/useAuthRedirect.js

import { useEffect } from 'react'
// you might want to use the useAuth function from redwood/lib/auth instead
import { useCustomAuth } from 'src/utils/useCustomAuth'

const useAuthRedirect = () => {
  const { isAuthenticated, loading } = useCustomAuth()

  useEffect(() => {
    const returnTo = location.pathname + location.search
    if (!isAuthenticated) {
      window.location = `/login?returnTo=${returnTo}`
    }
  }, [isAuthenticated])

  return { isAuthenticated, loading }
}

export default useAuthRedirect

And in your page/layout you can use it like this, for example

ExamplePage.js

const ExamplePage = (params) => {
  const { isAuthenticated } = useAuthRedirect()

  return (
    <DashboardLayout>
      {isAuthenticated && <ExampleCell label={params.label} />}
    </DashboardLayout>
  )
}

I’d be happy to work on a PR for this. I’m having some trouble getting a local dev version of Redwood to be used in my app though. I’m using the Local Package Registry Emulation approach, and while Verdaccio seems to be working, my app doesn’t seem to be using that version of Redwood. I’ll mess with it for a while longer to see if I can get it to connect.

Assuming I get Verdaccio working, I was trying to think of the right way to add something like a “next url” to the private routes. The most straightforward way might be to add a prop to the Redirect component here that accepts query params to add to the resulting redirected URL. Then when Redirect is used in the PrivatePageLoader it could include a query param pointing to the current URL taken from useLocation. It would be up to the page indicated in the unauthenticatedRoute to handle the query param and redirect the user back to that page once they have successfully authenticated.

If that sounds like a reasonable approach, I’ll spend some more time trying to get a local dev version of Redwood working.

I’ve been thinking about this for a little while.

If you look at the auth0 client:

export const auth0 = (client: Auth0): AuthClientAuth0 => {
  return {
    type: 'auth0',
    client,
    restoreAuthState: async () => {
      if (window.location.search.includes('code=')) {
        const { appState } = await client.handleRedirectCallback()
        window.history.replaceState(
          {},
          document.title,
          appState && appState.targetUrl
            ? appState.targetUrl
            : window.location.pathname
        )
      }
    },
    login: async () => client.loginWithRedirect(),
    logout: (options?) => client.logout(options),
    getToken: async () => client.getTokenSilently(),
    getUserMetadata: async () => {
      const user = await client.getUser()
      return user || null
    },
  }
}

If you were to pass in options with the RedirectLoginOptions with redirect_uri overriding that in your client config – and making sure that this path is an allowed redirect url in Auth0 setup …

https://auth0.github.io/auth0-spa-js/interfaces/redirectloginoptions.html#redirect_uri

… then when your unauthenticated page renders assuming it has a login button and you set that to the location … that would redirect you and no changes needed to router or page at all.

It’s similar to the restoreAuthState:

where you’d set the window.history to the targetUrl because the appState has it already.

I’m going to try this out in my app (with a change to the RW auth0 client).

Reading: https://auth0.com/docs/users/guides/redirect-users-after-login

1 Like

Did some experiments today and found that if I let options be passed to Auth0’s login() (code change):

/* packages/auth/src/authClients/auth0.ts */

    login: async (options?) => client.loginWithRedirect(options),

then when I invoke that login, via say a button:

    <Button
      onClick={async () => {
        if (isAuthenticated) {
          await logOut({ returnTo: process.env.AUTH0_REDIRECT_URI })
        } else {
          await logIn({ appState: { targetUrl: 'http://localhost:8910/iwanttogotothere' } })
        }
      }}
    >
      {isAuthenticated ? 'Log out' : 'Log in'}
    </Button>

I can pass in to Auth0’s appState and there already was code there to handle the redirect in

/* packages/auth/src/authClients/auth0.ts */

    restoreAuthState: async () => {
      if (window.location.search.includes('code=')) {
        const { appState } = await client.handleRedirectCallback()
        window.history.replaceState(
          {},
          document.title,
          appState && appState.targetUrl
            ? appState.targetUrl
            : window.location.pathname
        )
      }
    },

So, now would just need to know what that value for targetUrl should be.

The most straightforward way might be to add a prop to the Redirect component here that accepts query params to add to the resulting redirected URL

I’m not certain, but feel like adding a property to the AuthProviderState

/* packages/auth/src/AuthProvider.tsx */
export class AuthProvider extends React.Component<
  AuthProviderProps,
  AuthProviderState
> {
  static defaultProps = {
    skipFetchCurrentUser: false,
  }

  state: AuthProviderState = {
    loading: true,
    isAuthenticated: false,
    userMetadata: null,
    currentUser: null,
  }

because then it would be easy to:

  • extract by way of useAuth
  • reset to null on logout

What if we added it as a query part to the url? http://localhost:8910/login/?redirectTo=/admin/ (escaped)

+1, I think that’s the most intuitive pattern to have, for sure.

Another thing to consider is if the token expires. So technically the user would be “logged in” but not authorized as they’ll need to login again.

+1 Agreed. simpler = better

So, I did a quick proof of concept (emphasis on quick).

1 - Add redirectTo to PrivatePageLoader in router.js

/* router.js*/

const PrivatePageLoader = ({
  useAuth,
  unauthenticatedRoute,
  redirectTo,
  children,
}) => {
  const { loading, isAuthenticated } = useAuth()

  if (loading) {
    return null
  }

  if (isAuthenticated) {
    return children
  } else {
    return (
      <Redirect to={`${unauthenticatedRoute()}?redirectTo=${redirectTo}`} />
    )
  }
}

:point_up: that’s some ugly-ish interpolation and not 100% sure it gets escaped

2 - in RouterImpl, privateRoutes:

          React.cloneElement(route, {
            private: true,
            redirectTo: null,
            unauthenticatedRedirect: unauthenticated,
          })

3 - and pass the path to that loader

          return (
            <PrivatePageLoader
              useAuth={useAuth}
              unauthenticatedRoute={
                namedRoutes[route.props.unauthenticatedRedirect]
              }
              redirectTo={route.props.path}
            >
              <Loaders />
            </PrivatePageLoader>

4 - In your app, when invoking login (for Auth0)

          const searchParams = new URLSearchParams(window.location.search)
          await logIn({
            appState: { targetUrl: searchParams.get('redirectTo') },
          })

Have to do some testing, but the basic flow works.

FYI - A WIP PR is https://github.com/redwoodjs/redwood/pull/876