Handle token refreshing

This commit is contained in:
dominik-zeglen 2020-07-23 15:37:39 +02:00
parent 765953c9a8
commit d23202bf00
15 changed files with 220 additions and 134 deletions

View file

@ -8,11 +8,11 @@
}, },
"entries": [ "entries": [
{ {
"_id": "5a18fed13283ef12aa42d45d206a87bd", "_id": "ec3f8ec4f0f88fc421dc0d46281d4515",
"_order": 0, "_order": 0,
"cache": {}, "cache": {},
"request": { "request": {
"bodySize": 572, "bodySize": 587,
"cookies": [], "cookies": [],
"headers": [ "headers": [
{ {
@ -28,7 +28,7 @@
{ {
"_fromType": "array", "_fromType": "array",
"name": "content-length", "name": "content-length",
"value": "572" "value": "587"
}, },
{ {
"_fromType": "array", "_fromType": "array",
@ -56,33 +56,33 @@
"postData": { "postData": {
"mimeType": "application/json", "mimeType": "application/json",
"params": [], "params": [],
"text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"admin\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n token\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" "text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"admin\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
}, },
"queryString": [], "queryString": [],
"url": "http://localhost:8000/graphql/" "url": "http://localhost:8000/graphql/"
}, },
"response": { "response": {
"bodySize": 1749, "bodySize": 1830,
"content": { "content": {
"mimeType": "application/json", "mimeType": "application/json",
"size": 1749, "size": 1830,
"text": "[{\"data\": {\"tokenCreate\": {\"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTUzMzk3NDIsImV4cCI6MTU5NTM0MDA0MiwidG9rZW4iOiJxQ1Jia0dOMnpOT28iLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU1RPT0iLCJpc19zdGFmZiI6dHJ1ZX0.hZ2hTVOU8h7fMf--Qm891DpV1hssicEaQShyy4sPKXM\", \"errors\": [], \"user\": {\"id\": \"VXNlcjoyMQ==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}]" "text": "[{\"data\": {\"tokenCreate\": {\"errors\": [], \"csrfToken\": \"oQ3yHRrBRRtQNVuJgn4D6Txh3aPWC8fl91XMcA2bukbgkdUotAEAJbAcCvTsXn3Z\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTU0MDk1ODUsImV4cCI6MTU5NTQwOTU5MCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU1RPT0iLCJpc19zdGFmZiI6dHJ1ZX0.jv57hd44KxRNMKO6IcP5bD7Upg2rnZ1fzXmsk0yAZDg\", \"user\": {\"id\": \"VXNlcjoyMQ==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}]"
}, },
"cookies": [ "cookies": [
{ {
"expires": "2020-08-20T13:55:42.000Z", "expires": "2020-07-22T09:49:45.000Z",
"httpOnly": true, "httpOnly": true,
"maxAge": 2592000, "maxAge": 1800,
"name": "refreshToken", "name": "refreshToken",
"path": "/", "path": "/",
"secure": true, "secure": true,
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTUzMzk3NDIsImV4cCI6MTU5NzkzMTc0MiwidG9rZW4iOiJxQ1Jia0dOMnpOT28iLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6ImZiRzlQR3FtR2dLZnRlTEJZNHIycW9kV0J0djczeDF6SEtnM3JVRXQweXdZT081bVk0dGRFcE9DRVYxVGpUREoifQ.OmbZJ6T-gseVTakIUNv2IP0rZ_t-W6TmV3Z99jiPF64" "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTU0MDk1ODUsImV4cCI6MTU5NTQxMTM4NSwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6Im9RM3lIUnJCUlJ0UU5WdUpnbjRENlR4aDNhUFdDOGZsOTFYTWNBMmJ1a2Jna2RVb3RBRUFKYkFjQ3ZUc1huM1oifQ.KeIbqoHp-XPFfny6tlq6EUA7BojWAnNcpePMtI2X4qo"
} }
], ],
"headers": [ "headers": [
{ {
"name": "date", "name": "date",
"value": "Tue, 21 Jul 2020 13:55:42 GMT" "value": "Wed, 22 Jul 2020 09:19:45 GMT"
}, },
{ {
"name": "server", "name": "server",
@ -106,7 +106,7 @@
}, },
{ {
"name": "content-length", "name": "content-length",
"value": "1749" "value": "1830"
}, },
{ {
"name": "x-content-type-options", "name": "x-content-type-options",
@ -115,17 +115,17 @@
{ {
"_fromType": "array", "_fromType": "array",
"name": "set-cookie", "name": "set-cookie",
"value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTUzMzk3NDIsImV4cCI6MTU5NzkzMTc0MiwidG9rZW4iOiJxQ1Jia0dOMnpOT28iLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6ImZiRzlQR3FtR2dLZnRlTEJZNHIycW9kV0J0djczeDF6SEtnM3JVRXQweXdZT081bVk0dGRFcE9DRVYxVGpUREoifQ.OmbZJ6T-gseVTakIUNv2IP0rZ_t-W6TmV3Z99jiPF64; expires=Thu, 20 Aug 2020 13:55:42 GMT; HttpOnly; Max-Age=2592000; Path=/; Secure" "value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTU0MDk1ODUsImV4cCI6MTU5NTQxMTM4NSwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6Im9RM3lIUnJCUlJ0UU5WdUpnbjRENlR4aDNhUFdDOGZsOTFYTWNBMmJ1a2Jna2RVb3RBRUFKYkFjQ3ZUc1huM1oifQ.KeIbqoHp-XPFfny6tlq6EUA7BojWAnNcpePMtI2X4qo; expires=Wed, 22 Jul 2020 09:49:45 GMT; HttpOnly; Max-Age=1800; Path=/; Secure"
} }
], ],
"headersSize": 804, "headersSize": 801,
"httpVersion": "HTTP/1.1", "httpVersion": "HTTP/1.1",
"redirectURL": "", "redirectURL": "",
"status": 200, "status": 200,
"statusText": "OK" "statusText": "OK"
}, },
"startedDateTime": "2020-07-21T13:55:42.298Z", "startedDateTime": "2020-07-22T09:19:45.127Z",
"time": 198, "time": 324,
"timings": { "timings": {
"blocked": -1, "blocked": -1,
"connect": -1, "connect": -1,
@ -133,7 +133,7 @@
"receive": 0, "receive": 0,
"send": 0, "send": 0,
"ssl": -1, "ssl": -1,
"wait": 198 "wait": 324
} }
} }
], ],

View file

@ -8,11 +8,11 @@
}, },
"entries": [ "entries": [
{ {
"_id": "482243508a181587879cd204a88524d7", "_id": "29fb7ad4777c005f81fdfd957c1c81af",
"_order": 0, "_order": 0,
"cache": {}, "cache": {},
"request": { "request": {
"bodySize": 1263, "bodySize": 588,
"cookies": [], "cookies": [],
"headers": [ "headers": [
{ {
@ -28,7 +28,7 @@
{ {
"_fromType": "array", "_fromType": "array",
"name": "content-length", "name": "content-length",
"value": "1263" "value": "588"
}, },
{ {
"_fromType": "array", "_fromType": "array",
@ -50,29 +50,29 @@
"value": "localhost:8000" "value": "localhost:8000"
} }
], ],
"headersSize": 255, "headersSize": 254,
"httpVersion": "HTTP/1.1", "httpVersion": "HTTP/1.1",
"method": "POST", "method": "POST",
"postData": { "postData": {
"mimeType": "application/json", "mimeType": "application/json",
"params": [], "params": [],
"text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTUzMzk3NDIsImV4cCI6MTU5NTM0MDA0MiwidG9rZW4iOiJxQ1Jia0dOMnpOT28iLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU1RPT0iLCJpc19zdGFmZiI6dHJ1ZX0.hZ2hTVOU8h7fMf--Qm891DpV1hssicEaQShyy4sPKXM\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"},{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"admin1\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n token\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" "text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"admin1\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
}, },
"queryString": [], "queryString": [],
"url": "http://localhost:8000/graphql/" "url": "http://localhost:8000/graphql/"
}, },
"response": { "response": {
"bodySize": 1814, "bodySize": 214,
"content": { "content": {
"mimeType": "application/json", "mimeType": "application/json",
"size": 1814, "size": 214,
"text": "[{\"data\": {\"tokenVerify\": {\"payload\": {\"iat\": 1595339742, \"exp\": 1595340042, \"token\": \"qCRbkGN2zNOo\", \"email\": \"admin@example.com\", \"type\": \"access\", \"user_id\": \"VXNlcjoyMQ==\", \"is_staff\": true}, \"user\": {\"id\": \"VXNlcjoyMQ==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"VerifyToken\"}}}, {\"data\": {\"tokenCreate\": {\"token\": null, \"errors\": [{\"field\": \"email\", \"message\": \"Please, enter valid credentials\", \"__typename\": \"AccountError\"}], \"user\": null, \"__typename\": \"CreateToken\"}}}]" "text": "[{\"data\": {\"tokenCreate\": {\"errors\": [{\"field\": \"email\", \"message\": \"Please, enter valid credentials\", \"__typename\": \"AccountError\"}], \"csrfToken\": null, \"token\": null, \"user\": null, \"__typename\": \"CreateToken\"}}}]"
}, },
"cookies": [], "cookies": [],
"headers": [ "headers": [
{ {
"name": "date", "name": "date",
"value": "Tue, 21 Jul 2020 13:55:42 GMT" "value": "Wed, 22 Jul 2020 09:21:11 GMT"
}, },
{ {
"name": "server", "name": "server",
@ -96,21 +96,21 @@
}, },
{ {
"name": "content-length", "name": "content-length",
"value": "1814" "value": "214"
}, },
{ {
"name": "x-content-type-options", "name": "x-content-type-options",
"value": "nosniff" "value": "nosniff"
} }
], ],
"headersSize": 316, "headersSize": 315,
"httpVersion": "HTTP/1.1", "httpVersion": "HTTP/1.1",
"redirectURL": "", "redirectURL": "",
"status": 200, "status": 200,
"statusText": "OK" "statusText": "OK"
}, },
"startedDateTime": "2020-07-21T13:55:42.541Z", "startedDateTime": "2020-07-22T09:21:11.006Z",
"time": 185, "time": 363,
"timings": { "timings": {
"blocked": -1, "blocked": -1,
"connect": -1, "connect": -1,
@ -118,7 +118,7 @@
"receive": 0, "receive": 0,
"send": 0, "send": 0,
"ssl": -1, "ssl": -1,
"wait": 185 "wait": 363
} }
} }
], ],

View file

@ -29,6 +29,7 @@ const credentials = {
beforeEach(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
sessionStorage.clear();
}); });
describe("User", () => { describe("User", () => {

View file

@ -24,9 +24,10 @@ import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken"; import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
import { import {
displayDemoMessage, displayDemoMessage,
getAuthToken, getTokens,
removeAuthToken, removeTokens,
setAuthToken setAuthToken,
setTokens
} from "./utils"; } from "./utils";
const persistToken = false; const persistToken = false;
@ -38,13 +39,14 @@ export function useAuthProvider(
) { ) {
const [userContext, setUserContext] = useState<undefined | User>(undefined); const [userContext, setUserContext] = useState<undefined | User>(undefined);
const autologinPromise = useRef<Promise<any>>(); const autologinPromise = useRef<Promise<any>>();
const refreshPromise = useRef<Promise<boolean>>();
const logout = () => { const logout = () => {
setUserContext(undefined); setUserContext(undefined);
if (isCredentialsManagementAPISupported) { if (isCredentialsManagementAPISupported) {
navigator.credentials.preventSilentAccess(); navigator.credentials.preventSilentAccess();
} }
removeAuthToken(); removeTokens();
}; };
const [tokenAuth, tokenAuthResult] = useMutation< const [tokenAuth, tokenAuthResult] = useMutation<
@ -63,7 +65,11 @@ export function useAuthProvider(
// `null`, because the LoginView uses this `null` to display error. // `null`, because the LoginView uses this `null` to display error.
setUserContext(user); setUserContext(user);
if (user) { if (user) {
setAuthToken(result.tokenCreate.token, persistToken); setTokens(
result.tokenCreate.token,
result.tokenCreate.csrfToken,
persistToken
);
} }
}, },
onError: logout onError: logout
@ -110,7 +116,7 @@ export function useAuthProvider(
}; };
useEffect(() => { useEffect(() => {
const token = getAuthToken(); const token = getTokens().auth;
if (!!token && !userContext) { if (!!token && !userContext) {
autologinPromise.current = tokenVerify({ variables: { token } }); autologinPromise.current = tokenVerify({ variables: { token } });
} else { } else {
@ -133,17 +139,28 @@ export function useAuthProvider(
return null; return null;
}; };
const loginByToken = (token: string, user: User) => { const loginByToken = (auth: string, refresh: string, user: User) => {
setUserContext(user); setUserContext(user);
setAuthToken(token, persistToken); setTokens(auth, refresh, persistToken);
}; };
const refreshToken = async () => { const refreshToken = (): Promise<boolean> => {
const token = getAuthToken(); if (!!refreshPromise.current) {
return refreshPromise.current;
}
const refreshData = await tokenRefresh({ variables: { token } }); return new Promise(resolve => {
const token = getTokens().refresh;
setAuthToken(refreshData.data.tokenRefresh.token, persistToken); return tokenRefresh({ variables: { token } }).then(refreshData => {
if (!!refreshData.data.tokenRefresh?.token) {
setAuthToken(refreshData.data.tokenRefresh.token, persistToken);
return resolve(true);
}
return resolve(false);
});
});
}; };
return { return {
@ -199,7 +216,7 @@ export const useAuth = () => {
const isAuthenticated = !!user.user; const isAuthenticated = !!user.user;
return { return {
hasToken: !!getAuthToken(), hasToken: !!getTokens(),
isAuthenticated, isAuthenticated,
tokenAuthLoading: user.tokenAuthLoading, tokenAuthLoading: user.tokenAuthLoading,
tokenVerifyLoading: user.tokenVerifyLoading, tokenVerifyLoading: user.tokenVerifyLoading,

View file

@ -11,6 +11,10 @@ export function isJwtError(error: GraphQLError): boolean {
return !!findValueInEnum(error.extensions.exception.code, JWTError); return !!findValueInEnum(error.extensions.exception.code, JWTError);
} }
export function isJwtExpiredError(error: GraphQLError): boolean {
return error.extensions.exception.code === JWTError.expired;
}
export function isTokenExpired(error: GraphQLError): boolean { export function isTokenExpired(error: GraphQLError): boolean {
return error.extensions.exception.code === JWTError.expired; return error.extensions.exception.code === JWTError.expired;
} }

View file

@ -15,10 +15,10 @@ import ResetPasswordSuccess from "./views/ResetPasswordSuccess";
interface UserContext { interface UserContext {
login: (username: string, password: string) => void; login: (username: string, password: string) => void;
loginByToken: (token: string, user: User) => void; loginByToken: (auth: string, csrf: string, user: User) => void;
logout: () => void; logout: () => void;
tokenAuthLoading: boolean; tokenAuthLoading: boolean;
tokenRefresh: () => Promise<void>; tokenRefresh: () => Promise<boolean>;
tokenVerifyLoading: boolean; tokenVerifyLoading: boolean;
user?: User; user?: User;
} }

39
src/auth/link.ts Normal file
View file

@ -0,0 +1,39 @@
import { setContext } from "apollo-link-context";
import { ErrorResponse, onError } from "apollo-link-error";
import { getTokens, removeTokens } from "./";
import { isJwtError, JWTError } from "./errors";
interface ResponseError extends ErrorResponse {
networkError?: Error & {
statusCode?: number;
bodyText?: string;
};
}
export const invalidTokenLink = onError((error: ResponseError) => {
if (
(error.networkError && error.networkError.statusCode === 401) ||
error.graphQLErrors?.some(isJwtError)
) {
if (error.graphQLErrors[0].extensions.code !== JWTError.expired) {
removeTokens();
}
}
});
export const tokenLink = setContext((_, context) => {
const authToken = getTokens().auth;
return {
...context,
headers: {
...context.headers,
Authorization: authToken ? `JWT ${authToken}` : null
}
};
});
const link = invalidTokenLink.concat(tokenLink);
export default link;

View file

@ -13,11 +13,12 @@ export const tokenAuthMutation = gql`
${fragmentUser} ${fragmentUser}
mutation TokenAuth($email: String!, $password: String!) { mutation TokenAuth($email: String!, $password: String!) {
tokenCreate(email: $email, password: $password) { tokenCreate(email: $email, password: $password) {
token
errors: accountErrors { errors: accountErrors {
field field
message message
} }
csrfToken
token
user { user {
...User ...User
} }
@ -68,6 +69,8 @@ export const setPassword = gql`
errors: accountErrors { errors: accountErrors {
...AccountErrorFragment ...AccountErrorFragment
} }
csrfToken
refreshToken
token token
user { user {
...User ...User

View file

@ -38,6 +38,8 @@ export interface SetPassword_setPassword_user {
export interface SetPassword_setPassword { export interface SetPassword_setPassword {
__typename: "SetPassword"; __typename: "SetPassword";
errors: SetPassword_setPassword_errors[]; errors: SetPassword_setPassword_errors[];
csrfToken: string | null;
refreshToken: string | null;
token: string | null; token: string | null;
user: SetPassword_setPassword_user | null; user: SetPassword_setPassword_user | null;
} }

View file

@ -37,8 +37,9 @@ export interface TokenAuth_tokenCreate_user {
export interface TokenAuth_tokenCreate { export interface TokenAuth_tokenCreate {
__typename: "CreateToken"; __typename: "CreateToken";
token: string | null;
errors: TokenAuth_tokenCreate_errors[]; errors: TokenAuth_tokenCreate_errors[];
csrfToken: string | null;
token: string | null;
user: TokenAuth_tokenCreate_user | null; user: TokenAuth_tokenCreate_user | null;
} }

View file

@ -2,20 +2,43 @@ import { UseNotifierResult } from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { IntlShape } from "react-intl"; import { IntlShape } from "react-intl";
const TOKEN_STORAGE_KEY = "dashboardAuth"; export enum TOKEN_STORAGE_KEY {
AUTH = "auth",
CSRF = "csrf"
}
export const getAuthToken = () => export const getTokens = () => ({
localStorage.getItem(TOKEN_STORAGE_KEY) || auth:
sessionStorage.getItem(TOKEN_STORAGE_KEY); localStorage.getItem(TOKEN_STORAGE_KEY.AUTH) ||
sessionStorage.getItem(TOKEN_STORAGE_KEY.AUTH),
refresh:
localStorage.getItem(TOKEN_STORAGE_KEY.CSRF) ||
sessionStorage.getItem(TOKEN_STORAGE_KEY.CSRF)
});
export const setAuthToken = (token: string, persist: boolean) => export const setTokens = (auth: string, csrf: string, persist: boolean) => {
persist if (persist) {
? localStorage.setItem(TOKEN_STORAGE_KEY, token) localStorage.setItem(TOKEN_STORAGE_KEY.AUTH, auth);
: sessionStorage.setItem(TOKEN_STORAGE_KEY, token); localStorage.setItem(TOKEN_STORAGE_KEY.CSRF, csrf);
} else {
sessionStorage.setItem(TOKEN_STORAGE_KEY.AUTH, auth);
sessionStorage.setItem(TOKEN_STORAGE_KEY.CSRF, csrf);
}
};
export const removeAuthToken = () => { export const setAuthToken = (auth: string, persist: boolean) => {
localStorage.removeItem(TOKEN_STORAGE_KEY); if (persist) {
sessionStorage.removeItem(TOKEN_STORAGE_KEY); localStorage.setItem(TOKEN_STORAGE_KEY.AUTH, auth);
} else {
sessionStorage.setItem(TOKEN_STORAGE_KEY.AUTH, auth);
}
};
export const removeTokens = () => {
localStorage.removeItem(TOKEN_STORAGE_KEY.AUTH);
// localStorage.removeItem(TOKEN_STORAGE_KEY.CSRF);
sessionStorage.removeItem(TOKEN_STORAGE_KEY.AUTH);
// sessionStorage.removeItem(TOKEN_STORAGE_KEY.CSRF);
}; };
export const displayDemoMessage = ( export const displayDemoMessage = (

View file

@ -19,7 +19,11 @@ const NewPassword: React.FC<RouteComponentProps> = ({ location }) => {
const handleSetPassword = async (data: SetPassword) => { const handleSetPassword = async (data: SetPassword) => {
if (data.setPassword.errors.length === 0) { if (data.setPassword.errors.length === 0) {
loginByToken(data.setPassword.token, data.setPassword.user); loginByToken(
data.setPassword.token,
data.setPassword.csrfToken,
data.setPassword.user
);
navigate("/", true); navigate("/", true);
} }
}; };

View file

@ -1,7 +1,7 @@
import { isJwtError } from "@saleor/auth/errors"; import { isJwtError, isJwtExpiredError } from "@saleor/auth/errors";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { maybe, RequireAtLeastOne } from "@saleor/misc"; import { maybe, RequireAtLeastOne } from "@saleor/misc";
import { ApolloQueryResult } from "apollo-client"; import { ApolloError, ApolloQueryResult } from "apollo-client";
import { DocumentNode } from "graphql"; import { DocumentNode } from "graphql";
import { useEffect } from "react"; import { useEffect } from "react";
import { QueryResult, useQuery as useBaseQuery } from "react-apollo"; import { QueryResult, useQuery as useBaseQuery } from "react-apollo";
@ -48,6 +48,37 @@ function makeQuery<TData, TVariables>(
}, },
errorPolicy: "all", errorPolicy: "all",
fetchPolicy: "cache-and-network", fetchPolicy: "cache-and-network",
onError: async (error: ApolloError) => {
if (error.graphQLErrors.some(isJwtError)) {
if (error.graphQLErrors.every(isJwtExpiredError)) {
const success = await user.tokenRefresh();
if (!success) {
user.logout();
notify({
status: "error",
text: intl.formatMessage(commonMessages.sessionExpired)
});
}
} else {
user.logout();
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
} else if (
!error.graphQLErrors.every(
err =>
maybe(() => err.extensions.exception.code) === "PermissionDenied"
)
) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
},
skip, skip,
variables variables
}); });
@ -63,26 +94,6 @@ function makeQuery<TData, TVariables>(
} }
}, [queryData.loading]); }, [queryData.loading]);
if (queryData.error) {
if (queryData.error.graphQLErrors.some(isJwtError)) {
user.logout();
notify({
status: "error",
text: intl.formatMessage(commonMessages.sessionExpired)
});
} else if (
!queryData.error.graphQLErrors.every(
err =>
maybe(() => err.extensions.exception.code) === "PermissionDenied"
)
) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
}
const loadMore = ( const loadMore = (
mergeFunc: (previousResults: TData, fetchMoreResult: TData) => TData, mergeFunc: (previousResults: TData, fetchMoreResult: TData) => TData,
extraVariables: RequireAtLeastOne<TVariables> extraVariables: RequireAtLeastOne<TVariables>

View file

@ -4,8 +4,6 @@ import { defaultDataIdFromObject, InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client"; import { ApolloClient } from "apollo-client";
import { ApolloLink } from "apollo-link"; import { ApolloLink } from "apollo-link";
import { BatchHttpLink } from "apollo-link-batch-http"; import { BatchHttpLink } from "apollo-link-batch-http";
import { setContext } from "apollo-link-context";
import { ErrorResponse, onError } from "apollo-link-error";
import { createUploadLink } from "apollo-upload-client"; import { createUploadLink } from "apollo-upload-client";
import React from "react"; import React from "react";
import { ApolloProvider } from "react-apollo"; import { ApolloProvider } from "react-apollo";
@ -19,11 +17,11 @@ import AppsSection from "./apps";
import { appsSection } from "./apps/urls"; import { appsSection } from "./apps/urls";
import AttributeSection from "./attributes"; import AttributeSection from "./attributes";
import { attributeSection } from "./attributes/urls"; import { attributeSection } from "./attributes/urls";
import Auth, { getAuthToken, removeAuthToken } from "./auth"; import Auth from "./auth";
import AuthProvider, { useAuth } from "./auth/AuthProvider"; import AuthProvider, { useAuth } from "./auth/AuthProvider";
import LoginLoading from "./auth/components/LoginLoading/LoginLoading"; import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
import SectionRoute from "./auth/components/SectionRoute"; import SectionRoute from "./auth/components/SectionRoute";
import { isJwtError } from "./auth/errors"; import authLink from "./auth/link";
import { hasPermission } from "./auth/misc"; import { hasPermission } from "./auth/misc";
import CategorySection from "./categories"; import CategorySection from "./categories";
import CollectionSection from "./collections"; import CollectionSection from "./collections";
@ -60,43 +58,15 @@ import { PermissionEnum } from "./types/globalTypes";
import WarehouseSection from "./warehouses"; import WarehouseSection from "./warehouses";
import { warehouseSection } from "./warehouses/urls"; import { warehouseSection } from "./warehouses/urls";
interface ResponseError extends ErrorResponse {
networkError?: Error & {
statusCode?: number;
bodyText?: string;
};
}
if (process.env.GTM_ID !== undefined) { if (process.env.GTM_ID !== undefined) {
TagManager.initialize({ gtmId: GTM_ID }); TagManager.initialize({ gtmId: GTM_ID });
} }
const invalidTokenLink = onError((error: ResponseError) => {
if (
(error.networkError && error.networkError.statusCode === 401) ||
error.graphQLErrors?.some(isJwtError)
) {
removeAuthToken();
}
});
const authLink = setContext((_, context) => {
const authToken = getAuthToken();
return {
...context,
headers: {
...context.headers,
Authorization: authToken ? `JWT ${authToken}` : null
}
};
});
// DON'T TOUCH THIS // DON'T TOUCH THIS
// These are separate clients and do not share configs between themselves // These are separate clients and do not share configs between themselves
// so we need to explicitly set them // so we need to explicitly set them
const linkOptions = { const linkOptions = {
credentials: "same-origin", credentials: "include",
uri: API_URI uri: API_URI
}; };
const uploadLink = createUploadLink(linkOptions); const uploadLink = createUploadLink(linkOptions);
@ -122,7 +92,7 @@ const apolloClient = new ApolloClient({
return defaultDataIdFromObject(obj); return defaultDataIdFromObject(obj);
} }
}), }),
link: invalidTokenLink.concat(authLink.concat(link)) link: authLink.concat(link)
}); });
const App: React.FC = () => { const App: React.FC = () => {

View file

@ -1,10 +1,10 @@
import { ApolloQueryResult } from "apollo-client"; import { ApolloError, ApolloQueryResult } from "apollo-client";
import { DocumentNode } from "graphql"; import { DocumentNode } from "graphql";
import React from "react"; import React from "react";
import { Query, QueryResult } from "react-apollo"; import { Query, QueryResult } from "react-apollo";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { isJwtError } from "./auth/errors"; import { isJwtError, isJwtExpiredError } from "./auth/errors";
import useAppState from "./hooks/useAppState"; import useAppState from "./hooks/useAppState";
import useNotifier from "./hooks/useNotifier"; import useNotifier from "./hooks/useNotifier";
import useUser from "./hooks/useUser"; import useUser from "./hooks/useUser";
@ -79,29 +79,40 @@ export function TypedQuery<TData, TVariables>(
skip={skip} skip={skip}
context={{ useBatching: true }} context={{ useBatching: true }}
errorPolicy="all" errorPolicy="all"
> onError={async (error: ApolloError) => {
{(queryData: QueryResult<TData, TVariables>) => { if (error.graphQLErrors.some(isJwtError)) {
if (queryData.error) { if (error.graphQLErrors.every(isJwtExpiredError)) {
if (queryData.error.graphQLErrors.some(isJwtError)) { const success = await user.tokenRefresh();
if (!success) {
user.logout();
notify({
status: "error",
text: intl.formatMessage(commonMessages.sessionExpired)
});
}
} else {
user.logout(); user.logout();
notify({
status: "error",
text: intl.formatMessage(commonMessages.sessionExpired)
});
} else if (
!queryData.error.graphQLErrors.every(
err =>
maybe(() => err.extensions.exception.code) ===
"PermissionDenied"
)
) {
notify({ notify({
status: "error", status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong) text: intl.formatMessage(commonMessages.somethingWentWrong)
}); });
} }
} else if (
!error.graphQLErrors.every(
err =>
maybe(() => err.extensions.exception.code) ===
"PermissionDenied"
)
) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
} }
}}
>
{(queryData: QueryResult<TData, TVariables>) => {
const loadMore = ( const loadMore = (
mergeFunc: ( mergeFunc: (
previousResults: TData, previousResults: TData,