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

VP of Consulting

I’m a Software Engineer with 10+ years experience in small and large companies prototyping, designing, building, and maintaining both in-house and customer facing applications.  I prides myself on my strong work ethic and ability to listen first, guide second, and deliver high quality, thoughtful, and lasting software for clients. My foundational development began in Java and has migrated to Javascript where I spend most of my time building Next Generation web applications and services.

Outside of work, I like to get my hands dirty working on vehicles and gardening, building anything my wife tells me to, and being a father to my two boys.

One thought on “Tracking Original URL Through Authentication

  1. Anonymous says:

    Excellent Post thank you.

Leave a Reply

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

Related Blog Posts
Snowflake CI/CD using Jenkins and Schemachange
CI/CD and Management of Data Warehouses can be a serious challenge. In this blog you will learn how to setup CI/CD for Snowflake using Schemachange, Github, and Jenkins. For access to the code check out […]
How to get your pull requests approved more quickly
TL;DR The fewer reviews necessary, the quicker your PR gets approved. Code reviews serve an essential function on any software codebase. Done right, they help ensure correctness, reliability, and maintainability of code. On many teams, […]
Kafka & Kubernetes: Scaling Consumers
Kafka and Kubernetes (K8s) are a great match. Kafka has knobs to optimize throughput and Kubernetes scales to multiply that throughput. On the consumer side, there are a few ways to improve scalability. Resource & […]
AWS RDS MYSQL Playground
Do you need a reliable database platform to hammer out some new application ideas? Or, maybe you’d like to learn MYSQL in a disposable environment? This Hashicorp Terraform MYSQL RDS Module will build all the […]