diff --git a/changes/issue-14401-update-auth-token-storage b/changes/issue-14401-update-auth-token-storage
new file mode 100644
index 0000000000..0e3ecda2e3
--- /dev/null
+++ b/changes/issue-14401-update-auth-token-storage
@@ -0,0 +1 @@
+- update storage of the auth token used in the UI; move if from local storage to a cookie.
diff --git a/frontend/__mocks__/userMock.ts b/frontend/__mocks__/userMock.ts
index a6436284d9..152400a514 100644
--- a/frontend/__mocks__/userMock.ts
+++ b/frontend/__mocks__/userMock.ts
@@ -13,6 +13,7 @@ const DEFAULT_USER_MOCK: IUser = {
global_role: "admin",
api_only: false,
teams: [],
+ fleets: [],
};
const createMockUser = (overrides?: Partial): IUser => {
diff --git a/frontend/components/App/App.tsx b/frontend/components/App/App.tsx
index ca805206fb..d3705b21af 100644
--- a/frontend/components/App/App.tsx
+++ b/frontend/components/App/App.tsx
@@ -10,7 +10,7 @@ import QueryProvider from "context/query";
import PolicyProvider from "context/policy";
import NotificationProvider from "context/notification";
import { AppContext } from "context/app";
-import { authToken, clearToken } from "utilities/local";
+import authToken from "utilities/auth_token";
import useDeepEffect from "hooks/useDeepEffect";
import { QueryParams } from "utilities/url";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
@@ -206,7 +206,7 @@ const App = ({ children, location }: IAppProps): JSX.Element => {
) {
return true;
}
- clearToken();
+ authToken.remove();
// if this is not the device user page,
// redirect to login
if (!location?.pathname.includes("/device/")) {
@@ -217,7 +217,7 @@ const App = ({ children, location }: IAppProps): JSX.Element => {
};
useEffect(() => {
- if (authToken() && !location?.pathname.includes("/device/")) {
+ if (authToken.get() && !location?.pathname.includes("/device/")) {
fetchCurrentUser();
}
}, [location?.pathname]);
diff --git a/frontend/interfaces/user.ts b/frontend/interfaces/user.ts
index 24ecd8191f..fcb80461b4 100644
--- a/frontend/interfaces/user.ts
+++ b/frontend/interfaces/user.ts
@@ -46,7 +46,7 @@ export interface IUser {
id: number;
name: string;
email: string;
- role: UserRole;
+ role?: UserRole;
force_password_reset: boolean;
gravatar_url?: string;
gravatar_url_dark?: string;
@@ -55,6 +55,7 @@ export interface IUser {
global_role: UserRole | null;
api_only: boolean;
teams: ITeam[];
+ fleets: ITeam[]; // This will eventually replace `teams`, but for now we need both to avoid breaking changes.
}
/**
diff --git a/frontend/pages/AccountPage/AccountPage.tsx b/frontend/pages/AccountPage/AccountPage.tsx
index 2a28928307..72793518aa 100644
--- a/frontend/pages/AccountPage/AccountPage.tsx
+++ b/frontend/pages/AccountPage/AccountPage.tsx
@@ -5,7 +5,7 @@ import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import { IUser } from "interfaces/user";
import usersAPI from "services/entities/users";
-import { authToken } from "utilities/local";
+import authToken from "utilities/auth_token";
import deepDifference from "utilities/deep_difference";
import formatErrorResponse from "utilities/format_error_response";
@@ -192,7 +192,7 @@ const AccountPage = ({ router }: IAccountPageProps): JSX.Element | null => {
This token is intended for SSO users to authenticate in the
diff --git a/frontend/pages/LoginPage/LoginPage.tsx b/frontend/pages/LoginPage/LoginPage.tsx
index faaf285840..581d02c1a5 100644
--- a/frontend/pages/LoginPage/LoginPage.tsx
+++ b/frontend/pages/LoginPage/LoginPage.tsx
@@ -10,6 +10,7 @@ import { RoutingContext } from "context/routing";
import { ISSOSettings } from "interfaces/ssoSettings";
import { ILoginUserData } from "interfaces/user";
import local from "utilities/local";
+import authToken from "utilities/auth_token";
import configAPI from "services/entities/config";
import sessionsAPI, { ISSOSettingsResponse } from "services/entities/sessions";
import formatErrorResponse from "utilities/format_error_response";
@@ -121,7 +122,7 @@ const LoginPage = ({ router, location }: ILoginPageProps) => {
const response = await sessionsAPI.login(formData);
const { user, available_teams, token } = response;
- local.setItem("auth_token", token);
+ authToken.save(token);
setCurrentUser(user);
setAvailableTeams(user, available_teams);
diff --git a/frontend/pages/LoginPage/LoginPreviewPage.tsx b/frontend/pages/LoginPage/LoginPreviewPage.tsx
index 8005c110dd..dae263f201 100644
--- a/frontend/pages/LoginPage/LoginPreviewPage.tsx
+++ b/frontend/pages/LoginPage/LoginPreviewPage.tsx
@@ -7,6 +7,7 @@ import paths from "router/paths";
import { AppContext } from "context/app";
import sessionsAPI from "services/entities/sessions";
import local from "utilities/local";
+import authToken from "utilities/auth_token";
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";
// @ts-ignore
@@ -35,7 +36,7 @@ const LoginPreviewPage = ({ router }: ILoginPreviewPageProps): JSX.Element => {
const { user, available_teams, token } = await sessionsAPI.login(
formData
);
- local.setItem("auth_token", token);
+ authToken.save(token);
setCurrentUser(user);
setAvailableTeams(user, available_teams);
diff --git a/frontend/pages/LogoutPage/LogoutPage.tsx b/frontend/pages/LogoutPage/LogoutPage.tsx
index d3ac154622..d43cc31caa 100644
--- a/frontend/pages/LogoutPage/LogoutPage.tsx
+++ b/frontend/pages/LogoutPage/LogoutPage.tsx
@@ -4,7 +4,7 @@ import { InjectedRouter } from "react-router";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import sessionsAPI from "services/entities/sessions";
-import { clearToken } from "utilities/local";
+import authToken from "utilities/auth_token";
interface ILogoutPageProps {
router: InjectedRouter;
@@ -18,7 +18,7 @@ const LogoutPage = ({ router }: ILogoutPageProps) => {
const logoutUser = async () => {
try {
await sessionsAPI.destroy();
- clearToken();
+ authToken.remove();
setTimeout(() => {
window.location.href = isSandboxMode
? "https://www.fleetdm.com/logout"
diff --git a/frontend/pages/MfaPage/MfaPage.tsx b/frontend/pages/MfaPage/MfaPage.tsx
index cb45fc2bf6..5c7b00a0c6 100644
--- a/frontend/pages/MfaPage/MfaPage.tsx
+++ b/frontend/pages/MfaPage/MfaPage.tsx
@@ -6,6 +6,7 @@ import { AppContext } from "context/app";
import { RoutingContext } from "context/routing";
import paths from "router/paths";
import local from "utilities/local";
+import authToken from "utilities/auth_token";
import configAPI from "services/entities/config";
import sessionsAPI from "services/entities/sessions";
@@ -44,7 +45,7 @@ const MfaPage = ({ router, params }: IMfaPage) => {
const response = await sessionsAPI.finishMFA({ token: mfaToken });
const { user, available_teams, token } = response;
- local.setItem("auth_token", token);
+ authToken.save(token);
setCurrentUser(user);
setAvailableTeams(user, available_teams);
diff --git a/frontend/pages/RegistrationPage/RegistrationPage.tsx b/frontend/pages/RegistrationPage/RegistrationPage.tsx
index 6ded1dcc53..ba55f0eefb 100644
--- a/frontend/pages/RegistrationPage/RegistrationPage.tsx
+++ b/frontend/pages/RegistrationPage/RegistrationPage.tsx
@@ -6,6 +6,7 @@ import paths from "router/paths";
import { AppContext } from "context/app";
import usersAPI from "services/entities/users";
import local from "utilities/local";
+import authToken from "utilities/auth_token";
import FlashMessage from "components/FlashMessage";
import { INotification } from "interfaces/notification";
@@ -61,7 +62,7 @@ const RegistrationPage = ({ router }: IRegistrationPageProps) => {
setIsLoading(true);
try {
const { token } = await usersAPI.setup(formData);
- local.setItem("auth_token", token);
+ authToken.save(token);
const { user, available_teams, settings } = await usersAPI.me();
setCurrentUser(user);
diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/components/AutocompleteDropdown/AutocompleteDropdown.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/components/AutocompleteDropdown/AutocompleteDropdown.tsx
index f8f029ee4c..62d5e17fee 100644
--- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/components/AutocompleteDropdown/AutocompleteDropdown.tsx
+++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/components/AutocompleteDropdown/AutocompleteDropdown.tsx
@@ -7,7 +7,7 @@ import React, { useCallback } from "react";
import { Async, OnChangeHandler, Option } from "react-select";
import classnames from "classnames";
-import { authToken } from "utilities/local";
+import authToken from "utilities/auth_token";
import debounce from "utilities/debounce";
import permissionUtils from "utilities/permissions";
import { IDropdownOption } from "interfaces/dropdownOption";
@@ -97,7 +97,7 @@ const AutocompleteDropdown = ({
fetch(createUrl(resourceUrl, input), {
headers: {
- authorization: `Bearer ${authToken()}`,
+ authorization: `Bearer ${authToken.get()}`,
},
})
.then((res) => {
diff --git a/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx b/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx
index 576251dcf4..5c79b08577 100644
--- a/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx
+++ b/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx
@@ -7,7 +7,7 @@ import { IApiError } from "interfaces/errors";
import { IInvite, IEditInviteFormData } from "interfaces/invite";
import { IUser, IUserFormErrors } from "interfaces/user";
import { ITeam } from "interfaces/team";
-import { clearToken } from "utilities/local";
+import authToken from "utilities/auth_token";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
@@ -415,7 +415,7 @@ const UsersTable = ({ router }: IUsersTableProps): JSX.Element => {
.deleteSessions(userEditing.id)
.then(() => {
if (isResettingCurrentUser) {
- clearToken();
+ authToken.remove();
setTimeout(() => {
window.location.href = "/";
}, 500);
diff --git a/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx b/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx
index 715e4c04ec..725320e491 100644
--- a/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx
+++ b/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx
@@ -9,7 +9,7 @@ import campaignHelpers from "utilities/campaign_helpers";
import queryAPI from "services/entities/queries";
import debounce from "utilities/debounce";
import { BASE_URL, DEFAULT_CAMPAIGN_STATE } from "utilities/constants";
-import { authToken } from "utilities/local";
+import authToken from "utilities/auth_token";
import { ICampaign, ICampaignState } from "interfaces/campaign";
import { IPolicy } from "interfaces/policy";
@@ -102,7 +102,7 @@ const RunQuery = ({
websocket?.send(
JSON.stringify({
type: "auth",
- data: { token: authToken() },
+ data: { token: authToken.get() },
})
);
websocket?.send(
diff --git a/frontend/pages/queries/live/screens/RunQuery.tsx b/frontend/pages/queries/live/screens/RunQuery.tsx
index 8c514d5a45..088fe64518 100644
--- a/frontend/pages/queries/live/screens/RunQuery.tsx
+++ b/frontend/pages/queries/live/screens/RunQuery.tsx
@@ -10,7 +10,7 @@ import campaignHelpers from "utilities/campaign_helpers";
import debounce from "utilities/debounce";
import { BASE_URL, DEFAULT_CAMPAIGN_STATE } from "utilities/constants";
-import { authToken } from "utilities/local";
+import authToken from "utilities/auth_token";
import { ICampaign, ICampaignState } from "interfaces/campaign";
import { IQuery } from "interfaces/query";
@@ -113,7 +113,7 @@ const RunQuery = ({
websocket?.send(
JSON.stringify({
type: "auth",
- data: { token: authToken() },
+ data: { token: authToken.get() },
})
);
websocket?.send(
diff --git a/frontend/router/components/AuthenticatedRoutes/AuthenticatedRoutes.tsx b/frontend/router/components/AuthenticatedRoutes/AuthenticatedRoutes.tsx
index f400171b3b..5837bc91a0 100644
--- a/frontend/router/components/AuthenticatedRoutes/AuthenticatedRoutes.tsx
+++ b/frontend/router/components/AuthenticatedRoutes/AuthenticatedRoutes.tsx
@@ -5,7 +5,7 @@ import paths from "router/paths";
import { AppContext } from "context/app";
import { RoutingContext } from "context/routing";
import useDeepEffect from "hooks/useDeepEffect";
-import { authToken, clearToken } from "utilities/local";
+import authToken from "utilities/auth_token";
import { useErrorHandler } from "react-error-boundary";
import permissions from "utilities/permissions";
@@ -78,7 +78,7 @@ export const AuthenticatedRoutes = ({
useDeepEffect(() => {
// this works with App.tsx. if authToken does
// exist, user state is checked and fetched if null
- if (!authToken()) {
+ if (!authToken.get()) {
if (window.location.hostname.includes(".sandbox.fleetdm.com")) {
window.location.href = "https://www.fleetdm.com/try-fleet/login";
}
@@ -86,7 +86,7 @@ export const AuthenticatedRoutes = ({
return redirectToLogin();
}
- if (currentUser?.force_password_reset && !authToken()) {
+ if (currentUser?.force_password_reset && !authToken.get()) {
return redirectToPasswordReset();
}
@@ -95,7 +95,7 @@ export const AuthenticatedRoutes = ({
}
if (currentUser && permissions.isNoAccess(currentUser)) {
- clearToken();
+ authToken.remove();
return handlePageError({ status: 403 });
}
}, [currentUser]);
diff --git a/frontend/services/entities/mdm_android.ts b/frontend/services/entities/mdm_android.ts
index 4e66fa2074..49d1506244 100644
--- a/frontend/services/entities/mdm_android.ts
+++ b/frontend/services/entities/mdm_android.ts
@@ -1,6 +1,6 @@
import sendRequest from "services";
import endpoints from "utilities/endpoints";
-import { authToken } from "utilities/local";
+import authToken from "utilities/auth_token";
interface IGetAndroidSignupUrlResponse {
android_enterprise_signup_url: string;
@@ -38,7 +38,7 @@ export default {
const response = await fetch(endpoints.MDM_ANDROID_SSE_URL, {
method: "GET",
headers: {
- Authorization: `Bearer ${authToken()}`,
+ Authorization: `Bearer ${authToken.get()}`,
},
signal: abortSignal,
});
diff --git a/frontend/services/entities/sessions.ts b/frontend/services/entities/sessions.ts
index 1f9f765d28..bc1787d8e5 100644
--- a/frontend/services/entities/sessions.ts
+++ b/frontend/services/entities/sessions.ts
@@ -1,4 +1,6 @@
import { ISSOSettings } from "interfaces/ssoSettings";
+import { IUser } from "interfaces/user";
+import { ITeamSummary } from "interfaces/team";
import sendRequest from "services";
import endpoints from "utilities/endpoints";
import helpers from "utilities/helpers";
@@ -16,8 +18,15 @@ export interface ISSOSettingsResponse {
settings: ISSOSettings;
}
+export interface ILoginResponse {
+ user: IUser;
+ available_teams: ITeamSummary[];
+ available_fleets: ITeamSummary[];
+ token: string;
+}
+
export default {
- login: ({ email, password }: ILoginProps) => {
+ login: ({ email, password }: ILoginProps): Promise => {
const { LOGIN } = endpoints;
return sendRequest(
@@ -38,13 +47,12 @@ export default {
throw rawResponse;
}
const response = rawResponse.data;
- const { user, available_teams } = response;
+ const { user } = response;
const userWithGravatarUrl = helpers.addGravatarUrlToResource(user);
return {
...response,
user: userWithGravatarUrl,
- available_teams,
};
});
},
diff --git a/frontend/services/index.ts b/frontend/services/index.ts
index 2226799e5c..caad3a1d27 100644
--- a/frontend/services/index.ts
+++ b/frontend/services/index.ts
@@ -3,8 +3,9 @@ import axios, {
ResponseType as AxiosResponseType,
AxiosProgressEvent,
} from "axios";
+
import URL_PREFIX from "router/url_prefix";
-import { authToken } from "utilities/local";
+import authToken from "utilities/auth_token";
export const sendRequestWithProgress = async ({
method,
@@ -32,7 +33,6 @@ export const sendRequestWithProgress = async ({
const { origin } = global.window.location;
const url = `${origin}${URL_PREFIX}/api${path}`;
- const token = authToken();
try {
const response = await axios({
@@ -42,7 +42,7 @@ export const sendRequestWithProgress = async ({
responseType,
timeout,
headers: {
- Authorization: `Bearer ${token}`,
+ Authorization: `Bearer ${authToken.get()}`,
},
onDownloadProgress,
onUploadProgress,
@@ -79,7 +79,6 @@ export const sendRequest = async (
const { origin } = global.window.location;
const url = `${origin}${URL_PREFIX}/api${path}`;
- const token = authToken();
try {
const response = await axios({
@@ -89,7 +88,7 @@ export const sendRequest = async (
responseType,
timeout,
headers: {
- Authorization: `Bearer ${token}`,
+ Authorization: `Bearer ${authToken.get()}`,
},
});
@@ -128,7 +127,6 @@ export const sendRequestWithHeaders = async (
const { origin } = global.window.location;
const url = `${origin}${URL_PREFIX}/api${path}`;
- const token = authToken();
try {
const response = await axios({
@@ -138,7 +136,7 @@ export const sendRequestWithHeaders = async (
responseType,
timeout,
headers: {
- Authorization: `Bearer ${token}`,
+ Authorization: `Bearer ${authToken.get()}`,
...customHeaders,
},
});
@@ -193,7 +191,6 @@ export const sendRequestWithProgressAndHeaders = async ({
const { origin } = global.window.location;
const url = `${origin}${URL_PREFIX}/api${path}`;
- const token = authToken();
try {
const response = await axios({
@@ -203,7 +200,7 @@ export const sendRequestWithProgressAndHeaders = async ({
responseType,
timeout,
headers: {
- Authorization: `Bearer ${token}`,
+ Authorization: `Bearer ${authToken.get()}`,
...customHeaders,
},
onDownloadProgress,
diff --git a/frontend/test/stubs.ts b/frontend/test/stubs.ts
index 6901ce23cf..05439cc986 100644
--- a/frontend/test/stubs.ts
+++ b/frontend/test/stubs.ts
@@ -25,6 +25,7 @@ export const userStub: IUser = {
gravatar_url: "https://image.com",
sso_enabled: false,
teams: [{ ...userTeamStub }],
+ fleets: [{ ...userTeamStub }],
};
export default {
diff --git a/frontend/utilities/auth_token/index.ts b/frontend/utilities/auth_token/index.ts
new file mode 100644
index 0000000000..7158fb07d9
--- /dev/null
+++ b/frontend/utilities/auth_token/index.ts
@@ -0,0 +1,25 @@
+/**
+ * This contains a collection of utility functions for working with
+ * users auth token.
+ */
+import Cookie from "js-cookie";
+
+const save = (token: string): void => {
+ Cookie.set("__Host-token", token, { secure: true, sameSite: "lax" });
+};
+
+const get = (): string | null => {
+ return Cookie.get("__Host-token") || null;
+};
+
+const remove = (): void => {
+ // NOTE: the entire cookie including the name and values must be provided
+ // to correctly remove. That is why we include the options here as well.
+ Cookie.remove("__Host-token", { secure: true, sameSite: "lax" });
+};
+
+export default {
+ save,
+ get,
+ remove,
+};
diff --git a/frontend/utilities/local.ts b/frontend/utilities/local.ts
index 63c985f146..8c4967a2b6 100644
--- a/frontend/utilities/local.ts
+++ b/frontend/utilities/local.ts
@@ -17,12 +17,4 @@ const local = {
},
};
-export const authToken = (): string | null => {
- return local.getItem("auth_token");
-};
-
-export const clearToken = (): void => {
- return local.removeItem("auth_token");
-};
-
export default local;
diff --git a/package.json b/package.json
index 208bbac480..fc5bd8c41a 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"file-saver": "1.3.8",
"history": "2.1.0",
"isomorphic-fetch": "3.0.0",
+ "js-cookie": "3.0.5",
"js-md5": "0.7.3",
"js-yaml": "3.14.2",
"lodash": "4.17.23",
@@ -104,6 +105,7 @@
"@types/expect": "1.20.3",
"@types/file-saver": "2.0.5",
"@types/jest": "29.5.12",
+ "@types/js-cookie": "3.0.6",
"@types/js-md5": "0.4.3",
"@types/js-yaml": "4.0.5",
"@types/lodash": "4.14.179",
diff --git a/yarn.lock b/yarn.lock
index e55fa33157..c0234a4cbe 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3680,6 +3680,11 @@
expect "^29.0.0"
pretty-format "^29.0.0"
+"@types/js-cookie@3.0.6":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.6.tgz#a04ca19e877687bd449f5ad37d33b104b71fdf95"
+ integrity sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==
+
"@types/js-md5@0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@types/js-md5/-/js-md5-0.4.3.tgz#c134cbb71c75018876181f886a12a2f9962a312a"
@@ -9308,6 +9313,10 @@ joycon@^3.0.1:
version "3.1.1"
resolved "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz"
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
+js-cookie@3.0.5:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
+ integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
js-md5@0.7.3:
version "0.7.3"