denvned/isomorphic-relay

Isomorphic JWT authentication

fenos opened this issue ยท 17 comments

fenos commented

Hi @denvned, I'm focusing my project on this package which provide isomorphism to relay.
This is not a question related to the package "Which works great" but more related to the isomorphism world and authentication. I'm writing here because i believe you may suggest something very precious to me and community regarding this subject.

In my current project I want to achieve the following criteria with an isomorphic app:

  • Have my GraphQL end points accepting only JWT authentication and be stateless
  • Have my isomorphic app aware of user authentication when they refresh the page (with security in mind)

To solve this I needed 2 authentications, one for the isomorphic app "session based" and the second for the GraphQL server which is the wanted JWT.

As following I created a diagram that describe this authentication flow:
note: it start from the browser requesting authentication token to graphQL

iso authentication

The biggest downside of this approach "The reason why i'm posting here" is the way how I pass the token to Javascript / React. I'm actually rendering the token in a div Similar way how we preload the data with relay "It render the data as a JSON string in a HTML element"

On DOM loaded i extract the token information from the <div id="auth">{"token":"usertoken"}</div>
and pass it to a AuthStore.js which keep it in memory and pass it to Relay for sub-sequential request.

I implemented this approach and it works just fine, but i'm concerned about security of rendering the token in the div. If you have any better idea to pass this token to Javascript would be great. Or did you ever done a similar authentication with a Isomorphic app?

Thanks for the time to read this

This is almost exactly the architecture I am using. I pass the token as an
encrypted http only cookie in the rendered response from the isomorphic
renderer server.

The problem I have is passing different tokens through to the relay
networking layer when rendering in the server. Currently it doesn't support
different contexts (ie, requests) sharing the same relay instance, so you
get different users sharing the same cache and no way to forward their
tokens to the graphql server, which is bad because it can let sensitive
information leak out.

I think there are some pull requests to Facebook to add contexts so a
server can share relay between users. They are necessary for isomorphic
relay to work.

On Tue, 9 Feb 2016, 06:28 Fabrizio notifications@github.com wrote:

Hi @denvned https://github.com/denvned, I'm focusing my project on this
package which provide isomorphism to relay.
This is not a question related to the package "Which works great" but more
related to the isomorphism world and authentication. I'm writing here
because i believe you may suggest something very precious to me and
community regarding this subject.

In my current project I want to achieve the following criteria with an
isomorphic app:

  • Have my GraphQL end points accepting only JWT authentication and be
    stateless
  • Have my isomorphic app aware of user authentication when they
    refresh the page (with security in mind)

To solve this I needed 2 authentications, one for the isomorphic app
"session based" and the second for the GraphQL server which is the wanted
JWT.

As following I created a diagram that describe this authentication flow:
note: it start from the browser requesting authentication token to
graphQL

[image: iso authentication]
https://cloud.githubusercontent.com/assets/4754064/12893633/1cb7ef92-ce8a-11e5-99f9-d0eabd91170e.jpg

The biggest downside of this approach "The reason why i'm posting here" is
the way how I pass the token to Javascript / React. I'm actually rendering
the token in a div Similar way how we preload the data with relay "It
render the data as a JSON string in a HTML element
"

On DOM loaded i extract the token information from the

{"token":"usertoken"}

and pass it to a AuthStore.js which keep it in memory and pass it to
Relay for sub-sequential request.

I implemented this approach and it works just fine, but i'm concerned
about security of rendering the token in the div.
If you have any better
idea to pass this token to Javascript would be great. Or did you ever done
a similar authentication with a Isomorphic app?

Thanks for the time to read this

โ€”
Reply to this email directly or view it on GitHub
#15.

Dylan Sale

CTO - Enabled
enabled.com.au
08 8272 6658

fenos commented

@DylanSale Thanks a lots for your feedback, I thought that isomorphic-relay would give also the context for differentiate caches, i will find out more about thanks.

About your comment saying:

I pass the token as an
encrypted http only cookie in the rendered response from the isomorphic
renderer server.

How do you access the token from the client side if is a http only cookie i missed that part. I find my self forced to render the token into the DOM

You do not want to access the cookie from the client side generally. You only retrieve it on the server and give it to relay, for instance as a root value, like here:

// Graphql server
router.use( '/graphql', graphQLHTTP( request => {
  let user_id = '00000000-0000-0000-0000-000000000000'; // Anonymous
  try
  {
    if( request.cookies.auth_token )
      if( request.cookies.auth_token.length > 10 )
      {
        var decoded = jwt.decode( request.cookies.auth_token, process.env.JWT_SECRET );
        user_id = decoded.user_id;
      }
  }
  catch( err )
  {
    console.log( chalk.bold.red( "Failure while decoding JWT token, using anonymous instead." ) );
    console.log( chalk.red( err.message ) );
    console.log( chalk.blue( '.' ) );
  }

  return( {
    schema: schema,
    rootValue: { user_id: user_id },
    pretty: true
  } )
} ) );

and then for server rendering you take it from the request and inject it into the network layer (global object!!):

          // Setting the STATIC network layer. No fear about it being static - we are in a queue!
          Relay.injectNetworkLayer( new Relay.DefaultNetworkLayer( GRAPHQL_URL, { headers: headers } ) );
          RelayStoreData.getDefaultInstance( ).getChangeEmitter( ).injectBatchingStrategy(() => { } );

(shameless self promotion ahead)
https://github.com/codefoundries/isomorphic-material-relay-starter-kit/blob/master/server/server.js
https://github.com/codefoundries/isomorphic-material-relay-starter-kit/blob/master/webapp/renderOnServer.js

fenos commented

@thelordoftheboards I get it! Thanks! Your repo is also very interesting! ๐Ÿ‘ I will go trough it. I will do some trials tomorrow

fenos commented

@thelordoftheboards I did some trials tonight trying to figure out what you suggested yesterday, and I came to the conclusion that i'm still miss something.

Your repository is great, but the difference between my architecture and yours is that my GraphQL server is not aware about sessions. Just the isomorphic app, and the requests are not sent to the Isomorphic app which has capability of handling sessions, they are directly sent to the GraphQL server which would want the JWT.

This is what i tried to do:

renderOnServer middleware

const TokenFromSession = (req.session && req.session.token) ? req.session.token : null;

// Verified token to graphQL server

Relay.injectNetworkLayer(new Relay.DefaultNetworkLayer(GRAPHQL_URL, {
       headers: {
          Authorization: 'Bearer ' + TokenFromSession
       }
}));

RelayStoreData.getDefaultInstance().getChangeEmitter().injectBatchingStrategy(() => {});

IsomorphicRouter.prepareData(renderProps).then(render, next);

now on the client i need to give the token again to Relay on the Default Network otherwise it wouldn't pass it to sub-sequential requests.

client.js

const GRAPHQL_URL = `http://localhost:8085/graphql`;

// HERE i need to give the JWT back to relay so it can send it every request! How??!
Relay.injectNetworkLayer(new Relay.DefaultNetworkLayer(GRAPHQL_URL)); 

let data = [];

// Preload relay data
if (document.getElementById('preloadedData').textContent) {
  data = JSON.parse(document.getElementById('preloadedData').textContent);
}

IsomorphicRelay.injectPreparedData(data);

const rootElement = document.getElementById('content');

ReactDOM.render(
    <IsomorphicRouter.Router routes={routes} history={browserHistory} />,
    rootElement
);

I'm spending a lot time to find the "right" way to do it. At the moment i'm hacking it, to let me continue with other functionalities. Any suggestions would be appreciated

How about this change, to allow it to transfer the cookie?

Relay.injectNetworkLayer( new Relay.DefaultNetworkLayer( GRAPHQL_URL, { credentials: 'same-origin' } ) );
fenos commented

I thought about it, but even if I allow Relay to pass the cookie to the GraphQL i don't think it would work:

Isomorphic app, would issue the HttpOnly cookie.
GraphQL is on a separate domain and server, would it be able to parse the cookie issued from "Isomorphic app"?

Edit: Now that i'm thinking to let the GraphQL receive the cookie and get the information from the session would break the logic of having GraphQL servers stateless

To be honest I can not say much about this. I have only survival skills with cookies and don't know the intricacies of how they work cross domain. One way to find out I guess ...

fenos commented

@thelordoftheboards eheh same here about cookies, I'm just drawing diagrams over and over to find the correct flow.

The only solution that i recently thought for achieve this is to store the token into the local storage of the browser, as well as in the httpOnly cookie so that on refresh i'm going to verify the token from the session
and on the client i'm going to give the token from the storage (previously saved when user logged in).

If the backend find out that the token is invalid, i can just render into the DOM an object containing a flag of validity:

<script id="validity" type="application/json">
{"isValidToken": false}
</script>

so that on the client, i can remove the token from the local storage.

Here i'm illustrating a basic example, of course there would be many scenario to cover, but local storage is better then having the token into the DOM on the first render

client.js

const validity = JSON.parse(document.getElementById('validity').textContent);
const headers = {};

if ( validity.isValidToken) {
    headers.Authorization = 'Bearer' + localStorage.getItem("token"); 
} else {
   localStorage.removeItem("token"); 
}

const GRAPHQL_URL = `http://localhost:8085/graphql`;

Relay.injectNetworkLayer(new Relay.DefaultNetworkLayer(GRAPHQL_URL,headers)); 

let data = [];

// Preload relay data
if (document.getElementById('preloadedData').textContent) {
  data = JSON.parse(document.getElementById('preloadedData').textContent);
}

IsomorphicRelay.injectPreparedData(data);

const rootElement = document.getElementById('content');

ReactDOM.render(
    <IsomorphicRouter.Router routes={routes} history={browserHistory} />,
    rootElement
);

I like your solution. However, if you decide to store in local storage, please consider the security implications, not sure which ones apply to your case:

https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage/

fenos commented

@thelordoftheboards thanks to point me about security of storing the token into the local storage. I read a lot about it and seem how you said that wouldn't be the best thing to do after all. The hacks that i can see with this approach would be only the self-xss which definitely i want to prevent.

Said that i have another solution and i think will be my final implementation, i would do as following.

On the client.js instead to get token from the local storage I would delay the front-end initialisation to one more request ("which should resolve pretty quickly").

The request would be sent to the isomorphic app for ex: 'http://localhost:8000/me" the entry point would just check your session and return the token to you, (if you are logged in)

/me entry point on isomorphic app

app.post('/me', (req,res) => {
    // CRSF token validation passed

     if (req.session && req.session.token) {
           return res.json({token: req.session.token});
     } 

      return res.json({authenticated: false});
});

So on the client.js this would happen:

const fetchTokenOptions = {
  method: 'POST',
  credentials: 'same-origin'    
};

fetch('http://localhost:8000/me',fetchTokenOptions).then((response) => {

    response.json().then((jsonRes) => {


        const headers = {};
        const GRAPHQL_URL = `http://localhost:8085/graphql`;

        // Only if the token is returned from the isomorphic app
        // would be given to Relay
        if (jsonRes.token) {
            headers.Authorization = 'Bearer ' + jsonRes.token;
        }

        Relay.injectNetworkLayer(new Relay.DefaultNetworkLayer(GRAPHQL_URL,headers)); 

        let data = [];

        // Preload relay data
        if (document.getElementById('preloadedData').textContent) {
          data = JSON.parse(document.getElementById('preloadedData').textContent);
        }

        IsomorphicRelay.injectPreparedData(data);

        const rootElement = document.getElementById('content');

        ReactDOM.render(
            <IsomorphicRouter.Router routes={routes} history={browserHistory} />,
            rootElement
        );

    });
});

What do you think about this approach? (it would need the CRSF token protection on this entry point to make it more secure) I believe it cover all security problems and the request would be relatively resolved pretty fast, it would need just to check the session storage (redis).

@thelordoftheboards Could you please explain what the code in the second line do?
Do we need to do that every time we create a new network layer?

          Relay.injectNetworkLayer( new Relay.DefaultNetworkLayer( GRAPHQL_URL, { headers: headers } ) );
          RelayStoreData.getDefaultInstance( ).getChangeEmitter( ).injectBatchingStrategy(() => { } );

@tuananhtd My understanding is that when we inject the network layer, we also pass the headers, which include some kind of user-specific token which is not displayed in your example. We will presumably derive a root value based on this token.
Since the headers are different for each user, the network layer has to be injected for every request.
Check out: https://facebook.github.io/relay/docs/guides-network-layer.html
Does that answer your question?

@fenos sorry for the delayed reply, maybe you have already figured out a satisfactory solution. Have you considered the relay local schema?
Check out: denvned/isomorphic-relay-router#13

@thelordoftheboards I know but what make me curious is the second line. Does it propagate the change of new injected network layer to the Relay Store?
btw it seems like the api has changed since the getDefaultInstance is not there anymore.

@tuananhtd this is above my competency. I am curious about the answer too. @denvned ?

Since v0.6 of isomorphic-relay, probably the most simple approach is to create a new instance of a Relay network layer (with injected user session data/JWT headers/cookies) on each request and pass it as the second argument to IsomorphicRelay.prepareData.