diff --git a/js/package.json b/js/package.json
index 45be10602..3250a61dc 100644
--- a/js/package.json
+++ b/js/package.json
@@ -17,7 +17,6 @@
     "apollo-client": "2.5.1",
     "apollo-link": "^1.2.11",
     "apollo-link-http": "^1.5.14",
-    "apollo-link-state": "^0.4.2",
     "buefy": "^0.7.3",
     "easygettext": "^2.7.0",
     "graphql": "^14.2.1",
@@ -53,6 +52,7 @@
     "@vue/cli-service": "^3.6.0",
     "@vue/eslint-config-typescript": "^4.0.0",
     "@vue/test-utils": "^1.0.0-beta.29",
+    "apollo-link-error": "^1.1.11",
     "chai": "^4.2.0",
     "dotenv-webpack": "^1.7.0",
     "eslint": "^6.0.1",
diff --git a/js/src/App.vue b/js/src/App.vue
index 183adb48c..2be5576e2 100644
--- a/js/src/App.vue
+++ b/js/src/App.vue
@@ -11,7 +11,7 @@
 <script lang="ts">
 import NavBar from '@/components/NavBar.vue';
 import { Component, Vue } from 'vue-property-decorator';
-import { AUTH_TOKEN, AUTH_USER_ACTOR, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
+import { AUTH_ACCESS_TOKEN, AUTH_USER_ACTOR, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
 import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
 import { ICurrentUser } from '@/types/current-user.model';
 import Footer from '@/components/Footer.vue';
@@ -34,20 +34,20 @@ export default class App extends Vue {
 
   actor = localStorage.getItem(AUTH_USER_ACTOR);
 
-  async mounted () {
+  async mounted() {
     await this.initializeCurrentUser();
   }
 
-  getUser (): ICurrentUser|false {
+  getUser(): ICurrentUser | false {
     return this.currentUser.id ? this.currentUser : false;
   }
 
   private initializeCurrentUser() {
     const userId = localStorage.getItem(AUTH_USER_ID);
     const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
-    const token = localStorage.getItem(AUTH_TOKEN);
+    const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
 
-    if (userId && userEmail && token) {
+    if (userId && userEmail && accessToken) {
       return this.$apollo.mutate({
         mutation: UPDATE_CURRENT_USER_CLIENT,
         variables: {
@@ -62,42 +62,42 @@ export default class App extends Vue {
 </script>
 
 <style lang="scss">
-  @import "variables";
+@import "variables";
 
-  /* Bulma imports */
-  @import "~bulma/sass/base/_all.sass";
-  @import "~bulma/sass/components/card.sass";
-  @import "~bulma/sass/components/media.sass";
-  @import "~bulma/sass/components/message.sass";
-  @import "~bulma/sass/components/modal.sass";
-  @import "~bulma/sass/components/navbar.sass";
-  @import "~bulma/sass/components/pagination.sass";
-  @import "~bulma/sass/components/dropdown.sass";
-  @import "~bulma/sass/elements/box.sass";
-  @import "~bulma/sass/elements/button.sass";
-  @import "~bulma/sass/elements/container.sass";
-  @import "~bulma/sass/form/_all";
-  @import "~bulma/sass/elements/icon.sass";
-  @import "~bulma/sass/elements/image.sass";
-  @import "~bulma/sass/elements/other.sass";
-  @import "~bulma/sass/elements/tag.sass";
-  @import "~bulma/sass/elements/title.sass";
-  @import "~bulma/sass/elements/notification";
-  @import "~bulma/sass/grid/_all.sass";
-  @import "~bulma/sass/layout/_all.sass";
-  @import "~bulma/sass/utilities/_all";
+/* Bulma imports */
+@import "~bulma/sass/base/_all.sass";
+@import "~bulma/sass/components/card.sass";
+@import "~bulma/sass/components/media.sass";
+@import "~bulma/sass/components/message.sass";
+@import "~bulma/sass/components/modal.sass";
+@import "~bulma/sass/components/navbar.sass";
+@import "~bulma/sass/components/pagination.sass";
+@import "~bulma/sass/components/dropdown.sass";
+@import "~bulma/sass/elements/box.sass";
+@import "~bulma/sass/elements/button.sass";
+@import "~bulma/sass/elements/container.sass";
+@import "~bulma/sass/form/_all";
+@import "~bulma/sass/elements/icon.sass";
+@import "~bulma/sass/elements/image.sass";
+@import "~bulma/sass/elements/other.sass";
+@import "~bulma/sass/elements/tag.sass";
+@import "~bulma/sass/elements/title.sass";
+@import "~bulma/sass/elements/notification";
+@import "~bulma/sass/grid/_all.sass";
+@import "~bulma/sass/layout/_all.sass";
+@import "~bulma/sass/utilities/_all";
 
-  /* Buefy imports */
-  @import "~buefy/src/scss/utils/_all";
-  @import "~buefy/src/scss/components/datepicker";
-  @import "~buefy/src/scss/components/notices";
-  @import "~buefy/src/scss/components/dropdown";
-  @import "~buefy/src/scss/components/autocomplete";
-  @import "~buefy/src/scss/components/form";
-  @import "~buefy/src/scss/components/modal";
-  @import "~buefy/src/scss/components/tag";
-  @import "~buefy/src/scss/components/taginput";
-  @import "~buefy/src/scss/components/upload";
+/* Buefy imports */
+@import "~buefy/src/scss/utils/_all";
+@import "~buefy/src/scss/components/datepicker";
+@import "~buefy/src/scss/components/notices";
+@import "~buefy/src/scss/components/dropdown";
+@import "~buefy/src/scss/components/autocomplete";
+@import "~buefy/src/scss/components/form";
+@import "~buefy/src/scss/components/modal";
+@import "~buefy/src/scss/components/tag";
+@import "~buefy/src/scss/components/taginput";
+@import "~buefy/src/scss/components/upload";
 
 .router-enter-active,
 .router-leave-active {
diff --git a/js/src/apollo/user.ts b/js/src/apollo/user.ts
index fdf1804bd..9bdf94446 100644
--- a/js/src/apollo/user.ts
+++ b/js/src/apollo/user.ts
@@ -1,27 +1,32 @@
-export const currentUser = {
-  defaults: {
-    currentUser: {
-      __typename: 'CurrentUser',
-      id: null,
-      email: null,
-      isLoggedIn: false,
-    },
-  },
+import { ApolloCache } from 'apollo-cache';
+import { NormalizedCacheObject } from 'apollo-cache-inmemory';
 
-  resolvers: {
-    Mutation: {
-      updateCurrentUser: (_, { id, email, isLoggedIn }, { cache }) => {
-        const data = {
+export function buildCurrentUserResolver(cache: ApolloCache<NormalizedCacheObject>) {
+  cache.writeData({
+    data: {
+      currentUser: {
+        __typename: 'CurrentUser',
+        id: null,
+        email: null,
+        isLoggedIn: false,
+      },
+    },
+  });
+
+  return {
+    updateCurrentUser: (_, { id, email, isLoggedIn }, { cache }) => {
+      const data = {
+        Mutation: {
           currentUser: {
             id,
             email,
             isLoggedIn,
             __typename: 'CurrentUser',
           },
-        };
+        },
+      };
 
-        cache.writeData({ data });
-      },
+      cache.writeData({ data });
     },
-  },
-};
+  };
+}
diff --git a/js/src/components/Event/AddressAutoComplete.vue b/js/src/components/Event/AddressAutoComplete.vue
index 023ef7963..b3362a61c 100644
--- a/js/src/components/Event/AddressAutoComplete.vue
+++ b/js/src/components/Event/AddressAutoComplete.vue
@@ -45,7 +45,7 @@ export default class AddressAutoComplete extends Vue {
     this.data = result.data.searchAddress as IAddress[];
   }
 
-  @Watch("selected")
+  @Watch('selected')
   updateSelected() {
     this.$emit('input', this.selected);
   }
diff --git a/js/src/components/NavBar.vue b/js/src/components/NavBar.vue
index 727325053..972a55dfa 100644
--- a/js/src/components/NavBar.vue
+++ b/js/src/components/NavBar.vue
@@ -61,9 +61,8 @@
 
 <script lang="ts">
 import { Component, Vue, Watch } from 'vue-property-decorator';
-import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
-import { onLogout } from '@/vue-apollo';
-import { deleteUserData } from '@/utils/auth';
+import { CURRENT_USER_CLIENT } from '@/graphql/user';
+import { logout } from '@/utils/auth';
 import { LOGGED_PERSON } from '@/graphql/actor';
 import { IPerson } from '@/types/actor';
 import { CONFIG } from '@/graphql/config';
@@ -89,7 +88,7 @@ import SearchField from '@/components/SearchField.vue';
 export default class NavBar extends Vue {
   notifications = [
     { header: 'Coucou' },
-    { title: "T'as une notification", subtitle: 'Et elle est cool' },
+    { title: 'T\'as une notification', subtitle: 'Et elle est cool' },
   ];
   loggedPerson: IPerson | null = null;
   config!: IConfig;
@@ -111,31 +110,20 @@ export default class NavBar extends Vue {
   }
 
   async logout() {
-    await this.$apollo.mutate({
-      mutation: UPDATE_CURRENT_USER_CLIENT,
-      variables: {
-        id: null,
-        email: null,
-        isLoggedIn: false,
-      },
-    });
-
-    deleteUserData();
-
-    onLogout(this.$apollo);
+    await logout(this.$apollo.provider.defaultClient);
 
     return this.$router.push({ path: '/' });
   }
 }
 </script>
 <style lang="scss" scoped>
-  @import "../variables.scss";
+@import "../variables.scss";
 
-  nav {
-    border-bottom: solid 1px #0a0a0a;
+nav {
+  border-bottom: solid 1px #0a0a0a;
 
-    .navbar-item img {
-      max-height: 2.5em;
-    }
+  .navbar-item img {
+    max-height: 2.5em;
   }
+}
 </style>
diff --git a/js/src/constants.ts b/js/src/constants.ts
index 9ccb54d2b..79dbe270f 100644
--- a/js/src/constants.ts
+++ b/js/src/constants.ts
@@ -1,4 +1,5 @@
-export const AUTH_TOKEN = 'auth-token';
+export const AUTH_ACCESS_TOKEN = 'auth-access-token';
+export const AUTH_REFRESH_TOKEN = 'auth-refresh-token';
 export const AUTH_USER_ID = 'auth-user-id';
 export const AUTH_USER_EMAIL = 'auth-user-email';
 export const AUTH_USER_ACTOR = 'auth-user-actor';
diff --git a/js/src/graphql/auth.ts b/js/src/graphql/auth.ts
index 8b757ee59..e491a9c11 100644
--- a/js/src/graphql/auth.ts
+++ b/js/src/graphql/auth.ts
@@ -3,7 +3,8 @@ import gql from 'graphql-tag';
 export const LOGIN = gql`
 mutation Login($email: String!, $password: String!) {
   login(email: $email, password: $password) {
-    token,
+    accessToken,
+    refreshToken,
     user {
       id,
     }
@@ -33,3 +34,12 @@ mutation ResendConfirmationEmail($email: String!) {
   resendConfirmationEmail(email: $email)
 }
 `;
+
+export const REFRESH_TOKEN = gql`
+  mutation RefreshToken($refreshToken: String!) {
+    refreshToken(refreshToken: $refreshToken) {
+      accessToken,
+      refreshToken,
+    }
+  }
+`;
diff --git a/js/src/graphql/user.ts b/js/src/graphql/user.ts
index 06c300404..b23b5da32 100644
--- a/js/src/graphql/user.ts
+++ b/js/src/graphql/user.ts
@@ -12,7 +12,8 @@ mutation CreateUser($email: String!, $password: String!) {
 export const VALIDATE_USER = gql`
 mutation ValidateUser($token: String!) {
   validateUser(token: $token) {
-    token,
+    accessToken,
+    refreshToken,
     user {
       id,
       email,
diff --git a/js/src/router/guards/auth-guard.ts b/js/src/router/guards/auth-guard.ts
index fb1c0c9ec..1f6bf82b4 100644
--- a/js/src/router/guards/auth-guard.ts
+++ b/js/src/router/guards/auth-guard.ts
@@ -1,13 +1,13 @@
 import { NavigationGuard } from 'vue-router';
 import { UserRouteName } from '@/router/user';
 import { LoginErrorCode } from '@/types/login-error-code.model';
-import { AUTH_TOKEN } from '@/constants';
+import { AUTH_ACCESS_TOKEN } from '@/constants';
 
 export const authGuardIfNeeded: NavigationGuard = async function (to, from, next) {
   if (to.meta.requiredAuth !== true) return next();
 
   // We can't use "currentUser" from apollo here because we may not have loaded the user from the local storage yet
-  if (!localStorage.getItem(AUTH_TOKEN)) {
+  if (!localStorage.getItem(AUTH_ACCESS_TOKEN)) {
     return next({
       name: UserRouteName.LOGIN,
       query: {
diff --git a/js/src/types/apollo.ts b/js/src/types/apollo.ts
new file mode 100644
index 000000000..a35cb8307
--- /dev/null
+++ b/js/src/types/apollo.ts
@@ -0,0 +1,7 @@
+import { ServerError, ServerParseError } from 'apollo-link-http-common';
+
+function isServerError(err: Error | ServerError | ServerParseError | undefined): err is ServerError {
+  return !!err && (err as ServerError).statusCode !== undefined;
+}
+
+export { isServerError };
diff --git a/js/src/types/login.model.ts b/js/src/types/login.model.ts
index 73289a066..3807b150b 100644
--- a/js/src/types/login.model.ts
+++ b/js/src/types/login.model.ts
@@ -1,7 +1,10 @@
 import { ICurrentUser } from '@/types/current-user.model';
 
-export interface ILogin {
-  user: ICurrentUser;
-
-  token: string;
+export interface IToken {
+  accessToken: string;
+  refreshToken: string;
+}
+
+export interface ILogin extends IToken {
+  user: ICurrentUser;
 }
diff --git a/js/src/utils/auth.ts b/js/src/utils/auth.ts
index c2cb8010a..29698bf7a 100644
--- a/js/src/utils/auth.ts
+++ b/js/src/utils/auth.ts
@@ -1,14 +1,38 @@
-import { AUTH_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
-import { ILogin } from '@/types/login.model';
+import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
+import { ILogin, IToken } from '@/types/login.model';
+import { UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
+import { onLogout } from '@/vue-apollo';
+import ApolloClient from 'apollo-client';
 
 export function saveUserData(obj: ILogin) {
   localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`);
   localStorage.setItem(AUTH_USER_EMAIL, obj.user.email);
-  localStorage.setItem(AUTH_TOKEN, obj.token);
+
+  saveTokenData(obj);
+}
+
+export function saveTokenData(obj: IToken) {
+  localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken);
+  localStorage.setItem(AUTH_REFRESH_TOKEN, obj.refreshToken);
 }
 
 export function deleteUserData() {
-  for (const key of [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_TOKEN]) {
+  for (const key of [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN]) {
     localStorage.removeItem(key);
   }
 }
+
+export function logout(apollo: ApolloClient<any>) {
+  apollo.mutate({
+    mutation: UPDATE_CURRENT_USER_CLIENT,
+    variables: {
+      id: null,
+      email: null,
+      isLoggedIn: false,
+    },
+  });
+
+  deleteUserData();
+
+  onLogout();
+}
diff --git a/js/src/views/Event/Create.vue b/js/src/views/Event/Create.vue
index 856432c05..0de44411e 100644
--- a/js/src/views/Event/Create.vue
+++ b/js/src/views/Event/Create.vue
@@ -137,9 +137,9 @@ export default class CreateEvent extends Vue {
     const obj = {
       organizerActorId: this.loggedPerson.id,
       beginsOn: this.event.beginsOn.toISOString(),
-      tags: this.event.tags.map((tag: ITag) => tag.title)
+      tags: this.event.tags.map((tag: ITag) => tag.title),
     };
-    let res = Object.assign({}, this.event, obj);
+    const res = Object.assign({}, this.event, obj);
 
     if (this.event.physicalAddress) {
       delete this.event.physicalAddress['__typename'];
diff --git a/js/src/views/Home.vue b/js/src/views/Home.vue
index 77970c1c5..2185c2c44 100644
--- a/js/src/views/Home.vue
+++ b/js/src/views/Home.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="container">
+  <div class="container" v-if="config">
     <section class="hero is-link" v-if="!currentUser.id || !loggedPerson">
       <div class="hero-body">
         <div class="container">
diff --git a/js/src/views/User/Validate.vue b/js/src/views/User/Validate.vue
index 7ac0a216e..76b3237d8 100644
--- a/js/src/views/User/Validate.vue
+++ b/js/src/views/User/Validate.vue
@@ -19,9 +19,10 @@
 <script lang="ts">
 import { VALIDATE_USER } from '@/graphql/user';
 import { Component, Prop, Vue } from 'vue-property-decorator';
-import { AUTH_TOKEN, AUTH_USER_ID } from '@/constants';
+import { AUTH_USER_ID } from '@/constants';
 import { RouteName } from '@/router';
 import { UserRouteName } from '@/router/user';
+import { saveTokenData } from '@/utils/auth';
 
 @Component
 export default class Validate extends Vue {
@@ -62,7 +63,8 @@ export default class Validate extends Vue {
 
   saveUserData({ validateUser: login }) {
     localStorage.setItem(AUTH_USER_ID, login.user.id);
-    localStorage.setItem(AUTH_TOKEN, login.token);
+
+    saveTokenData(login);
   }
 }
 </script>
diff --git a/js/src/vue-apollo.ts b/js/src/vue-apollo.ts
index 69b2ef726..770e90b08 100644
--- a/js/src/vue-apollo.ts
+++ b/js/src/vue-apollo.ts
@@ -1,15 +1,18 @@
 import Vue from 'vue';
 import VueApollo from 'vue-apollo';
-import { ApolloLink } from 'apollo-link';
+import { ApolloLink, Observable } from 'apollo-link';
 import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
+import { onError } from 'apollo-link-error';
 import { createLink } from 'apollo-absinthe-upload-link';
-import { AUTH_TOKEN } from './constants';
 import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint';
-import { withClientState } from 'apollo-link-state';
-import { currentUser } from '@/apollo/user';
-import merge from 'lodash/merge';
 import { ApolloClient } from 'apollo-client';
 import { DollarApollo } from 'vue-apollo/types/vue-apollo';
+import { buildCurrentUserResolver } from '@/apollo/user';
+import { isServerError } from '@/types/apollo';
+import { inspect } from 'util';
+import { REFRESH_TOKEN } from '@/graphql/auth';
+import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from '@/constants';
+import { logout, saveTokenData } from '@/utils/auth';
 
 // Install the vue plugin
 Vue.use(VueApollo);
@@ -44,14 +47,11 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
   },
 });
 
-const cache = new InMemoryCache({ fragmentMatcher });
-
 const authMiddleware = new ApolloLink((operation, forward) => {
   // add the authorization to the headers
-  const token = localStorage.getItem(AUTH_TOKEN);
   operation.setContext({
     headers: {
-      authorization: token ? `Bearer ${token}` : null,
+      authorization: generateTokenHeader(),
     },
   });
 
@@ -64,21 +64,54 @@ const uploadLink = createLink({
   uri: httpEndpoint,
 });
 
-const stateLink = withClientState({
-  ...merge(currentUser),
-  cache,
+let refreshingTokenPromise: Promise<boolean> | undefined;
+let alreadyRefreshedToken = false;
+const errorLink = onError(({ graphQLErrors, networkError, forward, operation }) => {
+  if (isServerError(networkError) && networkError.statusCode === 401 && !alreadyRefreshedToken) {
+    if (!refreshingTokenPromise) refreshingTokenPromise = refreshAccessToken();
+
+    return promiseToObservable(refreshingTokenPromise).flatMap(() => {
+      refreshingTokenPromise = undefined;
+      alreadyRefreshedToken = true;
+
+      const context = operation.getContext();
+      const oldHeaders = context.headers;
+
+      operation.setContext({
+        headers: {
+          ...oldHeaders,
+          authorization: generateTokenHeader(),
+        },
+      });
+
+      return forward(operation);
+    });
+  }
+
+  if (graphQLErrors) {
+    graphQLErrors.forEach(({ message, locations, path }) =>
+      console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`),
+    );
+  }
+
+  if (networkError) console.log(`[Network error]: ${networkError}`);
 });
 
-const link = stateLink.concat(authMiddleware).concat(uploadLink);
+const link = authMiddleware
+  .concat(errorLink)
+  .concat(uploadLink);
+
+const cache = new InMemoryCache({ fragmentMatcher });
 
 const apolloClient = new ApolloClient({
   cache,
   link,
   connectToDevTools: true,
+  resolvers: {
+    currentUser: buildCurrentUserResolver(cache),
+  },
 });
 
-apolloClient.onResetStore(stateLink.writeDefaults as any);
-
 export const apolloProvider = new VueApollo({
   defaultClient: apolloClient,
   errorHandler(error) {
@@ -93,13 +126,65 @@ export function onLogin(apolloClient) {
 }
 
 // Manually call this when user log out
-export async function onLogout(apolloClient: DollarApollo<any>) {
+export async function onLogout() {
   // if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
 
   try {
-    await apolloClient.provider.defaultClient.resetStore();
+    await apolloClient.resetStore();
   } catch (e) {
     // eslint-disable-next-line no-console
     console.log('%cError on cache reset (logout)', 'color: orange;', e.message);
   }
 }
+
+async function refreshAccessToken() {
+  // Remove invalid access token, so the next request is not authenticated
+  localStorage.removeItem(AUTH_ACCESS_TOKEN);
+
+  const refreshToken = localStorage.getItem(AUTH_REFRESH_TOKEN);
+
+  console.log('Refreshing access token.');
+
+  try {
+    const res = await apolloClient.mutate({
+      mutation: REFRESH_TOKEN,
+      variables: {
+        refreshToken,
+      },
+    });
+
+    saveTokenData(res.data.refreshToken);
+
+    return true;
+  } catch (err) {
+
+    return false;
+  }
+}
+
+function generateTokenHeader() {
+  const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
+
+  return token ? `Bearer ${token}` : null;
+}
+
+// Thanks: https://github.com/apollographql/apollo-link/issues/747#issuecomment-502676676
+const promiseToObservable = <T> (promise: Promise<T>) => {
+  return new Observable<T>((subscriber) => {
+    promise.then(
+      (value) => {
+        if (subscriber.closed) {
+          return;
+        }
+        subscriber.next(value);
+        subscriber.complete();
+      },
+      (err) => {
+        console.error('Cannot refresh token.', err);
+
+        subscriber.error(err);
+        logout(apolloClient);
+      },
+    );
+  });
+};
diff --git a/js/yarn.lock b/js/yarn.lock
index 0d77013df..d449524c3 100644
--- a/js/yarn.lock
+++ b/js/yarn.lock
@@ -1845,6 +1845,15 @@ apollo-link-dedup@^1.0.0:
     apollo-link "^1.2.12"
     tslib "^1.9.3"
 
+apollo-link-error@^1.1.11:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.11.tgz#7cd363179616fb90da7866cee85cb00ee45d2f3b"
+  integrity sha512-442DNqn3CNRikDaenMMkoDmCRmkoUx/XyUMlRTZBEFdTw3FYPQLsmDO3hzzC4doY5/BHcn9/jdYh9EeLx4HPsA==
+  dependencies:
+    apollo-link "^1.2.12"
+    apollo-link-http-common "^0.2.14"
+    tslib "^1.9.3"
+
 apollo-link-http-common@^0.2.14, apollo-link-http-common@^0.2.4:
   version "0.2.14"
   resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.14.tgz#d3a195c12e00f4e311c417f121181dcc31f7d0c8"
@@ -1863,14 +1872,6 @@ apollo-link-http@^1.3.2, apollo-link-http@^1.5.14:
     apollo-link-http-common "^0.2.14"
     tslib "^1.9.3"
 
-apollo-link-state@^0.4.2:
-  version "0.4.2"
-  resolved "https://registry.yarnpkg.com/apollo-link-state/-/apollo-link-state-0.4.2.tgz#ac00e9be9b0ca89eae0be6ba31fe904b80bbe2e8"
-  integrity sha512-xMPcAfuiPVYXaLwC6oJFIZrKgV3GmdO31Ag2eufRoXpvT0AfJZjdaPB4450Nu9TslHRePN9A3quxNueILlQxlw==
-  dependencies:
-    apollo-utilities "^1.0.8"
-    graphql-anywhere "^4.1.0-alpha.0"
-
 apollo-link@^1.0.0, apollo-link@^1.0.7, apollo-link@^1.2.11, apollo-link@^1.2.12:
   version "1.2.12"
   resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429"
@@ -1890,7 +1891,7 @@ apollo-utilities@1.2.1:
     ts-invariant "^0.2.1"
     tslib "^1.9.3"
 
-apollo-utilities@1.3.2, apollo-utilities@^1.0.8, apollo-utilities@^1.2.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
+apollo-utilities@1.3.2, apollo-utilities@^1.2.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
   integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==
@@ -4931,15 +4932,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
   resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
   integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
 
-graphql-anywhere@^4.1.0-alpha.0:
-  version "4.2.4"
-  resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.2.4.tgz#7f1c08c9348c730c6bb5e818c81f0b72c13696a8"
-  integrity sha512-rN6Op5vle0Ucqo8uOVPuFzRz1L/MB+ZVa+XezhFcQ6iP13vy95HOXRysrRtWcu2kQQTLyukSGmfU08D8LXWSIw==
-  dependencies:
-    apollo-utilities "^1.3.2"
-    ts-invariant "^0.3.2"
-    tslib "^1.9.3"
-
 graphql-tag@^2.10.1:
   version "2.10.1"
   resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02"
@@ -10196,13 +10188,6 @@ ts-invariant@^0.2.1:
   dependencies:
     tslib "^1.9.3"
 
-ts-invariant@^0.3.2:
-  version "0.3.3"
-  resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.3.3.tgz#b5742b1885ecf9e29c31a750307480f045ec0b16"
-  integrity sha512-UReOKsrJFGC9tUblgSRWo+BsVNbEd77Cl6WiV/XpMlkifXwNIJbknViCucHvVZkXSC/mcWeRnIGdY7uprcwvdQ==
-  dependencies:
-    tslib "^1.9.3"
-
 ts-invariant@^0.4.0:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
diff --git a/lib/mobilizon/users/users.ex b/lib/mobilizon/users/users.ex
index 0883f5d75..92697d931 100644
--- a/lib/mobilizon/users/users.ex
+++ b/lib/mobilizon/users/users.ex
@@ -27,7 +27,9 @@ defmodule Mobilizon.Users do
   @spec register(map()) :: {:ok, User.t()} | {:error, String.t()}
   def register(%{email: _email, password: _password} = args) do
     with {:ok, %User{} = user} <-
-           %User{} |> User.registration_changeset(args) |> Mobilizon.Repo.insert() do
+           %User{}
+           |> User.registration_changeset(args)
+           |> Mobilizon.Repo.insert() do
       Mobilizon.Events.create_feed_token(%{"user_id" => user.id})
       {:ok, user}
     end
@@ -51,13 +53,15 @@ defmodule Mobilizon.Users do
           from(u in User, where: u.email == ^email, preload: :default_actor)
 
         true ->
-          from(u in User,
+          from(
+            u in User,
             where: u.email == ^email and not is_nil(u.confirmed_at),
             preload: :default_actor
           )
 
         false ->
-          from(u in User,
+          from(
+            u in User,
             where: u.email == ^email and is_nil(u.confirmed_at),
             preload: :default_actor
           )
@@ -75,7 +79,8 @@ defmodule Mobilizon.Users do
   @spec get_user_by_activation_token(String.t()) :: Actor.t()
   def get_user_by_activation_token(token) do
     Repo.one(
-      from(u in User,
+      from(
+        u in User,
         where: u.confirmation_token == ^token,
         preload: [:default_actor]
       )
@@ -88,7 +93,8 @@ defmodule Mobilizon.Users do
   @spec get_user_by_reset_password_token(String.t()) :: Actor.t()
   def get_user_by_reset_password_token(token) do
     Repo.one(
-      from(u in User,
+      from(
+        u in User,
         where: u.reset_password_token == ^token,
         preload: [:default_actor]
       )
@@ -197,14 +203,16 @@ defmodule Mobilizon.Users do
   @spec get_actor_for_user(Mobilizon.Users.User.t()) :: Mobilizon.Actors.Actor.t()
   def get_actor_for_user(%Mobilizon.Users.User{} = user) do
     case Repo.one(
-           from(a in Actor,
+           from(
+             a in Actor,
              join: u in User,
              on: u.default_actor_id == a.id,
              where: u.id == ^user.id
            )
          ) do
       nil ->
-        case user |> get_actors_for_user() do
+        case user
+             |> get_actors_for_user() do
           [] -> nil
           actors -> hd(actors)
         end
@@ -223,22 +231,55 @@ defmodule Mobilizon.Users do
   """
   def authenticate(%{user: user, password: password}) do
     # Does password match the one stored in the database?
-    case Argon2.verify_pass(password, user.password_hash) do
-      true ->
-        # Yes, create and return the token
-        MobilizonWeb.Guardian.encode_and_sign(user)
-
+    with true <- Argon2.verify_pass(password, user.password_hash),
+         # Yes, create and return the token
+         {:ok, tokens} <- generate_tokens(user) do
+      {:ok, tokens}
+    else
       _ ->
         # No, return an error
         {:error, :unauthorized}
     end
   end
 
+  @doc """
+  Generate access token and refresh token
+  """
+  def generate_tokens(user) do
+    with {:ok, access_token} <- generate_access_token(user),
+         {:ok, refresh_token} <- generate_refresh_token(user) do
+      {:ok, %{access_token: access_token, refresh_token: refresh_token}}
+    end
+  end
+
+  defp generate_access_token(user) do
+    with {:ok, access_token, _claims} <-
+           MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "access") do
+      {:ok, access_token}
+    end
+  end
+
+  def generate_refresh_token(user) do
+    with {:ok, refresh_token, _claims} <-
+           MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "refresh") do
+      {:ok, refresh_token}
+    end
+  end
+
   def update_user_default_actor(user_id, actor_id) do
     with _ <-
-           from(u in User, where: u.id == ^user_id, update: [set: [default_actor_id: ^actor_id]])
+           from(
+             u in User,
+             where: u.id == ^user_id,
+             update: [
+               set: [
+                 default_actor_id: ^actor_id
+               ]
+             ]
+           )
            |> Repo.update_all([]) do
-      Repo.get!(User, user_id) |> Repo.preload([:default_actor])
+      Repo.get!(User, user_id)
+      |> Repo.preload([:default_actor])
     end
   end
 
diff --git a/lib/mobilizon_web/context.ex b/lib/mobilizon_web/context.ex
index be5834d04..7721162d4 100644
--- a/lib/mobilizon_web/context.ex
+++ b/lib/mobilizon_web/context.ex
@@ -17,7 +17,8 @@ defmodule MobilizonWeb.Context do
     context =
       case Guardian.Plug.current_resource(conn) do
         %User{} = user ->
-          Map.put(context, :current_user, user)
+          context
+          |> Map.put(:current_user, user)
 
         nil ->
           context
diff --git a/lib/mobilizon_web/guardian.ex b/lib/mobilizon_web/guardian.ex
index 465178f1b..8fd954c6d 100644
--- a/lib/mobilizon_web/guardian.ex
+++ b/lib/mobilizon_web/guardian.ex
@@ -61,6 +61,14 @@ defmodule MobilizonWeb.Guardian do
     end
   end
 
+  def on_refresh({old_token, old_claims}, {new_token, new_claims}, _options) do
+    with {:ok, _, _} <- Guardian.DB.on_refresh({old_token, old_claims}, {new_token, new_claims}) do
+      {:ok, {old_token, old_claims}, {new_token, new_claims}}
+    end
+  end
+
+  def on_exchange(old_stuff, new_stuff, options), do: on_refresh(old_stuff, new_stuff, options)
+
   #  def build_claims(claims, _resource, opts) do
   #    claims = claims
   #             |> encode_permissions_into_claims!(Keyword.get(opts, :permissions))
diff --git a/lib/mobilizon_web/resolvers/user.ex b/lib/mobilizon_web/resolvers/user.ex
index fed884d19..500036fa3 100644
--- a/lib/mobilizon_web/resolvers/user.ex
+++ b/lib/mobilizon_web/resolvers/user.ex
@@ -20,7 +20,15 @@ defmodule MobilizonWeb.Resolvers.User do
   @doc """
   Return current logged-in user
   """
-  def get_current_user(_parent, _args, %{context: %{current_user: user}}) do
+  def get_current_user(
+        _parent,
+        _args,
+        %{
+          context: %{
+            current_user: user
+          }
+        }
+      ) do
     {:ok, user}
   end
 
@@ -35,7 +43,11 @@ defmodule MobilizonWeb.Resolvers.User do
         _parent,
         %{page: page, limit: limit, sort: sort, direction: direction},
         %{
-          context: %{current_user: %User{role: role}}
+          context: %{
+            current_user: %User{
+              role: role
+            }
+          }
         }
       )
       when is_moderator(role) do
@@ -53,8 +65,9 @@ defmodule MobilizonWeb.Resolvers.User do
   """
   def login_user(_parent, %{email: email, password: password}, _resolution) do
     with {:ok, %User{} = user} <- Users.get_user_by_email(email, true),
-         {:ok, token, _} <- Users.authenticate(%{user: user, password: password}) do
-      {:ok, %{token: token, user: user}}
+         {:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
+           Users.authenticate(%{user: user, password: password}) do
+      {:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
     else
       {:error, :user_not_found} ->
         {:error, "User with email not found"}
@@ -64,6 +77,31 @@ defmodule MobilizonWeb.Resolvers.User do
     end
   end
 
+  @doc """
+  Refresh a token
+  """
+  def refresh_token(
+        _parent,
+        %{
+          refresh_token: refresh_token
+        },
+        _context
+      ) do
+    with {:ok, user, _claims} <- MobilizonWeb.Guardian.resource_from_token(refresh_token),
+         {:ok, _old, {exchanged_token, _claims}} <-
+           MobilizonWeb.Guardian.exchange(refresh_token, ["access", "refresh"], "access"),
+         {:ok, refresh_token} <- Users.generate_refresh_token(user) do
+      {:ok, %{access_token: exchanged_token, refresh_token: refresh_token}}
+    else
+      {:error, message} ->
+        Logger.debug("Cannot refresh user token: #{inspect(message)}")
+        {:error, "Cannot refresh the token"}
+    end
+  end
+
+  def refresh_token(_parent, _params, _context),
+    do: {:error, "You need to have an existing token to get a refresh token"}
+
   @doc """
   Register an user:
     - check registrations are enabled
@@ -92,9 +130,14 @@ defmodule MobilizonWeb.Resolvers.User do
     with {:check_confirmation_token, {:ok, %User{} = user}} <-
            {:check_confirmation_token, Activation.check_confirmation_token(token)},
          {:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)},
-         {:guardian_encode_and_sign, {:ok, token, _}} <-
-           {:guardian_encode_and_sign, MobilizonWeb.Guardian.encode_and_sign(user)} do
-      {:ok, %{token: token, user: Map.put(user, :default_actor, actor)}}
+         {:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
+           Users.generate_tokens(user) do
+      {:ok,
+       %{
+         access_token: access_token,
+         refresh_token: refresh_token,
+         user: Map.put(user, :default_actor, actor)
+       }}
     else
       err ->
         Logger.info("Unable to validate user with token #{token}")
@@ -145,15 +188,22 @@ defmodule MobilizonWeb.Resolvers.User do
   def reset_password(_parent, %{password: password, token: token}, _resolution) do
     with {:ok, %User{} = user} <-
            ResetPassword.check_reset_password_token(password, token),
-         {:ok, token, _} <- MobilizonWeb.Guardian.encode_and_sign(user) do
-      {:ok, %{token: token, user: user}}
+         {:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
+           Users.authenticate(%{user: user, password: password}) do
+      {:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
     end
   end
 
   @doc "Change an user default actor"
-  def change_default_actor(_parent, %{preferred_username: username}, %{
-        context: %{current_user: user}
-      }) do
+  def change_default_actor(
+        _parent,
+        %{preferred_username: username},
+        %{
+          context: %{
+            current_user: user
+          }
+        }
+      ) do
     with %Actor{id: actor_id} <- Actors.get_local_actor_by_name(username),
          {:user_actor, true} <-
            {:user_actor, actor_id in Enum.map(Users.get_actors_for_user(user), & &1.id)},
diff --git a/lib/mobilizon_web/schema.ex b/lib/mobilizon_web/schema.ex
index 38d223007..c3819ab75 100644
--- a/lib/mobilizon_web/schema.ex
+++ b/lib/mobilizon_web/schema.ex
@@ -31,7 +31,12 @@ defmodule MobilizonWeb.Schema do
 
   @desc "A JWT and the associated user ID"
   object :login do
-    field(:token, non_null(:string), description: "A JWT Token for this session")
+    field(:access_token, non_null(:string), description: "A JWT Token for this session")
+
+    field(:refresh_token, non_null(:string),
+      description: "A JWT Token to refresh the access token"
+    )
+
     field(:user, non_null(:user), description: "The user associated to this session")
   end
 
diff --git a/lib/mobilizon_web/schema/user.ex b/lib/mobilizon_web/schema/user.ex
index 86bbf8d73..1b3a525e6 100644
--- a/lib/mobilizon_web/schema/user.ex
+++ b/lib/mobilizon_web/schema/user.ex
@@ -45,6 +45,12 @@ defmodule MobilizonWeb.Schema.UserType do
     )
   end
 
+  @desc "Token"
+  object :refreshed_token do
+    field(:access_token, non_null(:string), description: "Generated access token")
+    field(:refresh_token, non_null(:string), description: "Generated refreshed token")
+  end
+
   @desc "Users list"
   object :users do
     field(:total, non_null(:integer), description: "Total elements")
@@ -118,12 +124,18 @@ defmodule MobilizonWeb.Schema.UserType do
     end
 
     @desc "Login an user"
-    field :login, :login do
+    field :login, type: :login do
       arg(:email, non_null(:string))
       arg(:password, non_null(:string))
       resolve(&User.login_user/3)
     end
 
+    @desc "Refresh a token"
+    field :refresh_token, type: :refreshed_token do
+      arg(:refresh_token, non_null(:string))
+      resolve(&User.refresh_token/3)
+    end
+
     @desc "Change default actor for user"
     field :change_default_actor, :user do
       arg(:preferred_username, non_null(:string))
diff --git a/mix.exs b/mix.exs
index a3e2c19d8..4357ae930 100644
--- a/mix.exs
+++ b/mix.exs
@@ -7,7 +7,7 @@ defmodule Mobilizon.Mixfile do
     [
       app: :mobilizon,
       version: @version,
-      elixir: "~> 1.9",
+      elixir: "~> 1.8",
       elixirc_paths: elixirc_paths(Mix.env()),
       compilers: [:phoenix, :gettext] ++ Mix.compilers(),
       start_permanent: Mix.env() == :prod,
diff --git a/test/mobilizon/users/users_test.exs b/test/mobilizon/users/users_test.exs
index a7ebceeb9..bb43805ab 100644
--- a/test/mobilizon/users/users_test.exs
+++ b/test/mobilizon/users/users_test.exs
@@ -68,7 +68,7 @@ defmodule Mobilizon.UsersTest do
     test "authenticate/1 checks the user's password" do
       {:ok, %User{} = user} = Users.register(%{email: @email, password: @password})
 
-      assert {:ok, _, _} = Users.authenticate(%{user: user, password: @password})
+      assert {:ok, _} = Users.authenticate(%{user: user, password: @password})
 
       assert {:error, :unauthorized} ==
                Users.authenticate(%{user: user, password: "bad password"})
diff --git a/test/mobilizon_web/resolvers/user_resolver_test.exs b/test/mobilizon_web/resolvers/user_resolver_test.exs
index dd96bd98c..b996d7ea9 100644
--- a/test/mobilizon_web/resolvers/user_resolver_test.exs
+++ b/test/mobilizon_web/resolvers/user_resolver_test.exs
@@ -3,6 +3,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
   alias Mobilizon.{Actors, Users, CommonConfig}
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Users.User
+  alias Mobilizon.Users
   alias MobilizonWeb.AbsintheHelpers
   alias Mobilizon.Service.Users.ResetPassword
   import Mobilizon.Factory
@@ -433,7 +434,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
             validateUser(
                   token: "#{user.confirmation_token}"
               ) {
-                token,
+                accessToken,
                 user {
                   id,
                 },
@@ -456,7 +457,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
             validateUser(
                   token: "no pass"
               ) {
-                token,
+                accessToken,
                 user {
                   id
                 },
@@ -641,7 +642,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
     end
   end
 
-  describe "Resolver: Login an user" do
+  describe "Resolver: Login a user" do
     test "test login_user/3 with valid credentials", context do
       {:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"})
 
@@ -658,7 +659,8 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
                   email: "#{user.email}",
                   password: "#{user.password}",
               ) {
-                token,
+                accessToken,
+                refreshToken,
                 user {
                   id
                 }
@@ -671,7 +673,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
         |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
 
       assert login = json_response(res, 200)["data"]["login"]
-      assert Map.has_key?(login, "token") && not is_nil(login["token"])
+      assert Map.has_key?(login, "accessToken") && not is_nil(login["accessToken"])
     end
 
     test "test login_user/3 with invalid password", context do
@@ -690,7 +692,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
                   email: "#{user.email}",
                   password: "bad password",
               ) {
-                token,
+                accessToken,
                 user {
                   default_actor {
                     preferred_username,
@@ -715,7 +717,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
                   email: "bad email",
                   password: "bad password",
               ) {
-                token,
+                accessToken,
                 user {
                   default_actor {
                     preferred_username,
@@ -733,6 +735,66 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
     end
   end
 
+  describe "Resolver: Refresh a token" do
+    test "test refresh_token/3 with a bad token", context do
+      mutation = """
+          mutation {
+            refreshToken(
+              refreshToken: "bad_token"
+            ) {
+              accessToken
+            }
+          }
+      """
+
+      res =
+        context.conn
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] ==
+               "Cannot refresh the token"
+    end
+
+    test "test refresh_token/3 with an appropriate token", context do
+      user = insert(:user)
+      {:ok, refresh_token} = Users.generate_refresh_token(user)
+
+      mutation = """
+          mutation {
+            refreshToken(
+              refreshToken: "#{refresh_token}"
+            ) {
+              accessToken
+            }
+          }
+      """
+
+      res =
+        context.conn
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert json_response(res, 200)["errors"] == nil
+
+      access_token = json_response(res, 200)["data"]["refreshToken"]["accessToken"]
+      assert String.length(access_token) > 10
+
+      query = """
+      {
+          loggedPerson {
+            preferredUsername,
+          }
+        }
+      """
+
+      res =
+        context.conn
+        |> Plug.Conn.put_req_header("authorization", "Bearer #{access_token}")
+        |> post("/api", AbsintheHelpers.query_skeleton(query, "logged_person"))
+
+      assert json_response(res, 200)["errors"] == nil
+    end
+  end
+
   describe "Resolver: change default actor for user" do
     test "test change_default_actor/3 with valid actor", context do
       # Prepare user with two actors