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

refactor(auth): Implement new initializer function API #3012

Merged
merged 17 commits into from
Mar 8, 2023

Conversation

kitten
Copy link
Member

@kitten kitten commented Mar 7, 2023

Resolves #2815

Summary

This is a breaking change, which updates the authExchange API.

This PR eliminates the need for an authState (i.e. the authentication state) to be kept with the authExchange, and moves it into the responsibility of the user.
It also eliminates getAuth's "double duty" of being called, both, to initialize the authentication state, and to refresh the authentication state after an authentication error.

The authExchange now accepts an initializer function, which is an async function (or a function returning a promise). This function is user-defined and will receive a utilities object and must return an authentication configuration object.

This configuration object is unchanged but now contains refreshAuth rather than getAuth. Several arguments that its functions accept have been simplified.

authExchange(async utils => {
  let token = localStorage.getItem('token');
  let refreshToken = localStorage.getItem('refreshToken');

  return {
    addAuthToOperation(operation) {
      return utils.appendHeaders(operation, {
        Authorization: `Bearer ${token}`,
      });
    },
    willAuthError(_operation) {
      return false;
    },
    didAuthError(error, _operation) {
      return error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN');
    },
    async refreshAuth() {
      const result = await utils.mutate(REFRESH, { token });
      if (result.data?.refreshLogin) {
        token = result.data.refreshLogin.token;
        refreshToken = result.data.refreshLogin.refreshToken;
        localStorage.setItem('token', token);
        localStorage.setItem('refreshToken', refreshToken);
      }
    },
  };
});

As we can see, the initializer function itself now initializes the authentication state, and the state is just kept in local variables (which also now makes it easier to keep the state in some "third object" outside of the authExchange without any hacky code)

The function returns the configuration and can use utils.mutate to send GraphQL mutations for authentication purposes, and now also has utils.appendHeaders to add headers to operations!

Set of changes

  • Simplify addAuthToOperation, willAuthError, and didAuthError arguments
  • Remove authState and getAuth and replace with initializer
  • Replace getAuth flow with refreshAuth call
  • Update tests
  • Add TSDocs to new exchange and APIs (generates a single d.ts output)
  • Update documentation

NOTE: This isn't super final yet, but the goal here is to remove the API docs on the website and move those over to TSDocs. Hence, the website docs for the authExchange API are very lean now.

@kitten kitten requested a review from JoviDeCroock March 7, 2023 04:08
@kitten kitten changed the title Feat/init auth refactor feat(auth): Implement new initializer function-based API Mar 7, 2023
@kitten kitten changed the title feat(auth): Implement new initializer function-based API refactor(auth): Implement new initializer function-based API Mar 7, 2023
@github-actions github-actions bot temporarily deployed to staging-3012 March 7, 2023 04:09 Inactive
@kitten
Copy link
Member Author

kitten commented Mar 7, 2023

Here's a small guide on how to map things from the "old" API to this "new" API, as proposed.
Comments in ALL_CAPS are meant to mark common sections.

Legacy API

authExchange({
  addAuthToOperation({ authState, operation }) {
    if (!authState || !authState.token) {
      return operation; // DONT_ADD_AUTH
    }
    return operation; // ADD_AUTH
  },
  willAuthError({ authState, operation }) {
    return false; // WILL_AUTH_ERROR
  },
  didAuthError({ error }) {
    return false; // DID_AUTH_ERROR
  },
  async getAuth ({ authState, mutate }) {
    if (!authState) {
      // INITIALIZE_AUTH
      const token = localStorage.getItem('token');
      const refreshToken = localStorage.getItem('refreshToken');
      return token ? { token, refreshToken } : null;
    }

    // REFRESH_AUTH
    const result = await mutate(refreshMutation, { token: authState?.refreshToken });
    if (result.data?.refreshLogin) {
      localStorage.setItem('token', result.data.refreshLogin.token);
      localStorage.setItem('refreshToken', result.data.refreshLogin.refreshToken);
      return {
        token: result.data.refreshLogin.token,
        refreshToken: result.data.refreshLogin.refreshToken,
      };
    }

    // BAIL_REFRESH_AUTH
    localStorage.clear();
    logout();
    return null;
  },
});

Proposed API

authExchange(async (utils) => {
  // INITIALIZE_AUTH
  let token = localStorage.getItem('token');
  let refreshToken = localStorage.getItem('refreshToken');

  return {
    addAuthToOperation(operation) {
      if (!token) {
        return operation; // DONT_ADD_AUTH
      }
      // ADD_AUTH
      return utils.appendHeaders(operation, {
        Authorization: `Bearer ${token}`,
      });
    },
    willAuthError(_operation) {
      return false; // WILL_AUTH_ERROR
    },
    didAuthError(error, _operation) {
      return false; // DID_AUTH_ERROR
    },
    async refreshAuth() {
      // REFRESH_AUTH
      const result = await mutate(refreshMutation, { token: authState?.refreshToken });
      if (result.data?.refreshLogin) {
        token = result.data.refreshLogin.token;
        refreshToken = result.data.refreshLogin.refreshToken;
        localStorage.setItem('token', token);
        localStorage.setItem('refreshToken', refreshToken);
      } else {
        // BAIL_REFRESH_AUTH
        localStorage.clear();
        logout();
      }
    },
  };
});

@kitten kitten force-pushed the feat/init-auth-refactor branch from 29b08b2 to 8e8ffa5 Compare March 7, 2023 17:13
@github-actions github-actions bot temporarily deployed to staging-3012 March 7, 2023 17:14 Inactive
@kitten kitten requested a review from kadikraman March 7, 2023 17:15
@kitten kitten changed the title refactor(auth): Implement new initializer function-based API refactor(auth): Implement new initializer function API Mar 7, 2023
Copy link
Collaborator

@JoviDeCroock JoviDeCroock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a huge improvement

@github-actions github-actions bot temporarily deployed to staging-3012 March 8, 2023 17:12 Inactive
@kitten kitten force-pushed the feat/init-auth-refactor branch from cb96b85 to 02fb651 Compare March 8, 2023 17:12
@github-actions github-actions bot temporarily deployed to staging-3012 March 8, 2023 17:14 Inactive
@github-actions github-actions bot temporarily deployed to staging-3012 March 8, 2023 18:41 Inactive
@github-actions github-actions bot temporarily deployed to staging-3012 March 8, 2023 22:22 Inactive
@github-actions github-actions bot temporarily deployed to staging-3012 March 8, 2023 22:35 Inactive
@kitten kitten merged commit 4133d91 into main Mar 8, 2023
@kitten kitten deleted the feat/init-auth-refactor branch March 8, 2023 22:47
@github-actions github-actions bot mentioned this pull request Mar 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

RFC (auth): separate initial and refresh auth
3 participants