Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do you use authMiddleware? #6

Open
sibelius opened this issue Jun 14, 2016 · 19 comments
Open

How do you use authMiddleware? #6

sibelius opened this issue Jun 14, 2016 · 19 comments

Comments

@sibelius
Copy link

I want to set the auth token after login/register process using the token received

is it possible? do u have an example?

@nodkz
Copy link
Collaborator

nodkz commented Jun 14, 2016

Just provide auth middleware to network layer with a token thunk. This function will be called every time, when Relay makes request. You may store your token in global var, localStorage. In my project I use https://www.npmjs.com/package/store

Relay.injectNetworkLayer(new RelayNetworkLayer([
  authMiddleware({
    token: () => window.MyGlobalVarWithToken, // store.get('jwt'), 
  }),
]));

@sibelius
Copy link
Author

thanks, I will use AsyncStorage.

calling it every time is not a problem?

@nodkz
Copy link
Collaborator

nodkz commented Jun 14, 2016

I don't sure that token: async () => await storage('token'), will work at all.
But if does, then you'll get delays before relay send queries to server.

So if possible, try to put token in some variable with sync access.

BTW don't forget about tokenRefreshPromise option in authMiddleware. This promise will be called only if your server returns 401 response. After resolving of token, it implicitly make re-request.

At the end, you may write your own middleware for working with your token logic. As example you may see this simple MW: https://github.com/nodkz/react-relay-network-layer/blob/master/src/middleware/perf.js
You may change req.headers = {} before call next(req).

@sibelius
Copy link
Author

thanks for all the help, I will try to write a middleware

@nodkz
Copy link
Collaborator

nodkz commented Jun 15, 2016

Few minutes ago I publish new version, where was added allowEmptyToken options to authMiddleware. If allowEmptyToken: true, and token is empty, request proceed without Authorization header.

This can help you, if you do requests to graphql server without token and don't want send empty auth header.

@sibelius
Copy link
Author

it could be very useful if we could just call a function on authMiddleware to save the token or remove it

@nodkz
Copy link
Collaborator

nodkz commented Jun 15, 2016

Assumption:
Suppose, such function exists. So you should somehow save reference to this function in some global variable. When you log in, you take this var with function and call it. So I can not imagine this somehow realization should be implemented.

Current realization:
Let this variable save not reference to function which you wish, let it keep token. So just provide this variable to arrow function for token option. When you log in, you just write token to this global var. And with next Relay request it reads token from this global var.

I try keep MW realization as simple, as possible. If I begin store token internally, I should also provide functions for reading and removing token ;).

@sibelius
Copy link
Author

I think it is better to use Relay.Environment to reset relay data on login, thanks for all the support

@nodkz
Copy link
Collaborator

nodkz commented Jun 21, 2016

How I actually use auth middleware:

class ClientApi {
  ...
  getRelayNetworkLayer = () => { 
    return new RelayNetworkLayer([
      authMiddleware({
        token: () => this._token,
        tokenRefreshPromise: () => this._getTokenFromServer(this.url('/auth/jwt/new')),
      }),
  ...
}

refresh token promise has the following view

  _getTokenFromServer = (url, data = {}) => {
    console.log('[AUTH] LOAD NEW CABINET JWT');
    const opts = {
      method: 'POST',
      headers: {
        'Accept': 'application/json', // eslint-disable-line
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    };
    return fetch(url, opts)
      .then(res => {
        if (res.status > 299) { throw new Error(`Wrong response status ${res.status}`); }
        return res;
      })
      .then(res => res.json())
      .then(json => {
        this._token = json.token;
        this._saveTokenToStorage();
        return this._token;
      })
      .catch(err => console.error('[AUTH] ERROR can not load new cabinet jwt', err));
  };

So if new user makes request to server, it responds with 401 header. AuthMW implicitly catchs this header and run _getTokenFromServer. Internally it makes new request to obtain new token from server, and if succeed, store it to localStorage and pass new token to AuthMW. AuthMW makes re-request and seamlessly pass data to Relay.

So app logic does not know anything about authorization.

Our ClientApi also contains following methods loginByOauth
(providerName), loginByEmail(...), logout(). And instance of ClientApi stored in global variable.

Our ClientApi class was written inspired by https://github.com/reindexio/reindex-js/blob/master/src/index.js

@nodkz nodkz changed the title [question] how can I set the token using authMiddleware How do you use authMiddleware? Jun 21, 2016
@nodkz nodkz reopened this Jun 21, 2016
@sibelius
Copy link
Author

for every 401 it will try to get the token?

how _getTokenFromServer get data parameter?

the problem with this approach is that you do not clean the Relay cache, so if an user logged in, then logout, and another user login, this second user could have access to some left data from the first user

@nodkz
Copy link
Collaborator

nodkz commented Jun 21, 2016

Yep, for every 401 request it tries to refresh token calling _getTokenFromServer, and made re-request. If second request produce again 401 error it will be thrown upper.
FYI you may open dialog window with login and password, send it server, get token, and after that resolve Promise from _getTokenFromServer.

data parameter is undefined for this case. I use method _getTokenFromServer from another methods like this ClientApi.switchUser(), loginByEmail(), logout(), loginByOauth(). This methods provide some data for auth server for generating proper token.

In my case I just reload page after logout(). For switchUser() after obtaining token, I just force reload some counters after call, in my case no need clear entire relay store.

@sibelius
Copy link
Author

reload a page is not a good user experience, and it does not work on react native either

@allpwrfulroot
Copy link

allpwrfulroot commented Aug 23, 2016

Also trying to solve this issue on React Native. Did anyone successfully implement a solution for pre-register -> post-login Relay?

@allpwrfulroot
Copy link

allpwrfulroot commented Aug 26, 2016

Happy to keep this (work in progress) React Native sample public as a template, but how to fix the middleware implementation?
https://github.com/allpwrfulroot/testMiddleware

Update: condensed the code question to a gist https://gist.github.com/allpwrfulroot/6b6b58ee2a3efebf138f00714c63630b

@chris-verclytte
Copy link

@sibelius, to address the problem avoiding page refresh, the solution provided by Relay is to create a new Store (see RelayEnvironment) which clean all the cache and avoid one user to see data it should not.
react-router-relay and Relay.Renderer allow you to provide this environment prop.

@sibelius
Copy link
Author

This could be useful to easily reset a RelayEnvironment

class RelayStore {
  constructor() {
    this._env = new Environment();
    this._networkLayer = null;
    this._taskScheduler = null;

    RelayNetworkDebug.init(this._env);
  }

  reset(networkLayer) {
    // invariant(
    //   !this._env.getStoreData().getChangeEmitter().hasActiveListeners() &&
    //   !this._env.getStoreData().getMutationQueue().hasPendingMutations() &&
    //   !this._env.getStoreData().getPendingQueryTracker().hasPendingQueries(),
    //   'RelayStore.reset(): Cannot reset the store while there are active ' +
    //   'Relay Containers or pending mutations/queries.'
    // );

    if (networkLayer !== undefined) {
      this._networkLayer = networkLayer;
    }

    this._env = new Environment();
    if (this._networkLayer !== null) {
      this._env.injectNetworkLayer(this._networkLayer);
    }
    if (this._taskScheduler !== null) {
      this._env.injectTaskScheduler(this._taskScheduler);
    }

    RelayNetworkDebug.init(this._env);
  }

  // Map existing RelayEnvironment methods
  getStoreData() {
    return this._env.getStoreData();
  }

  injectNetworkLayer(networkLayer) {
    this._networkLayer = networkLayer;
    this._env.injectNetworkLayer(networkLayer);
  }

  injectTaskScheduler(taskScheduler) {
    this._taskScheduler = taskScheduler;
    this._env.injectTaskScheduler(taskScheduler);
  }

  primeCache(...args) {
    return this._env.primeCache(...args);
  }

  forceFetch(...args) {
    return this._env.forceFetch(...args);
  }

  read(...args) {
    return this._env.read(...args);
  }

  readAll(...args) {
    return this._env.readAll(...args);
  }

  readQuery(...args) {
    return this._env.readQuery(...args);
  }

  observe(...args) {
    return this._env.observe(...args);
  }

  getFragmentResolver(...args) {
    return this._env.getFragmentResolver(...args);
  }

  applyUpdate(...args) {
    return this._env.applyUpdate(...args);
  }

  commitUpdate(...args) {
    return this._env.commitUpdate(...args);
  }

  /**
   * @deprecated
   *
   * Method renamed to commitUpdate
   */
  update(...args) {
    return this._env.update(...args);
  }
}

const relayStore = new RelayStore();

just call:
RelayStore.reset(networkLayer)

@chris-verclytte
Copy link

chris-verclytte commented Sep 26, 2016

I prefer to solve the problem by simply storing the instance of the environment in a singleton and provide a method to refresh the environment as it avoids a lot of code duplication.
Here is my way to deal with this:

import Relay from 'react-relay';
import {
  RelayNetworkLayer,
  authMiddleware,
  urlMiddleware,
} from 'react-relay-network-layer';

let instance = null;

const tokenRefreshPromise = () => ...;

const refresh = () => {
  instance = new Relay.Environment();

  instance.injectNetworkLayer(new RelayNetworkLayer([
    urlMiddleware({
      url: '<some_url>',
      batchUrl: '<some_batch_url>',
    }),

    next => req => next(
      // Retrieve token info and assign it to req
    ),
    authMiddleware({
      allowEmptyToken: true,
      token: req => req.token,
      tokenRefreshPromise,
    }),
  ], { disableBatchQuery: true }));

  return instance;
};

const getInstance = () => (instance || refresh());

export default {
  getCurrent: getInstance.bind(this),
  refresh: refresh.bind(this),
};

Then you can import this file as Store and pass it to your RelayRenderer as Store.getInstance() and when you need to refresh your data after a logout for instance, just include Store in your file and call Store.refresh(), it generates a new instance and updates it everywhere.

@ryanblakeley
Copy link

ryanblakeley commented Mar 19, 2017

This is how I'm doing it:

networkLayer.js

const networkLayer = new RelayNetworkLayer([
  urlMiddleware({
    url: _ => '/graphql',
  }),
  authMiddleware({
    token: () => localStorage.getItem('id_token'),
  }),
], {disableBatchQuery: true});

Root.js

import React from 'react';
import Relay from 'react-relay';
import {
  Router,
  browserHistory,
  applyRouterMiddleware,
} from 'react-router';
import useRelay from 'react-router-relay';
import ReactGA from 'react-ga';
import networkLayer from 'shared/utils/networkLayer';
import routes from './routes';

class Root extends React.Component {
  static childContextTypes = {
    logout: React.PropTypes.func,
  };
  state = {
    environment: null,
  };
  getChildContext () {
    return {
      logout: _ => this.logout(),
    };
  }
  componentWillMount () {
    ReactGA.initialize(process.env.GOOGLE_ANALYTICS_KEY);
    const environment = new Relay.Environment();
    environment.injectNetworkLayer(networkLayer);
    this.setState({environment});
  }
  logout () {
    const environment = new Relay.Environment();
    environment.injectNetworkLayer(networkLayer);
    this.setState({environment});
  }
  logPageView = _ => {
    ReactGA.set({ page: window.location.pathname });
    ReactGA.pageview(window.location.pathname);
  }
  render () {
    return <Router
      render={applyRouterMiddleware(useRelay)}
      history={browserHistory}
      environment={this.state.environment}
      routes={routes}
      onUpdate={this.logPageView}
      key={Math.random()}
    />;
  }
}

LoginPage.js

This page has a method that gets passed to a login form. The login form calls an AuthenticateUserMutation, and uses this function as a callback:

  loginUser = data => {
    localStorage.setItem('id_token', data.jwtToken);
    localStorage.setItem('user_uuid', data.userId);
    this.context.setLoggedIn(true);
    this.context.setUserId(data.userId);
    this.context.router.push('/profile');
  }

LoginForm.js

  processLogin = response => {
    const { authenticateUser: { authenticateUserResult } } = response;

    if (authenticateUserResult && authenticateUserResult.userId) {
      this.props.loginUser(authenticateUserResult);
    } else {
      this.setState({ loginError: 'Email and/or Password is incorrect' });
    }
  }
  loginUser = ({ email, password }) => {
    this.props.relay.commitUpdate(
      new AuthenticateUserMutation({ email, password }),
      { onSuccess: this.processLogin },
    );
  }

@koistya
Copy link

koistya commented Apr 3, 2023

core/relay.ts

import { getAuth } from "firebase/auth";
import { Environment, Network, RecordSource, Store } from "relay-runtime";

/**
 * Initializes a new instance of Relay environment.
 * @see https://relay.dev/docs/
 */
export function createRelay(): Environment {
  // Configure a network layer that fetches data from the GraphQL API
  // https://relay.dev/docs/guides/network-layer/
  const network = Network.create(async function fetchFn(operation, variables) {
    const auth = getAuth();
    const headers = new Headers({ ["Content-Type"]: "application/json" });

    // When the user is authenticated append the ID token to the request
    if (auth.currentUser) {
      const idToken = await auth.currentUser.getIdToken();
      headers.set("Authorization", `Bearer ${idToken}`);
    }

    const res = await fetch("/api", {
      method: "POST",
      headers,
      credentials: "include",
      body: JSON.stringify({ query: operation.text, variables }),
    });

    if (!res.ok) {
      throw new HttpError(res.status, res.statusText);
    }

    return await res.json();
  });

  // Initialize Relay records store
  const recordSource = new RecordSource();
  const store = new Store(recordSource);

  return new Environment({ store, network, handlerProvider: null });
}

Then, somewhere at the root level:

import { useMemo } as React from "react";
import { RelayEnvironmentProvider } from "react-relay";
import { createRelay } from "../core/relay.js";
import { AppRoutes } from "../routes/index.js";

/**
 * The top-level (root) React component.
 */
export function App(): JSX.Element {
  const relay = useMemo(() => createRelay(), []);

  return (
    <RelayEnvironmentProvider environment={relay}>
      <AppRoutes />
    </RelayEnvironmentProvider>
  );
}

https://github.com/kriasoft/relay-starter-kit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants