Leveraging the JWT protected AWS Apigateway / Lambda in React

In my last series of posts (one, two, three, and four) I showed how to set up an AWS Lambda and protect access to it with a JWT authorizer using Keycloak.  

The previous code and posts were aimed towards a server to server model.  They used users that were defined within the Keycloak server coupled with an optional client secret.

This is a great setup when you can share the credentials and the client secret with a trusted consumer of your services.  The caller can use something like AWS Systems Manager Parameter Store or the AWS Key Management Service to store the parameters in a secure way.

 

But what if you want your service to available to a website?  Manually creating each user in Keycloak would be a maintenance headache, depending on the number of users in your system.  Keycloak supports self-registration but then your site becomes a target people who want to try to abuse it. And if you had a service with a client secret the secret becomes worthless if it is distributed with the code for your web page.

So I'm going to demo a simple site that has a secure area.  It will leverage Google SSO so that, in my example, people who work with me with the same Google business account can all access the "private" content.  My UI is based an open source admin dashboard - mantis-free-react-admin-template - that I've used in a few other projects.  It's a ReactJS / MUI based template and is pretty straight forward.  For this demo, I'm intentionally trying to not modify it too much.

Let me preface by saying that while I can develop a ReactJS UI I'm not an expert.  I'll admit that, to me, there is so much noise in the JavaScript front end world that I have to start with something pretty simple.  Indeed, the commercial version has 3 versions, plain JavaScript, JSX and TypeScript.  The open source version uses JSX so that's what you'll see here but really, how many variations of the same basic concept does one environment need?  But because the open source version is just the UI it's far simpler to get started on.

 

Finding an Oauth2/OIDC React Library

The first thing I need is a React/JavaScript library that can handle the interaction with Keycloak and OIDC in general.  While there are Keycloak specific libraries, the whole point of using a standard is that I should be able to use anything.  Again, in the JavaScript world if there is one library to do something there are likely 100 and OIDC is no different.  But I chose the AxaFrance/oidc-client library and that includes a React wrapper with the code.  It's pretty simple to use and is updated very frequently.  The documentation is so-so but it's easy enough to get going.

My Github repository has the complete solution but I'll touch on the primary points here.

The only two new dependencies I had to add were:

"@axa-fr/oidc-client": "^7.22.32",
"@axa-fr/react-oidc": "^7.22.32",

Of course, you'll need to do a yarn install after adding those.  Additionally, I added a postinstall action in the script section:

"postinstall": "node ./node_modules/@axa-fr/oidc-client/bin/copy-service-worker-files.mjs public

as suggested by the oidc-client documentation.  One thing that took me a bit is that you'll need to create the public directory - it doesn't come with the demo UI.

 

Changes to the original front end template code

Now for the interesting stuff.  In App.jsx you need to configure the OIDC handler.  Mine contains the configuration:

const configuration = {
  client_id: 'dadjokeapi',
  redirect_uri: window.location.origin + '/authentication/callback',
  silent_redirect_uri: window.location.origin + '/authentication/silent-callback',
  scope: 'openid profile offline_access', // offline_access scope allow your client to retrieve the refresh_token
  authority: 'https://auth.hotjoe.com/realms/hotjoe',
  service_worker_relative_url: '/OidcServiceWorker.js',
  service_worker_only: true,
  token_renew_mode: 'access_token_or_id_token_invalid'
};

This configuration doesn't contain anything secret or hidden.  Obviously this is what you want on a public website as, even with the compilation/obfuscation of the code in production someone can still figure out what's going on.  And if you had any secrets there you'd get hacked.

The second part of App.jsx is wrapping the entire application in the OidcProvider container:

export default function App() {
  return (
    <OidcProvider configuration={configuration}>
      <ThemeCustomization>
        <ScrollTop>
          <RouterProvider router={router} />
        </ScrollTop>
      </ThemeCustomization>
    </OidcProvider>
  );

Congrats - you're on your way to securing this application.  Now, we get to actually see something.  I chose to add a widget to the dashboard main page.  By default the top part looks like:

  

I'm choosing to put my dad joke api (of course) as a "protected" resource.  The ultimate look will be:

 

The dad joke is just a card in between some other cards on a grid.

Notice though two additional things on this screen.  My avatar and name are filled in.  These came from Google, through my Keycloak instance, and are displayed on the UI screen.

The changes to handle the putting the dad joke in is just a little addition to src/pages/dashboard/index.jsx to add the code for the dad joke:

{isAuthenticated && (
  <Grid item xs={12} sx={{ mb: -2.25 }}>
    <tDadJoke />
  </Grid>
)}

All this does is check if the logged in user is authenticated and, if they are, the grid item is added which contains the DadJoke component.

It's super important to understand that, while this prevents access to the dad joke API, it is really just a user experience enhancement, not actual security.  That's why we have the JWT verifier on the API Gateway.  It boils down to "you can't trust the client side", especially in a web environment.  All browsers come with tools to show what data is being sent and received and, if your security checks were only on the browser side, it would be pretty easy to still access the API through tools like Postman or writing your own code.

The DadJoke component is pretty boring but I will show you one interesting part of it (again, all the source code is available at the link above - make sure you use the "stdunbar" one).  The advantage of using the AXA France code is that it automatically adds the authentication token during a fetch()call.  So I just call what I need:

const fetchDadJoke = async () => {
  try {
    const response = await fetch('https://<api gateway>.amazonaws.com/default/get-joke', {
      method: 'GET',
      cache: 'no-cache'
    });
    if (response.ok) {
      const body = await response.json();
      setJoke(body.joke);
} else {
console.log('got back a response of ' + response.status + ' from the service');
  } catch (err) {
console.log('error getting dad joke - ' + err);
setJoke('error getting dad joke');
}
}; 

and not worry about the Authorization header.  I also don't have to directly access the JWT.  The commercial versions break this a bit with Next.js - it wants to be the hook for fetch() but there are ways to work around it.

 This brings back the body of the joke that is then displayed in the grid.

Modification to the API Gateway configuration

The were no changes needed to the AWS Lambda that backs this service - it's the same as the previous set of articles.  On the API Gateway side I added a route for this integration.  The biggest reason is that, because I'm using Google SSO with Keycloak I can no longer use curl to get the token.  Since I want to be able to have both a "server" hit the API and the ReactJS UI I separated them out.  Additionally, on API Gateway I changed it from accepting any HTTP verb to just accept GET.  The React code was sending an OPTIONS call and I didn't need that.
 
That's it on the API Gateway side and that's the point.  I'm using the same Keycloak authorization and the same AWS Lambda.   The token is validated the same way via Postman or via the browser.




 

 


 

 

 

Photo by Pavel Okrema on Unsplash

Comments

Popular posts from this blog

Starting SSO with Keycloak

Generating JWT's using the Auth0 library