Tracking Original URL Through Authentication

If you read my other post about refreshing AWS tokens, then you probably have a use case for keeping track of the original requested resource while the user goes through authentication so you can route the user to that resource once they’re authenticated. A simple idea of going directly to the link can get a little confusing once you add authentication in the mix. Even more confusing when the authentication flow reloads your web app and you can no longer rely on in-memory state. In this post, I am going to continue from the prior post by showing you how to can keep track of the original URL by utilizing AWS Amplify’s customOAuthState.

Backing up a little bit to try and recap the last post, we’re using the AWS Amplify javascript library that integrates with our web app. In the Amplify configuration, we’re using a custom OAuth provider, Microsoft Azure in this case. Lastly, we’re using React for our web framework.

Let’s start with a web app’s entry point, a sign in component, and a home component. Nothing special here. Just showing how simple routing can be without authentication complexities.

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';

ReactDOM.render(
  <Router>
    <App />
  </Router>
);
App.js
import React from 'react';
import { Route } from 'react-router-dom';
import { Home, SignIn } from './components';

function App() {
  
  return (
    <div>
      <Route
        path="/home"
        render={() => <Home />}
      />
      <Route
        path="/signIn"
        render={() => <SignIn />}
      />
    </div>
  );
}

export default App;
SignIn.js
import React from 'react';

function SignIn() {

  function signIn() {
    alert('sign in not implemented');
  }

  return (
    <div>
      <Button onClick={() => signIn()}>
        Sign In
      </Button>
    </div>
  );
}

export default SignIn;
Home.js
import React from 'react';

function Home() {
  return (
    <div>You are home</div>
  );
}

export default Home;

Now let’s begin to add the authentication pieces. First, we’ll focus on SignIn.js and add Amplify’s sign in function for an OAuth provider that’s invoked when the user clicks our button.

SignInWithAuthentication.js
import React from 'react';
import Auth from 'aws-amplify/packages/auth';

function SignIn() {

  function signIn() {
    Auth.federatedSignIn({ provider: 'Azure' });
  }

  return (
    <div>
      <Button onClick={() => signIn()}>
        Sign In
      </Button>
    </div>
  );
}

export default SignIn;

Now we’ll move to App.js and add some authentication state with the help of Amplify’s very convenient Hub module. This allows us to listen for auth events and let us react to update our state! Now that we can listen to the auth events from Amplify, we can make better routing choices as you can from the /home route. One item you may not have picked up was that because the Hub listener is inside a useEffect with no dependencies, our listener will be instantiated once on load of App.js. Now, we can listen for auth events through an app reload and recreate our state!

AppWithHub.js
import React, { useEffect, useState } from 'react';
import { Redirect, Route } from 'react-router-dom';
import { Hub } from '@aws-amplify/core';

// Greatly simplified app entry point that shows the structure of the routing
function App() {
  const [isAuthenticated, setAuthenticated] = useState(false);

  useEffect(() => {
    Hub.listen('auth', async ({ payload: { event, data } }) => {
      switch (event) {
        case 'signIn':
          // `data` contains user auth information and can be used for your app.
          // We will just assume happy path and set authentication true.
          setAuthenticated(true);
          break;
      }
    });
  }, []);

  return (
    <div>
      <Route
        path="/home"
        render={ props => {
          return isAuthenticated ? (
            <Home />
          ) : (
            <Redirect to="/signIn" />
          );
        }}
      />
    </div>
  );
}

export default App;

Hopefully you’re tracking with me this far because now we can get to the part where we can ask the question:

“How can we preserve the user’s original url if they are unauthenticated”?

Remember, this OAuth flow has a hard redirect in it causing our app to reload.

Amplify has another very convenient feature that we can use that lets us put custom state into our authentication call and listen for that custom state via a special Hub event. Keeping our focus on App.js, we can imagine the user attempting to access /home, but they aren’t authenticated so we redirect them to /signIn. Doing this as is, we lose track of the original url by doing the redirect so we need to add that data to the tag.

AppWithHubAndLocation.js
import React, { useEffect, useState } from 'react';
import { Redirect, Route } from 'react-router-dom';
import { Hub } from '@aws-amplify/core';

// Greatly simplified app entry point that shows the structure of the routing
function App() {
  const [isAuthenticated, setAuthenticated] = useState(false);

  useEffect(() => {
    Hub.listen('auth', async ({ payload: { event, data } }) => {
      switch (event) {
        case 'signIn':
          // `data` contains user auth information and can be used for your app.
          // We will just assume happy path and set authentication true.
          setAuthenticated(true);
          break;
      }
    });
  }, []);

  return (
    <div>
      <Route
        path="/home"
        render={ props => {
          return isAuthenticated ? (
            <Home />
          ) : (
            <Redirect
              to={{
                pathname: '/signIn',
                state: {
                  from: props.location
                }
              }}
            />
          );
        }}
      />
    </div>
  );
}

export default App;

Now we move attention back to SignIn.js which now is being provided some data. Let’s take advantage of that incoming state by hooking up to Amplify’s customState. By sending a location into the customState parameter of Amplify, we are now able to persist data through an app reload.

SignInWIthAuthenticationAndLocation.js
import React from 'react';
import Auth from 'aws-amplify/packages/auth';

function SignIn({ location }) {
  // Location is provided in React via react-router-dom and Redirect component.
  const from = location ? `${location.state?.from?.pathname}` : null;

  function signIn(from) {
    Auth.federatedSignIn({ provider: 'Azure', customState: from });
  }

  return (
    <div>
      <Button onClick={() => signIn(from)}>
        Sign In
      </Button>
    </div>
  );
}

export default SignIn;

Now that we sent out the authentication request, we need to handle its response so we can pull the location back into our app. To accomplish this, open up App.js again and add a customOAuthState event in the Hub listener. In addition to repopulating our state, we can now decide where to go once we know the user is authenticated.

AppWithCustomOAuthEvent.js
import React, { useEffect, useState } from 'react';
import { Redirect, Route } from 'react-router-dom';
import { Hub } from '@aws-amplify/core';

// Greatly simplified app entry point that shows the structure of the routing
function App() {
  const [isAuthenticated, setAuthenticated] = useState(false);
  const [from, setFrom] = useState(null);

  useEffect(() => {
    Hub.listen('auth', async ({ payload: { event, data } }) => {
      switch (event) {
        case 'signIn':
          // `data` contains user auth information and can be used for your app.
          // We will just assume happy path and set authentication true.
          setAuthenticated(true);
          break;
        case 'customOAuthState':
          const originalUrl = decodeURIComponent(data);
          setFrom(originalUrl);
          break;
      }
    });
  }, []);

  return (
    <div>
      <Route
        path="/home"
        render={ props => {
          return isAuthenticated ? (
            from ? <Redirect to={from} /> : <Home />
          ) : (
            <Redirect
              to={{
                pathname: '/signIn',
                state: {
                  from: props.location
                }
              }}
            />
          );
        }}
      />
    </div>
  );
}

export default App;

That’s the end result and the user should now be able to request a protected resource, get authenticated, and get redirected to their original url. All while our app reloads and we recreate state!

Thanks for reading and happy coding!

About the Author

Brian Rue profile.

Brian Rue

Sr. Consultant

Brian is a Software Consultant with 10 years experience in small and large companies prototyping, designing, building, and maintaining both in-house and customer facing applications.  He prides himself on his strong work ethic and ability to listen, guide and build high quality, thoughtful, and lasting software for clients. Brian’s foundational development began in Java and has migrated to Javascript where he spends most of his time building Next Generation web applications.

Outside of work, Brian likes to get his hands dirty working on vehicles, building anything his wife tells him to, and being a father to his two boys.

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Blog Posts
Performance Test Liquibase Update
When doing a liquibase update to a database if you’re having performance issues, it can be hard to find out which updates are causing problems. If you need to measure the time to apply each […]
TICK Stack Monitoring for the Non-Technical
TICK – Telegraf, Influx, Chronograf, and Kapacitor – is a method of monitoring your systems and applications. In this article, I discuss in non-technical terms what the difference is between TICK and Prometheus Grafana A […]
Design Systems, Part 1 • Introduction
Business leaders need a practical guide to plan and execute Design System Initiatives. The aim of this series is to be that guide. This installment introduces terms and definitions as a primer on Design Systems.
ML for Translating Dysarthria Speech (Pre-Part 1)
What is Dysarthria? Per the Mayo Clinic, Dysarthria occurs when the muscles you use for speech are weak or you have difficulty controlling them. Dysarthria often causes slurred or slow speech that can be difficult […]