5

My web application uses React in the frontend and Flask in the backend.

I would like to add AWS Cognito for user management. As per the documentation, this requires nothing more than wrapping an export statement in the frontend, i.e. changing a line

export default App;

in an App.js file (defining the main application React component) to

export default withAuthenticator(App, true);

plus adding the required import statements.

This trivial change gets me all the user-focussed functionality required by any reasonable web application used in production, e.g.

  • user signup
  • email verification
  • login / logout
  • forgotten password
  • etc. pp.

including

  • themable UI
  • email sending services
  • federated logins (e.g. Google, Facebook, etc.)

The problem: I don't see how to integrate this with the backend.

The approach: (as I think it should be)

  1. an existing user logs into the web application using the UI elements provided by Cognito and displayed by the frontend
  2. upon successful login, Cognito makes user data available by means of currentAuthenticatedUser(); among others, a JSON Web Token (JWT)
  3. whenever the frontend sends requests to the backend, it passes that JWT as a request header
  4. the backend uses that JWT to determine if the user has properly logged in, which user the request is made for and if that user has sufficient permissions

I have implemented the usual Flask /login or /logout request handlers and a login_required decorator and also passing the JWT and decoding claims works fine; however, I don't see where the frontend should notify the backend about a user logging in.

I tried doing this with a Cognito Post Authentication Lambda Trigger, but this has a number of massive drawbacks:

For starters, the AWS lambda function triggered by the user login must be able to access the backend, so it must be publicly available. For security reasons, I would actually prefer the backend to be visible / accessible to / by the frontend only. Also, during development, I'm running both frontend and backend in Docker containers on my MacBook; this approach forces me to run at least the backend in the cloud.

Biggest problem though is that it doesn't work: From the backend perspective, requests from the frontend and Cognito take place in separate sessions, so even after the user logs in using Cognito (via the frontend) and Cognito notifies the backend about this login (via a lambda trigger), any subsequent requests from the frontend to the backend - from the backend perspective - are not associated with the (authenticated) session between backend and Cognito and are therefore unauthenticated.

Well, apart from the massive drawbacks and the fact that it doesn't work, the lambda trigger approach also feels total overkill.

Update: I don't know why I didn't see this earlier, but there is a section in the Amplify docs that exactly explains how to customize withAuthenticator. However, I don't want to override render() as in the example, but signIn() (or so I believe). I have successfully implemented the code to send a request to http://mybackend.mydomain.com/login with appropriate headers to log into the backend, but I can't seem to figure out where / how to call that code.

I have copied the code from the Amplify docs linked above, looked at the SignIn JSX source code and the ("rendered" / "built" ?) version in my local source code folder at node_modules/aws-amplify-react/dist/Auth/SignIn.js and tried this:

class MySignIn extends SignIn {
  async signIn() {
    console.log('MySignIn.signIn() start');
    super.signIn()
        .then(data => console.log(data))
        .catch(err => console.log(err));
    console.log('MySignIn.signIn() end');
  }
}

but all this gets me is an empty page and no messages in the log whatsoever.

New question: How do I implement MySignIn so that it does all the SignIn work, but after successful login, also does the additional work I need ?

Thank you very much for your consideration! :-)

ssc
  • 9,528
  • 10
  • 64
  • 94
  • Since every request to the backend also sends the JWT token as a header, can't you wrap your relevant routes using a decorator which checks the validity of the JWT token (since it also containes the user id)? You don't really need to store the state of the logged in user on the backend, do you? Or am I overlooking something? – Joost Dec 17 '18 at 12:19
  • That is a valid point and is what I thought of at first. However, Flask has this nice and clean concept of logging in once to start a session and then working with the `session` and `g` objects in subsequent calls which I'd ideally like to stick to. In order to make progress in this project, I might have to pursue that approach and hope for the better solution to become available down the road somehow... – ssc Dec 18 '18 at 13:43

1 Answers1

2

I checked the code and the signIn function seems hard to extend, as it doesn't return anything so there's no point in using super and then because the resolving function isn't going to receive any data. That's why your logs are empty. To make use of it you'd need to essentially redefine it from scratch.

What you can do is reimplementing the changeState method itself, as in that case you can just call the parent method and then do whatever you want afterwards.

But there's a problem with just extending any of the Auth components, as either the render or the showComponent function must be reimplemented if you want to subclass them because the parent ones --as the parent is hidden now-- won't return anything. (This is probably something they should clear up in the docs, although in their example the render function is being explicitly redefined).

An easy way to call the showComponent method of the parent class without it returning a blank page is to remove the parent class from the list of hidden components that every component receives.

So that, together with the extension of the changeState method would look something like this:

class MySignIn extends SignIn {
  showComponent(theme) {
    const signIn = this.props.hide.indexOf(SignIn)
    this.props.hide.splice(signIn, 1) // make sure SignIn is not hidden
    return super.showComponent(theme) // call showComponent from SignIn
  }
  changeState(state, data) {
    super.changeState(state, data)
    if (state !== 'signedIn') return
    console.log(data)
    // make the request to the backend
  }
}

Now you can pass MySignIn exactly like it shows in the docs.

Keep in mind that changing the props is supposed to be an anti-pattern, but I haven't found another option that doesn't involved redefining the showComponent (or worse, render) method completely, since in the parent that's what it looks for.

Probably an idea for a PR would be making those methods (like signIn) more easily extendable.

Old Answer:

This cannot be implemented because the onStateChange property is overwritten when the component is actually instantiated.

An alternative option is passing a function to onStateChange as a prop to your signin component and check for the signedIn state, like this:

function onStateChange(state, data) {
  if (state !== 'signedIn') return
  console.log(data)
  // do the request to the backend here
}

And in your customization of withAuthenticator

export default withAuthenticator(App, false, [
  <SignIn onStateChange={ onStateChange } />,
  // the rest
Luis Orduz
  • 2,887
  • 1
  • 13
  • 19
  • Thanks for your response! :-) I tried what you advised; I think the syntax must be `onStateChange={ onStateChange }`, otherwise the code fails to compile with `Parsing error: JSX value should be either an expression or a quoted JSX text`, but even after fixing that, the `onStateChange` function does not seem to get called (added more `console.log(...)` statements before the `if` to test). Also tried `MySignIn` with a `onStateChange()` method, but that again gets me an empty page. I feel this could be the way forward, but something just doesn't work yet and I don't see how to debug this... – ssc Dec 18 '18 at 14:11
  • Yeah, I messed the syntax there, fixed. Did you try `MySignIn` and passing `onStateChange` as a prop instead of creating a method? As per [this line](https://github.com/aws-amplify/amplify-js/blob/dda334b121966b2348b40e4ffdaa11a918de8a29/packages/aws-amplify-react/src/Auth/AuthPiece.jsx#L58), it only looks at the prop. – Luis Orduz Dec 18 '18 at 15:21
  • Any idea why `onStateChange` does not get called ? I'd like to keep trying to get that to work as my attempts regarding `MySignIn` were not very promising so far; all I get is an empty page and no idea of where to start looking for the problem. – ssc Dec 18 '18 at 15:44
  • I'll have to set a local environment replicating yours, do you have a minimal working replica that I could use to set up locally faster? – Luis Orduz Dec 18 '18 at 15:47
  • I'll create one and get back to you; will probably take me an hour or two. – ssc Dec 18 '18 at 15:52
  • Awesome, I'll check it out as soon as you post the link. – Luis Orduz Dec 18 '18 at 15:56
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/185441/discussion-between-ssc-and-luis-orduz). – ssc Dec 18 '18 at 18:22