PF project: Authentification
This post is intended as a personal exploration of various software patterns, rather than a guide.
What do we working with?
- Client - Next JS
- Server - Pyhton FastApi
Our backend using JWT tokens as an access token with rotation of refresh tokens.
Libraries
On the clinet side I use Tanstack React Query to fetch and cashe user session data
- React-Query - elegant layer for everething related to fetching and cashing Everything is intuitive and handled with a very little amount of code.
- Axios for http requests - cleaner and nicely combines with React-useQuery
Approach
To fetch and cache session information, I divided API calls in 2 categories:
Actions
- API calls that does not have an effect on cached entity data.Custom hooks
- React-useQuery API calls that influences a cached entity in-app.
Action example
authActions.ts
async function login(credentials){
try {
const { data, status } = await apiLoginUser(credentials);
presistTokens(data.tokens)
return { status: status };
} catch (error) {
hanldeError(error.response.status )
return { status: error.response.status };
}
}
Custom hook example
useAuth.tsx
function useAuth(){
return useQuery('user', getSession, {
enabled: false, // needed to handle refetch manually
retry: false,
staleTime: 60 * (60 * 1000), // 60 min
onSuccess: (data) => {
/* success callback */
},
onError: () => {
/* error callback */
},
/* `select` will be returned `data` when accessing query results */
select: (data) => formatResponse(data),
});
};
Flow
Each time the Client will make a request for protected page/data - we do request on the Server. No need to store User data in React state.
Login pattern:
- Our
login
(action) send user credentials and receives JWT. - Short-lived JWT is encoded and stored and being sent with each protected API call.
- On successful
login
, we trigger re-fetching in custom hookuseAuth()
- Each component that depends on the user session will be updated based on new
useAuth()
query result
Components examples
Login.tsx
function Login(){
...
const { refetch } = useAuth()
async function onLoginSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const { status } = await login({ username, password });
if (status === 200) {
/* trigger user refetch to update cashed user */
refetch();
}
}
return (
<form onSubmit={onLoginSubmit}>
...
</form>
)
}
Navbar.tsx
function Navbar() {
const { data: user, isLoading } = useAuth();
return (
<nav >
...
{ user ? <AppLink /> : <LoginButton />}
...
</nav>
);
}