0

I'm trying to implement Azure AD SSO into my application, but am running into an issue where my app tries to use the code it pulls our the the URL more than once (not sure why it is running multiple times), but this causes Azure AD to send an error.

The error is: AADSTS54005: OAuth2 Authorization code was already redeemed, please retry with a new valid code or use an existing refresh token.

I think I understand that the error is because my app is trying to use the code more than once - but it SHOULD NOT BE.

I'm trying to figure out why it is and then stop it. I'm going to try and add all the relevant code snippets, but if you feel like something is missing, let me know and I can try and help.

I tried many things - mostly in regard to the AzureRedirect.jsx useEffect() since this is where the logic that is running more than once when it should run once is.

  1. I added console logs to make sure that I was right about the useEffect() running more than once and also I added a console log into just the component, outside of the useEffect() to see that the component was rendering several times

  2. I tried moving the logic that creates the 'code' object and populates it from the URL outside of the useEffect()

  3. I tried moving the if(user) {navigate('/')} logic outside of the useEffect into the if/else statement below the useEffect()

  4. I checked that the window.location.search value is not changing unexpectedly by using console.log

  5. I tried messing around with the dependencies for the useEffect() - removing and adding various combinations of dependencies. Nothing worked, though some did 'sometimes' produce slightly different results, but then if I ran the test again, I'd be back to the same original error (weird!)

  6. I also tried adding a return statement into the useEffect() to make sure it only ran once, but this didn't stop it from running multiple times... and at this point, I'm not sure the useEffect() doesn't need to be accessed more than once.

Now onto the code, hopefully this clears up any confusion from reading my previous problem/what I tried.

The component with the useEffect() that gets the URL code and is trying to reuse it:

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { azureRedirect } from '../features/auth/authSlice';
import { Link, useNavigate } from 'react-router-dom';
import Spinner from './Spinner';

const AzureRedirect = () => {
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const { user } = useSelector((state) => state.auth);

  useEffect(() => {
    console.log('Im in AzureRedirect.jsx useEffect()');

    const code = {
      code: new URLSearchParams(window.location.search).get('code'),
    };

    if (user) {
      navigate('/');
    } else if (code) {
      dispatch(azureRedirect(code));
    } else {
      console.log('No code found in URL');
    }
  }, [dispatch, navigate, user]);

  let displayedOutput;

  if (!user) {
    displayedOutput = (
      <div>
        An error has been encountered, please contact your administrator.
        <br />
        <Link to='/login'>Return to Login</Link>
      </div>
    );
  } else {
    return <Spinner />;
  }

  return <div className='pt-4'>{displayedOutput}</div>;
};

export default AzureRedirect;

Now I'm going to post the following code - the AzureRedirect action from my slice(redux), service(redux), and controller(backend)

authSlice.js

export const azureRedirect = createAsyncThunk(
  'users/azureRedirect',
  async (code, thunkAPI) => {
    try {
      console.log('Im in users/azureRedirect');
      return await authService.azureRedirect(code);
    } catch (error) {
      return thunkAPI.rejectWithValue(extractErrorMessage(error));
    }
  }
);

authService.js

const azureRedirect = async (code) => {
  console.log('Im in azureRedirect in the authService.js file');

  try {
    const response = await axios.post(API_URL + 'az-redirect', code);
    // Save to localStorage
    const { token, user } = response.data;
    user.token = token;
    localStorage.setItem('user', JSON.stringify(response.data.user));
    return user;
  } catch (error) {
    console.log('error from azureRedirect (authService.js): ', error);
  }
};

userController.js

// @desc    Login Azure AD User / Returning JWT Token
// @route   POST api/users/az-redirect
// @access  Public
const azRedirect = asyncHandler(async (req, res) => {
  const errors = {};

  const tokenRequest = {
    code: req.body.code,
    scopes: ['user.read'],
    redirectUri: process.env.REDIRECT_URI,
  };

  try {
    const response = await cca.acquireTokenByCode(tokenRequest);
    console.log(
      'response from the original cca.acquireTokenByCode request',
      response
    );
    const user = await User.findOne({ email: response.account.username });

    if (!user) {
      let nameArray = response.account.name.split(' ');
      const newUser = new User({
        firstName: nameArray[0],
        lastName: nameArray[nameArray.length - 1],
        email: response.account.username,
        azureID: response.uniqueId,
      });

      user = await User.create(newUser);
      console.log('\nNew user created: \n', user);

      const payload = {
        id: user.id,
        email: user.email,
        firstName: user.firstName,
        lastName: user.lastName,
        isTechnician: user.isTechnician,
        isAdmin: user.isAdmin,
      };

      jwt.sign(
        payload,
        process.env.JWT_SECRET,
        { expiresIn: '30d' },
        (err, token) => {
          res.json({
            success: true,
            token: token,
            user: user,
          });
        }
      );
    } else if (!user.isActive) {
      errors.azure = 'User is no longer permitted to access this application';
      return res.status(401).json(errors);
    } else {
      if (!user.azureID) {
        user.azureID = response.uniqueId;
        user.save().catch((err) => console.log(err));
      }
      const payload = {
        id: user.id,
        email: user.email,
        firstName: user.firstName,
        lastName: user.lastName,
        isAdmin: user.isAdmin,
        isSupervisor: user.isSupervisor,
      };

      jwt.sign(
        payload,
        process.env.JWT_SECRET,
        { expiresIn: '30d' },
        (err, token) => {
          res.json({
            success: true,
            token: token,
            user: user,
          });
        }
      );
    }
  } catch (error) {
    console.log(error);
    res.status(500).send(error);
  }
});

Any ideas/help would be greatly appreciated!

EDIT:

Turning off strict mode has helped solve the issue, but that isn't a good long-term solution. I'm looking for a solution that either ensures that the dispatch(azureRedirect(code)); will only run once, or if there is a better practice here I don't know of. Thanks!

  • Does this answer your question? [Why useEffect running twice and how to handle it well in React?](https://stackoverflow.com/questions/72238175/why-useeffect-running-twice-and-how-to-handle-it-well-in-react) – Youssouf Oumar Jan 01 '23 at 09:57
  • That post definitely had some useful information on it, but I'm still left with the fact that the dispatch(azureRedirect(code)); can ONLY be ran once with the same code. If it does it twice, it errors because it is used for authentication purposes. Indeed, turning off strict mode 'fixes' the issue (because it only runs once), but how do I write the useEffect() so that it doesn't run the dispatch more than once? – Shea Erickson Jan 01 '23 at 15:13

0 Answers0