Adding authentication
This step consists in adding an authentication system to the application
onekijs
provides an authentication library offering:
- several authentication methods (form based, oauth2, external login, ...)
- the restriction of access to pages to authenticated users only (can be based on roles).
- the provision of a security context accessible everywhere in the application. The security context contains data identifying the connected user (username, roles, ...).
Final result
The result of this step is as follows:
The shopping cart page (accessible via the checkout button) is now only for authenticated users.
If you are not yet authenticated, you are redirected to the login page where you can log in.
- Vite App
- Nextjs App
Securing the cart page
onekijs
provides several hooks to handle authentication
In this tutorial, we will use these hooks:
- useLogin returns a service handling the login phase. In our case, this is sending a POST request to /auth/login containing the username and password.
- useLogout returns a service managing the disconnection phase. In our case, sending a GET request to /auth/logout
- useSecurityContext returns an object identifying the disconnected user.
To prevent an unauthenticated user from accessing the shopping cart page, you can use the HOC secure (to learn more about the HOC, click here).
- Vite App
- Nextjs App
loading...
loading...
By default, an unauthenticated user is redirected to /login.
However, this path can be configured via a global configuration introduced below.
Adding a global configuration
onekijs
recommends to place the configuration settings in a central location. Usually the configuration is placed in the file src/settings.ts.
Some components of the framework use the content of this file to configure themselves. This is the case for the secure HOC.
To redirect the user to /auth instead of /login, create the file src/settings.ts
with the following content:
- Vite App
- Nextjs App
export default {
routes: {
// redirect to /auth if a non authenticated user
// tries to access a secured page
login: '/auth',
},
} as AppSettings;
<App/>
loading...
export default {
routes: {
// redirect to /auth if a non authenticated user
// tries to access a secured page
login: '/auth',
},
} as AppSettings;
<NextApp/>
loading...
Adding the login page
The login page displays a very basic form containing two fields: username and password
The page uses the useLogin hook to handle the login phase which consists of:
- sending a POST ajax request containing the username / password to a server
- process the response by creating a security context and storing it in the global state. The content of the security context is the content of the response.
useLogin also uses the global configuration src/settings.ts
to know:
- the type of authentication (form based, external, Oauth2, ...)
- the URL to use to send the POST request
Let's first update the content of src/settings.ts
- Vite App
- Nextjs App
export default {
routes: {
login: '/login',
},
idp: {
default: {
// We want to use a form based authentication
type: 'form',
// The URL to send the POST request containing
// username / password
loginEndpoint: '/auth/login',
// URL to retrieve the security context
// This URL is called to verify if the user
// is already authenticated or not
userinfoEndpoint: '/userinfo',
},
},
} as AppSettings;
The endpoints /auth/login, /auth/logout and /userinfo are mocked to simulate a backend server.
The mocked server is defined in src/__server__
You can check the documentation of the library msw to learn more about mocking a server inside a browser
The auth page uses the hook useForm which is explained later in the step Adding form
loading...
<LoginPage/>
to /loginconst RootRouter = (): JSX.Element => {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<AppLayout />}>
<Route path="/products/*" element={<ProductsRouter />} />
<Route path="/cart" element={<CartPage />} />
</Route>
<Route index element={<Navigate to="/products" replace />} />
</Routes>
);
};
export default RootRouter;
export default {
routes: {
login: '/login',
},
idp: {
default: {
// We want to use a form based authentication
type: 'form',
// The URL to send the POST request containing
// username / password
loginEndpoint: '/api/auth/login',
// URL to retrieve the security context
// This URL is called to verify if the user
// is already authenticated or not
userinfoEndpoint: '/api/userinfo',
},
},
} as AppSettings;
The auth page uses the useForm hook which is explained later in the step Adding form
loading...
Updating the Navbar to show the logged in user
Now we want to show the username of the logged in user in the navigation bar.
We can retrieve the username anywhere in the application via the useSecurityContext hook
The content of the security context is the response sent by the backend when the userinfo endpoint is called.
In our case, the backend returns a very simple object containing only the username:
{
"username": "john.doe"
}
<Navbar/>
component:- Vite App
- Nextjs App
loading...
loading...
Adding the logout page
We want to provide the user with a link to log out.
The logout action calls the backend server to delete the cookie and clean up the security context from the global state.
This action is handled by the useLogout hook. This hook gets the logout endpoint from src/settings.ts
- Vite App
- Nextjs App
export default {
routes: {
login: '/login',
},
idp: {
default: {
type: 'form',
loginEndpoint: '/auth/login',
logoutEndpoint: '/auth/logout',
userinfoEndpoint: '/userinfo',
},
},
} as AppSettings;
Create a logout page to handle the logout process.
By default, the useLogout hook sends a GET request to the backend server
loading...
Update the router to take into account this new page
const RootRouter = (): JSX.Element => {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/logout" element={<LogoutPage />} />
<Route element={<AppLayout />}>
<Route path="/products/*" element={<ProductsRouter />} />
<Route path="/cart" element={<CartPage />} />
</Route>
<Route index element={<Navigate to="/products" replace />} />
</Routes>
);
};
export default RootRouter;
export default {
routes: {
login: '/login',
},
idp: {
default: {
type: 'form',
loginEndpoint: '/api/auth/login',
logoutEndpoint: '/api/auth/logout',
userinfoEndpoint: '/api/userinfo',
},
},
} as AppSettings;
Create a logout page to handle the logout process.
By default, the useLogout hook sends a GET request to the backend server
loading...
Updating Navbar to display a logout link
const Navbar: FC = () => {
const [loggedUser] = useSecurityContext('username');
return (
<div className="app-top-bar">
<Link href="/">
<h1>My Store</h1>
</Link>
<div className="app-top-bar-right">
{loggedUser && (
<div className="user">
{loggedUser}{' '}
<Link className="logout" href="/logout">
[Log out]
</Link>
</div>
)}
<Link href="/cart" className="button fancy-button">
<i className="material-icons">shopping_cart</i>
Checkout
</Link>
</div>
</div>
);
};
export default Navbar;
Next step
Now that we can identify the logged-in user, we can save the cart contents in the cloud so we don't lose its contents after a refresh
In the next step, we introduce the services offered by onekijs
to retrieve and send data via AJAX requests.