All Articles

OAuth for authentication and authorization in React applications

Security
Protecting data is extremely key in applications handling sensitive information

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.

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:

JS Origins
For now, we're still only working from localhost

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:

Login Page
Login options look pretty neat I'd say

If we now click on the Sign In with Google button, we get forwarded to the familiar Google Authentication view:

Confirm Login screen
The application we're logging into is familiar also

And our alert shows us that the Login was successful!

Successful login alert
This alert will be removed of course, but it's pleasant to see!

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:

Token refreshes
The token is getting automatically refreshed, without user interference!

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!