Last week, we added OAuth with Google for Authentication server-side. That is one of the two options of handling Authentication. The other would be to authenticate the user frontend/client-side. If this approach is chosen, however, the authentication should definitely be verified server-side.
There are advantages to have authentication done server-side or client-side. I prefer to have it done client-side, and then with the backend validation, as this way, even if we have multiple backends, the user will not need to log into each backend separately. A token will be passed from the one frontend to all backends, and these can simply verify the token for its authenticity.
In this article, we’ll add a login page that is only prompted when the user tries to access pages that require being authenticated. All other pages should be visible without logging in. The protection of authenticated pages will follow in one of the next articles.$ Also, we will currently only create the Google login page, but the user should have the choice if he wants to use his Google credentials, or perhaps something else, which we may add in the future. These options will already be shown - they just won’t be functional.
There will be only the React part of the application landscape. The verification of the token will follow in the next article.
Adding the link to the login
We start by adapting our Navigation to create the link to the login page. To do this, we use the useHistory
hook provided
by the react-router-doc
package. In the newer versions, this has been replaced by useNavigate
, just in case you are
following along and your IDE shows some errors.
So we add the following snippets in our src/routing/SmallNavBar.tsx
:
const [auth, setAuth] = useState(false);
const history = useHistory();
const navigateToLogin = () => {
history.push('/login')
}
{auth ? (
<div>
<IconButton
size="large"
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
color="inherit"
>
<AccountCircle/>
</IconButton>
</div>
) :
<Button color="inherit" onClick={navigateToLogin}>Login</Button>
}
You may notice that, for now, the auth is still hardcoded to false, as we’ve had it before. This will change soon.
In the src/routing/ApplicationRouter.tsx
, we add the Route to the LoginPage:
<Route path="/login" component={LoginPage}/>
Creating a login page
Now we want to actually create the login page. In here, we’d like to offer the user some options that he can log in with.
Since at some point in the future, we may want to add also a username and password based login, I will already add those first:
in the LoginPage
:
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import Link from '@mui/material/Link';
import Grid from '@mui/material/Grid';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import CssBaseline from "@mui/material/CssBaseline";
import Divider from '@mui/material/Divider';
import Chip from '@mui/material/Chip';
import GoogleLoginHook from './GoogleLoginHook';
function Copyright(props: any) {
return (
<Typography variant="body2" color="text.secondary" align="center" {...props}>
{'Copyright © '}
<Link color="inherit" href={"/"}>
Money Management
</Link>{' '}
{new Date().getFullYear()}
{'.'}
</Typography>
);
}
export const LoginPage = () => {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
// eslint-disable-next-line no-console
console.log({
email: data.get('email'),
password: data.get('password'),
});
};
return (
<Container component="main" maxWidth="xs">
<CssBaseline/>
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar sx={{m: 1, bgcolor: 'secondary.main'}}>
<LockOutlinedIcon/>
</Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1}}>
<TextField
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
/>
<FormControlLabel
control={<Checkbox value="remember" color="primary"/>}
label="Remember me"
/>
<Button
type="submit"
fullWidth
variant="contained"
sx={{mt: 3, mb: 2}}
>
Sign In
</Button>
<Grid container>
<Grid item xs>
<Link href="#" variant="body2">
Forgot password?
</Link>
</Grid>
<Grid item>
<Link href="#" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</Box>
</Box>
<Copyright sx={{mt: 8, mb: 4}}/>
</Container>
);
}
You may notice that there are some additional links for forgotten passwords, registrations, etc. These are currently of no use yet, but will also be added in the future.
Now, come to think of it, I would like to have the Google Login display the Google logo also. Since we’re not allowed to just use
any logo from Google, here is the page where you can download the latest
images provided by them for this exact purpose. I have chosen one of the icons and put it under public/icons/google.png
.
Adding Google OAuth
Next, we need to rethink how we’d like OAuth to work. Technically, we already have a way of adding the users after their first
login into the DB on server-side. However, it may make sense to gather the OAuth token already on the frontend, and pass this
to the backend upon any operation. In order to add this functionality, we add the popular library react-google-login
to handle the login with Google client-side. We install it first by typing
npm i react-google-login
. Now we have some useful hooks or components that we can use to handle the login.
The hook itself is added as such:
import React from 'react';
import {useGoogleLogin} from 'react-google-login';
// refresh token
// import { refreshTokenSetup } from './refreshTokenSetup';
const clientId = `${process.env.REACT_APP_GOOGLE_CLIENT_ID}`;
function GoogleLoginHook() {
const onSuccess = (res: any) => {
console.log('Login Success: currentUser:', res.profileObj);
alert(
`Logged in successfully welcome ${res.profileObj.name} 😍.`
);
// refreshTokenSetup(res);
};
const onFailure = (res: any) => {
console.log('Login failed: res:', res);
alert(
`Failed to login.`
);
};
const {signIn} = useGoogleLogin({
onSuccess,
onFailure,
clientId,
isSignedIn: true,
accessType: 'offline',
// responseType: 'code',
// prompt: 'consent',
});
return (
<div style={{cursor: "pointer"}}>
<img onClick={signIn} src="icons/google.png" alt="google login" className="icon"></img>
</div>
);
}
export default GoogleLoginHook;
The value for the REACTAPPGOOGLECLIENTID is added in the .env
file, and it should be the same as we’ve previously used
in the backend. (Don’t forget to restart your application after extracting the value into the .env
file!)
In order for this to work, keep in mind that you’ll also need to tell Google that there will be requests for the OAuth registration
coming from this host. So we need to add the http://localhost:3000/login
to the JavaScript origins:
Great, let’s now add the section for the GoogleLoginHook to the LoginPage, below the box with the password recovery:
<Divider style={{width:'100%', paddingTop: "15px", paddingBottom: "15px"}} variant="middle">
<Chip label="OR"/>
</Divider>
<Box>
<Grid container>
<Grid item xs>
<GoogleLoginHook/>
</Grid>
<Grid item>
Facebook login will be here
</Grid>
</Grid>
</Box>
The LoginPage now looks as follows:
If we now click on the Sign In with Google
button, we get forwarded to the familiar Google Authentication view:
And our alert shows us that the Login was successful!
Similarly, we can have the Logout taken care of client-side:
import React from 'react';
import {useGoogleLogout} from 'react-google-login';
const clientId = `${process.env.REACT_APP_GOOGLE_CLIENT_ID}`;
function GoogleLogoutHook() {
const onLogoutSuccess = () => {
console.log('Logged out Success');
alert('Logged out Successfully ✌');
};
const onFailure = () => {
console.log('Handle failure cases');
};
const {signOut} = useGoogleLogout({
clientId,
onLogoutSuccess,
onFailure,
});
return (
<button onClick={signOut} className="button">
<span className="buttonText">Sign out</span>
</button>
);
}
export default GoogleLogoutHook;
If you’d like to test this, simply add this hook somewhere in the NavBar and set the authenticated status to true. The alert should pop up after clicking to confirm the successful logout. (You may want to change the button - this is only temporary for now.)
Okay, there we have it! We can now authenticate the frontend application with Google!
In the next articles, we’ll focus on how to verify the token with Spring in the backend. I will focus an entire article on this, as I have been looking a long time myself on how to get this to work. It’s actually quite easy, but it’s not that easy to find how to do it. Therefore, people who may want to do this should have a small article without the entire frontend part.
We’ll also focus on how to make the application aware of being logged in. We’ll cover the useContext
hook for that purpose,
as well as how to store some information locally, so that we’ll be able to handle refreshes as well.
Bonus: Token refresh
The generated token is working just fine now. However, the generated token has a lifespan of one hour usually. Now, if the user wants to browse the page for a longer time, it would be a real shame if it stopped working after that. So we’ll cover the automated refresh for the user.
You may have noticed earlier that a certain line in the login hook was commented out. We’ll now implement this refresh by
uncommenting it, and adding that functionality. It will look like this (of course, you should remove all the console.logs
before committing) in the refreshTokenSetup.ts
on the same level:
export const refreshTokenSetup = (res: any) => {
// Timing to renew access token
let refreshTiming = (res.tokenObj.expires_in || 3600 - 5 * 60) * 1000;
console.log('New token required in ', refreshTiming);
const refreshToken = async () => {
const newAuthRes = await res.reloadAuthResponse();
refreshTiming = (newAuthRes.expires_in || 3600 - 5 * 60) * 1000;
console.log('newAuthRes:', newAuthRes);
// saveUserToken(newAuthRes.access_token); <-- save new token
localStorage.setItem('authToken', newAuthRes.id_token);
// Setup the other timer after the first one
setTimeout(refreshToken, refreshTiming);
};
// Setup first refresh timer
setTimeout(refreshToken, refreshTiming);
};
Now, importing that refresh back into the hook:
function GoogleLoginHook() {
const onSuccess = (res: any) => {
console.log('Login Success: currentUser:', res.profileObj);
console.log("Now registering with backend...")
//AuthService.loginUser(res.tokenObj);
refreshTokenSetup(res);
};
...
}
In order to test that the refresh is working, divide the timeouts by 1000, so they take place every 3.6 seconds instead of once every hour. If successful, you will see this in the console:
Sweet! Now the frontend is fully covered for Google authentication! Keep posted for the next articles to include the backend and improve the user experience in the UI with more customization!