Search code examples
reactjsaws-api-gatewayamazon-cognitoreact-admin

ra-data-json-server in conjunction with ra-auth-cognito


Before I start, I'm very new to React but not so to AWS which has resulted in me picking off a project above my ability set it appears.

I'm building a very simple CRUD website which has a list of posts and a list of users who made those posts. I started off following this tutorial here https://marmelab.com/react-admin/Tutorial.html once at the end I started extending it out into an AWS deployment.

Ostensibly an S3 bucket with the React site, and API to provide the data interfaces all behind a cloudfront distribution.

I don't want every man and his dog to have access so I want to implement a Cognito authentication provider infront of my service, like this:

enter image description here

My issue is that the tutorial walks me through setting up a dataProvider using the ra-data-json-server module. The ra-auth-cognito module provides what I need for logging in and works correctly, valid Cognito users can log-in to the dashboard, though they cannot access any data via the API calls.

My App.tsx file contains

...
import { CognitoAuthProvider, Login } from 'ra-auth-cognito';
import { CognitoUserPool } from 'amazon-cognito-identity-js';

const userPool = new CognitoUserPool({
  UserPoolId: "us-east-1_MyPoOlId",
  ClientId: "MyClIeNtId",
});

const authProvider = CognitoAuthProvider(userPool);

export const App = () => (
  <Admin authProvider={authProvider} dataProvider={dataProvider} dashboard={Dashboard} loginPage={Login} >
    <Resource name="posts" list.......

My dataProvider.ts looks like

import jsonServerProvider from "ra-data-json-server";

// I know i need a httpClient function here (I think)

export const dataProvider = (() => {
    return jsonServerProvider("https://my-url/api/");
})();

I've been working through the documentation https://marmelab.com/react-admin/Authentication.html which I understand lets me pass an httpClient in to the jsonServerProvider as a second parameter, the provided code block results in the error "The dataProvider threw an error. It should return a rejected Promise instead"

I've also tried following what this post advised incorporating ra-cognito in react-admin with ra-data-json-server promise problem though this user is using Amplify which I am not however it doesn't work for me regardless

The issue is, I can now protect the admin panel, but doing anything which calls the backend API with authentication enabled isn't being sent the authentication, the API is expecting a Bearer Token which correctly replies with the data expected when I hit it through postman with a valid Bearer token.

How do I link the calls to the API in such a was that the logged in token is passed thro to the api calls made by the ra-data-json-server


Solution

  • Kudos for providing a detailed question to begin with :)

    I believe you are missing two things to get it to work.

    1. Obtain the token for the currently logged user

    ra-auth-cognito almost does it internally, but unfortunately it does not document how to obtain the JWT token for the currently logged user. Here's one way to get it (inspired by authProvider.ts):

            const getJwtToken = async () => {
                return new Promise((resolve, reject) => {
                    const user = userPool.getCurrentUser();
    
                    if (!user) {
                        return reject();
                    }
    
                    user.getSession((err, session: CognitoUserSession) => {
                        if (err) {
                            return reject(err);
                        }
                        if (!session.isValid()) {
                            return reject();
                        }
                        const token = session.getIdToken().getJwtToken();
                        return resolve(token);
                    });
                });
            }
    

    2. Use the token to query the API

    I believe using a custom httpClient is indeed the best approach here. You almost had it working :).

    Here is an example adapted from the ra-data-json-server documentation:

    import { fetchUtils, Admin, Resource } from 'react-admin';
    import jsonServerProvider from 'ra-data-json-server';
    
    const getJwtToken = async () => { /* ... */ };
    const httpClient = async (url: string, options: any = {}) => {
        options.user = {
            authenticated: true,
            token: await getJwtToken(),
        };
        return fetchUtils.fetchJson(url, options);
    };
    const dataProvider = jsonServerProvider('https://jsonplaceholder.typicode.com', httpClient);
    
    render(
        <Admin dataProvider={dataProvider} title="Example Admin">
           ...
        </Admin>,
        document.getElementById('root')
    );
    

    This should be enough to include the Bearer Token to each API call.

    Hope this helps! :)