Quantcast
Channel: Blog | Object Partners
Viewing all articles
Browse latest Browse all 93

Tracking Original URL Through Authentication

$
0
0

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!


Viewing all articles
Browse latest Browse all 93

Trending Articles