T
TanStack2y ago
fair-rose

Amplify authenticator component

Hi there, I am using AWS Amplify's react authenticator component with cognito and would appreciate some guidance 🙂 I am wondering the best approach to handle authenticated routes. If I was having the whole app behind authentication, I would wrap the routerprovider in App.tsx with the Authenticator component. However I only need to force a user to authenticate when they attempt to make an action of a logged in user. I have read through the Authenticated Routes docs page and explored the examples on stackblitz but am a bit overwhelmed. Thank you very much. Related links: https://ui.docs.amplify.aws/react/connected-components/authenticator https://ui.docs.amplify.aws/react/connected-components/authenticator/advanced
Amplify UI
Authenticator | Amplify UI for React
Authenticator component adds complete authentication flows to your application with minimal boilerplate.
Amplify UI
Advanced Usage | Amplify UI for React
Access Authenticator UI component state outside of the UI component
20 Replies
like-gold
like-gold2y ago
Hey, no worries. Looking at the docs, it shouldn't be too difficult doing this in an SPA. 1. If you are using <Authenticator>, with its default flow of blocking all UI if not authenticated with its components, you could render the <RouterProvider /> in its functions a children method. ie:
<Authenticator>
{({ user, signout }) => (
<RouterProvider context={{ user, signout }} />
)}
</Authenticator>
<Authenticator>
{({ user, signout }) => (
<RouterProvider context={{ user, signout }} />
)}
</Authenticator>
2. Or if you want to do something similar to the authenticated routes example, and handle this logic by yourself using the useAuthenticator hook. This is approx. of it being consumed by router and ofc. you'd need to wrap this in <Authenticator.Provider> first.
const { authStatus, signout, user = null } = useAuthenticator(context => [context.authStatus, context.signout, context.user]);

return authStatus === 'configuring' ? <Loader /> : <RouterProvider context={{ authStatus, signout, user }} />
const { authStatus, signout, user = null } = useAuthenticator(context => [context.authStatus, context.signout, context.user]);

return authStatus === 'configuring' ? <Loader /> : <RouterProvider context={{ authStatus, signout, user }} />
All-in-all its honestly not that bad.
fair-rose
fair-roseOP2y ago
Thank you very much for replying @Sean Cassiere, I will review and make some attempts and report back 🙂 Thanks again @Sean Cassiere, I think it is working now as I expect it to ! I had to move Auth.Provider up to main.tsx level
like-gold
like-gold2y ago
Good stuff! Makes sense about having to move the AuthProvider up a level
fair-rose
fair-roseOP2y ago
Appreciate it, I've only ever built one (very basic) app before, using electron and react router, so am learning a lot
like-gold
like-gold2y ago
🎉 ship ship ship
fair-rose
fair-roseOP2y ago
Hope you don't mind me replying here again Sean 🙂 I was re-reading https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes#authentication-using-react-contexthooks and I wondered do I need to adjust my
export const Route = createRootRoute
export const Route = createRootRoute
to use createRootRouteWithContext instead? and similarly to adjust my:
const router = createRouter({ routeTree, defaultNotFoundComponent: NotFound })
const router = createRouter({ routeTree, defaultNotFoundComponent: NotFound })
to add in
context: {auth: undefined!}
context: {auth: undefined!}
here? this is my current :
function App() {
console.log("hello")
const {
authStatus,
signOut,
user = null,
} = useAuthenticator((context) => [
context.authStatus,
context.signOut,
context.user,
])

return (
<>
{authStatus === "configuring" ? (
<div>Loading...</div>
) : (
<RouterProvider
context={{ authStatus, signOut, user }}
router={router}
/>
)}
</>
)
}
function App() {
console.log("hello")
const {
authStatus,
signOut,
user = null,
} = useAuthenticator((context) => [
context.authStatus,
context.signOut,
context.user,
])

return (
<>
{authStatus === "configuring" ? (
<div>Loading...</div>
) : (
<RouterProvider
context={{ authStatus, signOut, user }}
router={router}
/>
)}
</>
)
}
Authenticated Routes | TanStack Router Docs
Authentication is an extremely common requirement for web applications. In this guide, we'll walk through how to use TanStack Router to build protected routes, and how to redirect users to login if they try to access them. The route.beforeLoad Option
like-gold
like-gold2y ago
Yup, if you are piping in your auth stuff into the Router's context, then you'll want to use the createRootRouteWithContext function when creating your root route. Like:
interface MyRouterContext {
auth: YourTypeHere
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
// ...
})
interface MyRouterContext {
auth: YourTypeHere
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
// ...
})
And when creating the router just set the field as undefined since you'll be setting it later like what you are doing in the App.
fair-rose
fair-roseOP2y ago
Thanks very much Sean, appreciate you answering what is probably a very basic question! 🙂
absent-sapphire
absent-sapphire2y ago
@Sean Cassiere Talking about auth. How can I make it automatically log-out when useQuery/useMutation onError occurs related to 401 or token expired?
like-gold
like-gold2y ago
That'd be really specific to your auth implementation. Intercepters could be used to achieve that in your API calls, or when an API call fails you can update your React context which then invalidates the router using router.invalidate(), and you could catch errors in the loader and also trigger a logout, and etc... Not to mention this can be compounded is you want to allow for retries (maybe with delays...) This is quite implementation specific and you'll only really be able figure the how part of this process once you've settled on the process for correctly determining this. On the side of TSR, when you've figured out how to get your app to the logout state, it's a matter of calling the invalidate method on the router instance.
absent-sapphire
absent-sapphire2y ago
@Sean Cassiere Thanks a lot for your enlighten.
fair-rose
fair-roseOP2y ago
Hope you don't mind if I sanity check my App and root with you, Sean 😆 App.tsx:
import { useAuthenticator } from "@aws-amplify/ui-react"
import { RouterProvider, createRouter } from "@tanstack/react-router"
import { routeTree } from "./routeTree.gen"
import NotFound from "./components/common/NotFound"

const router = createRouter({
routeTree,
context: { authStatus: undefined! },
defaultNotFoundComponent: NotFound,
})

declare module "@tanstack/react-router" {
interface Register {
router: typeof router
}
}

function App() {
console.log("hello")
const { authStatus } = useAuthenticator((context) => [context.authStatus])

return (
<>
{authStatus === "configuring" ? (
<div>Loading...</div>
) : (
<RouterProvider context={{ authStatus }} router={router} />
)}
</>
)
}

export default App
import { useAuthenticator } from "@aws-amplify/ui-react"
import { RouterProvider, createRouter } from "@tanstack/react-router"
import { routeTree } from "./routeTree.gen"
import NotFound from "./components/common/NotFound"

const router = createRouter({
routeTree,
context: { authStatus: undefined! },
defaultNotFoundComponent: NotFound,
})

declare module "@tanstack/react-router" {
interface Register {
router: typeof router
}
}

function App() {
console.log("hello")
const { authStatus } = useAuthenticator((context) => [context.authStatus])

return (
<>
{authStatus === "configuring" ? (
<div>Loading...</div>
) : (
<RouterProvider context={{ authStatus }} router={router} />
)}
</>
)
}

export default App
__root.tsx:
import { createRootRouteWithContext } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
import MainLayout from "../layouts/MainLayout"

interface MyRouterContext {
authStatus: string
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => (
<>
<MainLayout />
<TanStackRouterDevtools />
</>
),
})
import { createRootRouteWithContext } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
import MainLayout from "../layouts/MainLayout"

interface MyRouterContext {
authStatus: string
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => (
<>
<MainLayout />
<TanStackRouterDevtools />
</>
),
})
I removed signOut and user as I was unsure why they were being passed
like-gold
like-gold2y ago
Yea, that looks fine. I'd probably still pass in the user/auth object and sign-out method for no other reason that its useful having the accessToken in your loaders, and it allows you to set up a /logout route. But this is purely icing on-top, what you've done looks good 👍🏼.
fair-rose
fair-roseOP2y ago
This is my _auth.tsx, which you might recognise from the file based kitchen sink example, I am unsure what the purpose/reason is for the " // Otherwise, return the user in context"
import { createFileRoute, redirect } from '@tanstack/react-router'

export const Route = createFileRoute("/_auth")({
// Before loading, authenticate the user via our auth context
// This will also happen during prefetching (e.g. hovering over links, etc)
beforeLoad: ({ context, location }) => {
// If the user is logged out, redirect them to the login page
if (context.authStatus === "unauthenticated") {
throw redirect({
to: "/login",
search: {
// Use the current location to power a redirect after login
// (Do not use `router.state.resolvedLocation` as it can
// potentially lag behind the actual current location)
redirect: location.href,
},
})
}

// Otherwise, return the user in context
return {
username: auth.username,
}
},
})
import { createFileRoute, redirect } from '@tanstack/react-router'

export const Route = createFileRoute("/_auth")({
// Before loading, authenticate the user via our auth context
// This will also happen during prefetching (e.g. hovering over links, etc)
beforeLoad: ({ context, location }) => {
// If the user is logged out, redirect them to the login page
if (context.authStatus === "unauthenticated") {
throw redirect({
to: "/login",
search: {
// Use the current location to power a redirect after login
// (Do not use `router.state.resolvedLocation` as it can
// potentially lag behind the actual current location)
redirect: location.href,
},
})
}

// Otherwise, return the user in context
return {
username: auth.username,
}
},
})
Okay I think I understand, I will make an attempt on it. I guess I will ned to update the type interface so it expects the object that amplify returns.. PS: do you have a "buy me a beer" link or similar? I am grateful for your time spent replying to me
like-gold
like-gold2y ago
Its basically so, now all the child routes in the _auth layout will be able to access the context.username. For example, a hypothetical dashboard page inside the auth layout will be able to access the username field inside the beforeLoad and loader callbacks, as well as the useRouteContext hook. Like I said, its mostly icing on-top. Do what you need to get your app up and running. Its easy enough to augment it in the future. Especially with typescript since Symbol renaming exists (using the F2 key in vs-code). Got a github sponsors page, but don't worry about it. I'm here because I've been super passionate about Router from November 2022, and still am using it.
like-gold
like-gold2y ago
You don't necessarily need to even return anything in your _auth layout either. This is from a project of mine, where I'm really just using the auth layout to checking for the logged-in status, if not triggering the sign-in.
GitHub
nv-rental-clone/src/routes/_auth.tsx at ea7749989a839beefeb6e790b37...
Navotar with Tailwind and the Tanstack. Contribute to SeanCassiere/nv-rental-clone development by creating an account on GitHub.
fair-rose
fair-roseOP2y ago
Would all the child routes not already receive
context.username
context.username
along with other stuff included in the user object (amplify) when the user logs in? Trying to understand why it specifically returns the username here after login (hope I am making sense!)
like-gold
like-gold2y ago
You are right, but the types for auth object will always have the user as being possibly null/undefined. When you do a return in the auth layout with the user object being defined, now the child routes know that this field in context at runtime will definitely have some data defined. It's not too dissimilar to the middleware concepts used by tRPC.
fair-rose
fair-roseOP2y ago
Thanks Sean, that makes sense! I have been doing some research to try figure out why auth redirect is not redirecting back to location.href and I think I have discovered problem with the docs I've noticed that some links I follow aren't going to the corresponding page, and instead putting me on the overview page I think it is because, for example the redirect function link that shows up when you search is: https://tanstack.com/router/v1/docs/api/router/redirectFunction but the actual link that works is: https://tanstack.com/router/v1/docs/framework/react/api/router/redirectFunction
Overview | TanStack Router Docs
TanStack Router is a router for building React applications. Some of its features include: 100% inferred TypeScript support
redirect function | TanStack Router Docs
The redirect function returns a new Redirect object that can be either returned or thrown from places like a Route's beforeLoad or loader callbacks to trigger redirect to a new location. redirect options
like-gold
like-gold2y ago
Yea, this search being broken on the docs has been bugging us for the past couple months for us as well. The Algolia's DocSearch platform is only generating links using the old tanstack website schema, and not at all indexing the new pages. We're a bit stuck on that front because Algolia doesn't really have a support channel for OSS projects using DocSearch.

Did you find this page helpful?