diff --git a/recordings/User_3768991250/will-be-logged-in-if-has-valid-credentials_3587751314/recording.har b/recordings/User_3768991250/will-be-logged-in-if-has-valid-credentials_3587751314/recording.har index 8f337cb51..f02c738f0 100644 --- a/recordings/User_3768991250/will-be-logged-in-if-has-valid-credentials_3587751314/recording.har +++ b/recordings/User_3768991250/will-be-logged-in-if-has-valid-credentials_3587751314/recording.har @@ -8,11 +8,11 @@ }, "entries": [ { - "_id": "5a18fed13283ef12aa42d45d206a87bd", + "_id": "ec3f8ec4f0f88fc421dc0d46281d4515", "_order": 0, "cache": {}, "request": { - "bodySize": 572, + "bodySize": 587, "cookies": [], "headers": [ { @@ -28,7 +28,7 @@ { "_fromType": "array", "name": "content-length", - "value": "572" + "value": "587" }, { "_fromType": "array", @@ -56,33 +56,33 @@ "postData": { "mimeType": "application/json", "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": [], "url": "http://localhost:8000/graphql/" }, "response": { - "bodySize": 1749, + "bodySize": 1830, "content": { "mimeType": "application/json", - "size": 1749, - "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\"}}}]" + "size": 1830, + "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": [ { - "expires": "2020-08-20T13:55:42.000Z", + "expires": "2020-07-22T09:49:45.000Z", "httpOnly": true, - "maxAge": 2592000, + "maxAge": 1800, "name": "refreshToken", "path": "/", "secure": true, - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTUzMzk3NDIsImV4cCI6MTU5NzkzMTc0MiwidG9rZW4iOiJxQ1Jia0dOMnpOT28iLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6ImZiRzlQR3FtR2dLZnRlTEJZNHIycW9kV0J0djczeDF6SEtnM3JVRXQweXdZT081bVk0dGRFcE9DRVYxVGpUREoifQ.OmbZJ6T-gseVTakIUNv2IP0rZ_t-W6TmV3Z99jiPF64" + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTU0MDk1ODUsImV4cCI6MTU5NTQxMTM4NSwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6Im9RM3lIUnJCUlJ0UU5WdUpnbjRENlR4aDNhUFdDOGZsOTFYTWNBMmJ1a2Jna2RVb3RBRUFKYkFjQ3ZUc1huM1oifQ.KeIbqoHp-XPFfny6tlq6EUA7BojWAnNcpePMtI2X4qo" } ], "headers": [ { "name": "date", - "value": "Tue, 21 Jul 2020 13:55:42 GMT" + "value": "Wed, 22 Jul 2020 09:19:45 GMT" }, { "name": "server", @@ -106,7 +106,7 @@ }, { "name": "content-length", - "value": "1749" + "value": "1830" }, { "name": "x-content-type-options", @@ -115,17 +115,17 @@ { "_fromType": "array", "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", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2020-07-21T13:55:42.298Z", - "time": 198, + "startedDateTime": "2020-07-22T09:19:45.127Z", + "time": 324, "timings": { "blocked": -1, "connect": -1, @@ -133,7 +133,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 198 + "wait": 324 } } ], diff --git a/recordings/User_3768991250/will-not-be-logged-in-if-doesn-t-have-valid-credentials_3719199657/recording.har b/recordings/User_3768991250/will-not-be-logged-in-if-doesn-t-have-valid-credentials_3719199657/recording.har index d2c6bb9b2..4673d4895 100644 --- a/recordings/User_3768991250/will-not-be-logged-in-if-doesn-t-have-valid-credentials_3719199657/recording.har +++ b/recordings/User_3768991250/will-not-be-logged-in-if-doesn-t-have-valid-credentials_3719199657/recording.har @@ -8,11 +8,11 @@ }, "entries": [ { - "_id": "482243508a181587879cd204a88524d7", + "_id": "29fb7ad4777c005f81fdfd957c1c81af", "_order": 0, "cache": {}, "request": { - "bodySize": 1263, + "bodySize": 588, "cookies": [], "headers": [ { @@ -28,7 +28,7 @@ { "_fromType": "array", "name": "content-length", - "value": "1263" + "value": "588" }, { "_fromType": "array", @@ -50,29 +50,29 @@ "value": "localhost:8000" } ], - "headersSize": 255, + "headersSize": 254, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { "mimeType": "application/json", "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": [], "url": "http://localhost:8000/graphql/" }, "response": { - "bodySize": 1814, + "bodySize": 214, "content": { "mimeType": "application/json", - "size": 1814, - "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\"}}}]" + "size": 214, + "text": "[{\"data\": {\"tokenCreate\": {\"errors\": [{\"field\": \"email\", \"message\": \"Please, enter valid credentials\", \"__typename\": \"AccountError\"}], \"csrfToken\": null, \"token\": null, \"user\": null, \"__typename\": \"CreateToken\"}}}]" }, "cookies": [], "headers": [ { "name": "date", - "value": "Tue, 21 Jul 2020 13:55:42 GMT" + "value": "Wed, 22 Jul 2020 09:21:11 GMT" }, { "name": "server", @@ -96,21 +96,21 @@ }, { "name": "content-length", - "value": "1814" + "value": "214" }, { "name": "x-content-type-options", "value": "nosniff" } ], - "headersSize": 316, + "headersSize": 315, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2020-07-21T13:55:42.541Z", - "time": 185, + "startedDateTime": "2020-07-22T09:21:11.006Z", + "time": 363, "timings": { "blocked": -1, "connect": -1, @@ -118,7 +118,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 185 + "wait": 363 } } ], diff --git a/src/auth/AuthProvider.test.ts b/src/auth/AuthProvider.test.ts index 3ec9c9570..8a55ae399 100644 --- a/src/auth/AuthProvider.test.ts +++ b/src/auth/AuthProvider.test.ts @@ -29,6 +29,7 @@ const credentials = { beforeEach(() => { localStorage.clear(); + sessionStorage.clear(); }); describe("User", () => { diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index a059e4aa6..ad83dcea8 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -24,9 +24,10 @@ import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth"; import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken"; import { displayDemoMessage, - getAuthToken, - removeAuthToken, - setAuthToken + getTokens, + removeTokens, + setAuthToken, + setTokens } from "./utils"; const persistToken = false; @@ -38,13 +39,14 @@ export function useAuthProvider( ) { const [userContext, setUserContext] = useState(undefined); const autologinPromise = useRef>(); + const refreshPromise = useRef>(); const logout = () => { setUserContext(undefined); if (isCredentialsManagementAPISupported) { navigator.credentials.preventSilentAccess(); } - removeAuthToken(); + removeTokens(); }; const [tokenAuth, tokenAuthResult] = useMutation< @@ -63,7 +65,11 @@ export function useAuthProvider( // `null`, because the LoginView uses this `null` to display error. setUserContext(user); if (user) { - setAuthToken(result.tokenCreate.token, persistToken); + setTokens( + result.tokenCreate.token, + result.tokenCreate.csrfToken, + persistToken + ); } }, onError: logout @@ -110,7 +116,7 @@ export function useAuthProvider( }; useEffect(() => { - const token = getAuthToken(); + const token = getTokens().auth; if (!!token && !userContext) { autologinPromise.current = tokenVerify({ variables: { token } }); } else { @@ -133,17 +139,28 @@ export function useAuthProvider( return null; }; - const loginByToken = (token: string, user: User) => { + const loginByToken = (auth: string, refresh: string, user: User) => { setUserContext(user); - setAuthToken(token, persistToken); + setTokens(auth, refresh, persistToken); }; - const refreshToken = async () => { - const token = getAuthToken(); + const refreshToken = (): Promise => { + 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 { @@ -199,7 +216,7 @@ export const useAuth = () => { const isAuthenticated = !!user.user; return { - hasToken: !!getAuthToken(), + hasToken: !!getTokens(), isAuthenticated, tokenAuthLoading: user.tokenAuthLoading, tokenVerifyLoading: user.tokenVerifyLoading, diff --git a/src/auth/errors.ts b/src/auth/errors.ts index 4ced5f7a0..c42a2dfa2 100644 --- a/src/auth/errors.ts +++ b/src/auth/errors.ts @@ -11,6 +11,10 @@ export function isJwtError(error: GraphQLError): boolean { 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 { return error.extensions.exception.code === JWTError.expired; } diff --git a/src/auth/index.tsx b/src/auth/index.tsx index 9a5349b2c..4865edc02 100644 --- a/src/auth/index.tsx +++ b/src/auth/index.tsx @@ -15,10 +15,10 @@ import ResetPasswordSuccess from "./views/ResetPasswordSuccess"; interface UserContext { login: (username: string, password: string) => void; - loginByToken: (token: string, user: User) => void; + loginByToken: (auth: string, csrf: string, user: User) => void; logout: () => void; tokenAuthLoading: boolean; - tokenRefresh: () => Promise; + tokenRefresh: () => Promise; tokenVerifyLoading: boolean; user?: User; } diff --git a/src/auth/link.ts b/src/auth/link.ts new file mode 100644 index 000000000..66c19a069 --- /dev/null +++ b/src/auth/link.ts @@ -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; diff --git a/src/auth/mutations.ts b/src/auth/mutations.ts index 3ae53b360..8c21ce32d 100644 --- a/src/auth/mutations.ts +++ b/src/auth/mutations.ts @@ -13,11 +13,12 @@ export const tokenAuthMutation = gql` ${fragmentUser} mutation TokenAuth($email: String!, $password: String!) { tokenCreate(email: $email, password: $password) { - token errors: accountErrors { field message } + csrfToken + token user { ...User } @@ -68,6 +69,8 @@ export const setPassword = gql` errors: accountErrors { ...AccountErrorFragment } + csrfToken + refreshToken token user { ...User diff --git a/src/auth/types/SetPassword.ts b/src/auth/types/SetPassword.ts index 9a09baf70..0ede71f38 100644 --- a/src/auth/types/SetPassword.ts +++ b/src/auth/types/SetPassword.ts @@ -38,6 +38,8 @@ export interface SetPassword_setPassword_user { export interface SetPassword_setPassword { __typename: "SetPassword"; errors: SetPassword_setPassword_errors[]; + csrfToken: string | null; + refreshToken: string | null; token: string | null; user: SetPassword_setPassword_user | null; } diff --git a/src/auth/types/TokenAuth.ts b/src/auth/types/TokenAuth.ts index 7df055cfc..8da32e338 100644 --- a/src/auth/types/TokenAuth.ts +++ b/src/auth/types/TokenAuth.ts @@ -37,8 +37,9 @@ export interface TokenAuth_tokenCreate_user { export interface TokenAuth_tokenCreate { __typename: "CreateToken"; - token: string | null; errors: TokenAuth_tokenCreate_errors[]; + csrfToken: string | null; + token: string | null; user: TokenAuth_tokenCreate_user | null; } diff --git a/src/auth/utils.ts b/src/auth/utils.ts index 72ebf3a45..cce76bc10 100644 --- a/src/auth/utils.ts +++ b/src/auth/utils.ts @@ -2,20 +2,43 @@ import { UseNotifierResult } from "@saleor/hooks/useNotifier"; import { commonMessages } from "@saleor/intl"; import { IntlShape } from "react-intl"; -const TOKEN_STORAGE_KEY = "dashboardAuth"; +export enum TOKEN_STORAGE_KEY { + AUTH = "auth", + CSRF = "csrf" +} -export const getAuthToken = () => - localStorage.getItem(TOKEN_STORAGE_KEY) || - sessionStorage.getItem(TOKEN_STORAGE_KEY); +export const getTokens = () => ({ + auth: + 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) => - persist - ? localStorage.setItem(TOKEN_STORAGE_KEY, token) - : sessionStorage.setItem(TOKEN_STORAGE_KEY, token); +export const setTokens = (auth: string, csrf: string, persist: boolean) => { + if (persist) { + localStorage.setItem(TOKEN_STORAGE_KEY.AUTH, auth); + 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 = () => { - localStorage.removeItem(TOKEN_STORAGE_KEY); - sessionStorage.removeItem(TOKEN_STORAGE_KEY); +export const setAuthToken = (auth: string, persist: boolean) => { + if (persist) { + 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 = ( diff --git a/src/auth/views/NewPassword.tsx b/src/auth/views/NewPassword.tsx index 5fe9267ef..95fa9a4df 100644 --- a/src/auth/views/NewPassword.tsx +++ b/src/auth/views/NewPassword.tsx @@ -19,7 +19,11 @@ const NewPassword: React.FC = ({ location }) => { const handleSetPassword = async (data: SetPassword) => { 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); } }; diff --git a/src/hooks/makeQuery.ts b/src/hooks/makeQuery.ts index 743c5e7e8..d5117469a 100644 --- a/src/hooks/makeQuery.ts +++ b/src/hooks/makeQuery.ts @@ -1,7 +1,7 @@ -import { isJwtError } from "@saleor/auth/errors"; +import { isJwtError, isJwtExpiredError } from "@saleor/auth/errors"; import { commonMessages } from "@saleor/intl"; import { maybe, RequireAtLeastOne } from "@saleor/misc"; -import { ApolloQueryResult } from "apollo-client"; +import { ApolloError, ApolloQueryResult } from "apollo-client"; import { DocumentNode } from "graphql"; import { useEffect } from "react"; import { QueryResult, useQuery as useBaseQuery } from "react-apollo"; @@ -48,6 +48,37 @@ function makeQuery( }, errorPolicy: "all", 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, variables }); @@ -63,26 +94,6 @@ function makeQuery( } }, [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 = ( mergeFunc: (previousResults: TData, fetchMoreResult: TData) => TData, extraVariables: RequireAtLeastOne diff --git a/src/index.tsx b/src/index.tsx index 2118f7298..f684c326c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,8 +4,6 @@ import { defaultDataIdFromObject, InMemoryCache } from "apollo-cache-inmemory"; import { ApolloClient } from "apollo-client"; import { ApolloLink } from "apollo-link"; 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 React from "react"; import { ApolloProvider } from "react-apollo"; @@ -19,11 +17,11 @@ import AppsSection from "./apps"; import { appsSection } from "./apps/urls"; import AttributeSection from "./attributes"; import { attributeSection } from "./attributes/urls"; -import Auth, { getAuthToken, removeAuthToken } from "./auth"; +import Auth from "./auth"; import AuthProvider, { useAuth } from "./auth/AuthProvider"; import LoginLoading from "./auth/components/LoginLoading/LoginLoading"; import SectionRoute from "./auth/components/SectionRoute"; -import { isJwtError } from "./auth/errors"; +import authLink from "./auth/link"; import { hasPermission } from "./auth/misc"; import CategorySection from "./categories"; import CollectionSection from "./collections"; @@ -60,43 +58,15 @@ import { PermissionEnum } from "./types/globalTypes"; import WarehouseSection from "./warehouses"; import { warehouseSection } from "./warehouses/urls"; -interface ResponseError extends ErrorResponse { - networkError?: Error & { - statusCode?: number; - bodyText?: string; - }; -} - if (process.env.GTM_ID !== undefined) { 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 // These are separate clients and do not share configs between themselves // so we need to explicitly set them const linkOptions = { - credentials: "same-origin", + credentials: "include", uri: API_URI }; const uploadLink = createUploadLink(linkOptions); @@ -122,7 +92,7 @@ const apolloClient = new ApolloClient({ return defaultDataIdFromObject(obj); } }), - link: invalidTokenLink.concat(authLink.concat(link)) + link: authLink.concat(link) }); const App: React.FC = () => { diff --git a/src/queries.tsx b/src/queries.tsx index 55f878368..96c4f8b5d 100644 --- a/src/queries.tsx +++ b/src/queries.tsx @@ -1,10 +1,10 @@ -import { ApolloQueryResult } from "apollo-client"; +import { ApolloError, ApolloQueryResult } from "apollo-client"; import { DocumentNode } from "graphql"; import React from "react"; import { Query, QueryResult } from "react-apollo"; import { useIntl } from "react-intl"; -import { isJwtError } from "./auth/errors"; +import { isJwtError, isJwtExpiredError } from "./auth/errors"; import useAppState from "./hooks/useAppState"; import useNotifier from "./hooks/useNotifier"; import useUser from "./hooks/useUser"; @@ -79,29 +79,40 @@ export function TypedQuery( skip={skip} context={{ useBatching: true }} errorPolicy="all" - > - {(queryData: QueryResult) => { - if (queryData.error) { - if (queryData.error.graphQLErrors.some(isJwtError)) { + 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.sessionExpired) - }); - } else if ( - !queryData.error.graphQLErrors.every( - err => - maybe(() => err.extensions.exception.code) === - "PermissionDenied" - ) - ) { 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) + }); } - + }} + > + {(queryData: QueryResult) => { const loadMore = ( mergeFunc: ( previousResults: TData,