DEV Community

Cover image for Deal with `HttpOnly` cookies with Redux+GraphQL
peterlits zo
peterlits zo

Posted on • Updated on

Deal with `HttpOnly` cookies with Redux+GraphQL

My idea

In this article Use GraphQL to log in with React and Apollo / GraphQL with HttpOnly cookie, I talk about how to deal with GraphQL when we try to sign in.

Now I have no idea how to sign out... I have no idea to ask server send response with Remove-Cookie. There is Set-Cookie rather than Remove-Cookie. By the way, do not tell user sign out if they are offline is also fucking stupid thing.

So the idea is:

  • Ask server send response with HttpOnly cookie.
  • If OK, set a isLogged flag to be "true" in LocalStorage.
  • If we want to logged out, just set isLogged flag to be "false".

In my opinion, it is the most easy way to deal with HttpOnly cookie --- Just do not deal with those.

With Redux

The state of authorization should be a global state rather than a private state of one component. So I will use Redux to handle that data.

We need create a slice named auth (I am going to use GraphQL to get data. It is not hard!):

// Redux's toolkit.
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// GraphQL client.
import { useLazyQuery, gql } from '@apollo/client';

// Import store's root's state's type.
import type { RootState } from './store';
// Import typed function (It works better with TypeScript).
import { useDispatch } from '../hooks';

// The slice's name and the key of the localStorage
const SLICE_NAME = 'auth';
const IS_LOGGED_ITEM_KEY = `store-${SLICE_NAME}-isLogged`;
Enter fullscreen mode Exit fullscreen mode

Now we need add type for the slice's state. It is the union of four substates:

export enum AuthStateEnum {
  LoggedWithInfo,
  LoggedWithoutInfo,
  LoggedAndLoadingInfo,
  Unlogged,
}

interface LoggedWithInfo {
  state: AuthStateEnum.LoggedWithInfo;
  name: string;
  email: string;
  isAdmin: boolean;
}

interface LoggedWithoutInfo {
  state: AuthStateEnum.LoggedWithoutInfo;
}

interface LoggedAndLoadingInfo {
  state: AuthStateEnum.LoggedAndLoadingInfo;
}

interface Unlogged {
  state: AuthStateEnum.Unlogged;
}

type AuthState =
  | LoggedWithInfo
  | LoggedWithoutInfo
  | LoggedAndLoadingInfo
  | Unlogged;
Enter fullscreen mode Exit fullscreen mode

At first, a user is Unlogged if it is the first time for he/she to enter the website. After he/she sign up (and sign in), localStorage will have a item, which means that the user is logged, but not information about the user. The state will be LoggedWithoutInfo. If the application find the user is LoggedWithoutInfo, it will send request to server and set the state to LoggedAndLoading. If finally application get the data, the state will change to LoggedWithInfo.

Then the user enter the website tomorrow, and no data still here for he/she but localStorage and HttpOnly cookies, the application will set his/her state to LoggedWithoutInfo and try to send request to get user's information.

So it will get the initial state by this statement:

const initialState = ((): AuthState => {
  // localStorage will store if client store the HttpOnly cookie `JWT` to get
  // the authentication. If it is is true, we will try to connect to server to
  // get user's information. (So it will be loading soon)
  const isLogged = !!localStorage.getItem(IS_LOGGED_ITEM_KEY);

  return isLogged
    ? {
        state: AuthStateEnum.LoggedWithoutInfo,
      }
    : {
        state: AuthStateEnum.Unlogged,
      };
})();
Enter fullscreen mode Exit fullscreen mode
export const authSlice = createSlice({
  name: SLICE_NAME,
  initialState,
  reducers: {
    // Sign out, just remove the flag.
    signOut: (state: AuthState) => {
      localStorage.removeItem(IS_LOGGED_ITEM_KEY);
      state.state = AuthStateEnum.Unlogged;
    },
    // Really signed, and set the current user's data
    // into store.
    signInWithInfo: (
      _state: AuthState,
      actions: PayloadAction<Omit<LoggedWithInfo, 'state'>>,
    ) => {
      localStorage.setItem(IS_LOGGED_ITEM_KEY, 'true');
      return {
        state: AuthStateEnum.LoggedWithInfo,
        ...actions.payload,
      } as LoggedWithInfo;
    },
    // Just change the state to `signInAndLoading`.
    signInAndLoading: (state: AuthState) => {
      localStorage.removeItem(IS_LOGGED_ITEM_KEY);
      state.state = AuthStateEnum.LoggedAndLoadingInfo;
    },
    // Just change the state to `signInWithoutInfo`.
    signInWithoutInfo: (state: AuthState) => {
      localStorage.removeItem(IS_LOGGED_ITEM_KEY);
      state.state = AuthStateEnum.LoggedWithoutInfo;
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Now we need use apollo's GraphQL query hook to sign in. It is hard to use hook in createAsyncThunk(Do you have good idea? Please email me), so I create a hook to deal with. Here is the GraphQL query statement.

const SIGN_IN_QUERY = gql`
  query SignIn($email: String!, $password: String!) {
    # *************************************************************************
    # WARING: Remember to change interface SIGN_IN_QUERY_Return if you change
    # the code below.
    # *************************************************************************

    # Input email, password, and no output.
    user(email: $email, password: $password) {
      name
      email
      isAdmin
    }
  }
`;

interface SIGN_IN_QUERY_Return {
  name: string;
  email: string;
  isAdmin: boolean;
}
Enter fullscreen mode Exit fullscreen mode

And the hook to sign in is:

export const useSignIn = () => {
  const [signIn, { data, error, loading }] = useLazyQuery(SIGN_IN_QUERY);
  const dispatch = useDispatch();

  if (data) {
    dispatch(signInWithInfo(data as SIGN_IN_QUERY_Return));
  } else if (loading) {
    dispatch(signInAndLoading());
  } else if (error) {
    // do nothing... now
    // TODO: Add error message for user.
  }

  return (email: string, password: string) =>
    signIn({ variables: {email, password} });
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)