Build a GraphQL client application to consume protected GraphQL API resources

image

This article is part 3 of our GraphQL application protection series. In this article, we will build a GraphQL client that is capable of invoking GraphQL API calls, obtain authorized access tokens from a Cloudentity authorization server and send the authorization token to underlying GraphQL services.

In the previous article, we built the GraphQL server with Node.js, expressgraphql and loki as a built-in database for this demonstration. The goal was to build a GraphQL server and have its endpoint protected.

We then deployed this application to a native Kubernetes cluster using kind and enforced centralized and decoupled authorization without changing any business logic or code. See the previous article for all the details.

As a recap, take a peek at the diagram below as a reminder of what we built in the previous article vs. the new consumer application that we will be building in this article, and how all these pieces will eventually work together.

Build a single page React app with Nodejs

We will be building a tweet-ui React based application with attached components. Full source code for this demo application can be found here

SKIP/JUMP LEVEL

In case you are not interested in building the application from scratch, you can skip some of the steps below and instead checkout/clone the attached github repo

git clone git@github.com:cloudentity/ce-samples-graphql-demo.git cd tweet-ui-graphql-react


<code>and then jump to [Register the application](#register-the-application-in-cloudentity-authorization-platform)</code>

Pre-requsisites

We will be using react for the application development.


- [npm](https://docs.npmjs.com/getting-started) - Recommended v8.3.0 +

Initialize React app


npx create-react-app tweet-ui-graphql-react

And install required packages


npm install --save react-router-dom
npm install --save @apollo/client graphql@15.3.0
npm install --save @mui/material @emotion/react @emotion/styled @mui/icons-material
npm install --save @cloudentity/auth
npm install --save jwt-decode

Define React components

Routing

For this react application, let's define the routing for various resources. As per this sample application, we will have

  • page that does not require authorization (/ page)
  • element that allows user to authorize (auth function within / page)
  • page that requires authorization (/usertweet) ** if there is no user authorization, the user will be redirected to / page

So in our application routing, let's define the router and the auth redirects. We will use the Cloudentity Auth sdk to handle OAuth authorization (code => token exchange), redirects and setting of the accessToken. If an accessToken is not available in the local storage, the user will be redirected to the page where they can see the authorize button and contents that do not require authorized tokens.

In index.js, located in the src directory of the app, we'll add:


function App() {

  //const navigate = useNavigate();
  const cloudentity = new CloudentityAuth(authConfig);
  const [authenticated] = useAuth(cloudentity);
  const accessTokenRaw = localStorage.getItem(authConfig.accessTokenName);
  const auth = authenticated &amp;&amp; accessTokenRaw;

  return (
    &lt;Router&gt;
      &lt;div&gt;
        &lt;Routes&gt;
          &lt;Route path="/usertweet"  element={!auth ?&lt;HomePageContent auth={auth} /&gt; : &lt;Navigate to='/usertweet' /&gt; }&gt;&lt;/Route&gt;
          &lt;/Route&gt;
          &lt;Route path="/"  element={!auth ? &lt;HomePageContent auth={auth} /&gt; : &lt;Navigate to='/usertweet' /&gt; }&gt;
          &lt;/Route&gt;
        &lt;/Routes&gt;
      &lt;/div&gt;
    &lt;/Router&gt;
  );

}

So as you can see the public page is at HomePageContent that is served by homepage.js. Let's dive into that snippet for some explanation.

Homepage

homepage.js contains mainly two main elements of interest other than navigation and style elements

  • authorize button => Calls to the Cloudentity authorization platform to authorize the user
  • Latest tweets element => Unprotected GraphQL API call

Configure GraphQL API client

We need to configure the GraphQL API client to send in an authorization token (if available) retrieved from the browser's local storage. If not, it will make GraphQL API calls without this authorization token . More details and explanation about usage of apollo client can be found here


import {
    ApolloClient,
    InMemoryCache,
    ApolloProvider,
    createHttpLink,
    from
  } from "@apollo/client";
  import { setContext } from '@apollo/client/link/context';
  import { onError } from "@apollo/client/link/error";

const httpLink = createHttpLink(
  {
    uri: 'graphql',
    credentials: 'same-origin'
  }
);

const authLink = setContext( (_, {headers}) =&gt; {
  // get the authentication token from local storage if it exists

 const accessTokenRaw = localStorage.getItem(authConfig.accessTokenName);
  //return the headers to the context so httpLink can read them

  return {
    headers: {
      ...headers,
      authorization: accessTokenRaw ? `Bearer ${accessTokenRaw}`: "",
    }
  }
});

//https://www.apollographql.com/docs/react/api/link/apollo-link-error/
const errorLink = onError(({ graphQLErrors, networkError }) =&gt; {
  if (graphQLErrors)
    graphQLErrors.forEach(({ message, locations, path }) =&gt;
      console.log(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
      ),
    );

  if (networkError) console.log(`[Network error]: ${networkError}`);
});


const client = new ApolloClient (
  {
    link: from([authLink, errorLink, httpLink]),
    cache: new InMemoryCache()
  }
);

Invoking GraphQL APIs

For invoking GraphQL APIs, we will utilize the Apollo client configured above


import {
  useQuery,
  gql
} from "@apollo/client";


const FETCH_LATEST_TWEETS = gql `
query {
    getLatestTweets {
        id,
        content,
        author
    }
  }
`;

export function GetLatestTweets() {
  const {loading, error, data} = useQuery(FETCH_LATEST_TWEETS);

  const  [latestTweetFetchStatus, setFetchLatestTweetsStatus] = useState(false);

  if (loading) return &lt;p&gt;Loading..&lt;/p&gt;;
  if (error &amp;&amp; isError403(error)) return (
    &lt;div&gt;
        &lt;h2&gt; Latest Tweets&lt;/h2&gt;
        &lt;p&gt; Our systems have detected that this request is NOT authorized to see the tweets... &lt;/p&gt;
        &lt;p&gt; Reason for unauthorized: &lt;/p&gt;

    &lt;/div&gt;
);
  if (error) return (
      &lt;div&gt;
          &lt;h2&gt; Latest Tweets&lt;/h2&gt;
          &lt;p&gt; System connection issues ... &lt;/p&gt;

      &lt;/div&gt;
  );

  if(!latestTweetFetchStatus &amp;&amp; data != null &amp;&amp; data.getLatestTweets != null) {
    setFetchLatestTweetsStatus(true);
  }

  if(latestTweetFetchStatus) {
    return (
      &lt;div&gt;
        &lt;h2&gt; Latest Tweets&lt;/h2&gt;

        {
          Object.keys(data.getLatestTweets).map((k,i) =&gt; (
              &lt;div&gt;
                  &lt;div&gt; {data.getLatestTweets[i].owner} @ {data.getLatestTweets[i].id}&lt;/div&gt;
                  &lt;div&gt; tweeted - &lt;/div&gt;
                &lt;div&gt;Hi {i} {k} {data.getLatestTweets[i].content} &lt;/div&gt;

            &lt;/div&gt;
          ))
        }


      &lt;/div&gt;
    );
  }
  return &lt;TweetsNotAvailable /&gt;;

}

Now we have a homepage that prompts the user to authorize, resulting in an authorization token to call more resource server APIs, as well as making a call to a publicly exposed GraphQL API resource endpoint.

Getting an authorization token from Cloudentity

We will use the Cloudentity OAuth JS SDK to perform an OAuth handshake with the Cloudentity authorization platform and fetch an authorization token.

authButton.js


import CloudentityAuth from '@cloudentity/auth';
import authConfig from './authConfig.js';

export const AuthButton = ({auth}) =&gt; {

  const classes = useStyles();

  const handleAuth = () =&gt; {
    const scopes = 'all';
    console.log(authConfig);
    const cloudentity = new CloudentityAuth({...authConfig, ...scopes});
    cloudentity.authorize();
  };

  const buttonOnClick = () =&gt; {
    handleAuth();
  }

  return (
    &lt;div className={classes.root} &gt;
      &lt;Stack className={classes.mainSection} direction="column" justifyContent="center" alignItems="center"&gt;
        &lt;p&gt;If you want to see more than this, we need to identity and authorize you further.
        &lt;h3&gt;Let's get you in there!&lt;/h3&gt;
        &lt;/p&gt;
        &lt;Tooltip title="This will intiate OAuth authorization request with Cloudentity ACP" placement="top"&gt;
          &lt;Button className="AuthenticateButton" variant="contained" onClick={() =&gt; buttonOnClick()}&gt;Authorize&lt;/Button&gt;
        &lt;/Tooltip&gt;
      &lt;/Stack&gt;
    &lt;/div&gt;
  );

}
  • Proxy to eliminate CORS error

By default you will run into CORS error as the GraphQL APIs are served on a different host/port. To eliminate this, we will use the react dev proxy to proxy all requests to the GraphQL API server.

package.json


{
  "proxy": "http://localhost:5001",
}

Register the application in Cloudentity authorization platform

To consume any resources protected by the Cloudentity authorization server, the consuming applications must first register themselves in the Cloudentity authorization platform. As a quick reminder, we will be using open standard based OAuth flows for all these integrations.

So get into the Cloudentity portal and register a single page client application.Follow this Cloudentity article to register a client application in the Cloudentity authorization platform

Now that we have a registered client application, we will feed that into the configuration settings for the application we have been building.

In authConfig.ts,

This file contains the configuration required to handshake with the Cloudentity authorization platform to obtain an access token to consume resources on behalf of an end user. The underlying Cloudentity SDK uses the authorization code grant with PKCE flow to get the access token. Read more about the OAuth PKCE flow.


const authConfig = {
    domain: 'rtest.authz.cloudentity.io', // e.g. 'example.demo.cloudentity.com.' Recommended; always generates URLs with 'https' protocol.
     // baseUrl: optional alternative to 'domain.' Protocol required, e.g. 'https://example.demo.cloudentity.com.'
     // In situations where protocol may dynamically resolve to 'http' rather than 'https' (for example in dev mode), use 'baseUrl' rather than 'domain'.
     tenantId: 'rtest',
     authorizationServerId: 'ce-dev-playground-integrations',
     clientId: 'c7e6u0eer3qh0m4pggig',
     redirectUri: 'http://localhost:3000/',
     silentAuthRedirectUri: 'window.location.href' + '/silent', // optional setting to redirect to a different endpoint following successful silent auth flow
     userInfoUri: 'https://rtest.authz.cloudentity.io/rtest/ce-dev-playground-integrations/userinfo', // optional, for fetching user info via API
     scopes: ['profile', 'email', 'openid'], // 'revoke_tokens' scope must be present for 'logout' action to revoke token! Without it, token will only be deleted from browser's local storage.
     letClientSetAccessToken: true,
     accessTokenName: 'ins_demo_access_token', // optional; defaults to '{tenantId}_{authorizationServerId}_access_token'
     idTokenName: 'ins_demo_id_token', // optional; defaults to '{tenantId}_{authorizationServerId}_id_token'

 };

export default authConfig;

Now that we have everything configured and ready to go, let's run the application and test it out.

Run the application


<code>npm start
</code>

http://localhost:3001

Modify authorization policies

Modify authorization policies attached to the GraphQL constructs to see how client data access can be dynamically controlled.

Exercise for the readers

Now you can play around with various policies protecting the GraphQL API resources and see how the application handles various pieces and responds. We hope this was an educational journey to clearly understand how the Cloudentity authorization solution can be used easily to solve the complex authorization requirements within your organization. Feel free to contribute any modifications to the demo applications and concepts back to repo for new readers to explore and understand. Happy learning!

Conclusion

This wraps up the tutorial for a sample GraphQL client server application protected using the Cloudentity Authorization platform. In this series, you will have accomplished the following things:

  • Develop a simple GraphQL service
  • Protect the GraphQL service API resources with the Cloudentity Authorization Platform
  • Develop a simple GraphQL client application
  • Fetch authorization tokens from the Cloudentity Authorization Platform and pass from the client app to the protected server resource
  • Dynamically change the protected server resource policy in ACP to control client app GraphQL API data access

Check it out for yourself using our FREE tenant. You'll find helpful product documentation here or contact us and we'd be happy to answer any questions and give you a demo.