mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
update auth token storage (#40504)
**Related issue:** Resolves #14401 this updates the mechanism of storing the auth token for a user that is used for making requests and validating a user session. We change the storage from local storage to a cookie. This allow a bit more security and prepares for a future change where we will allow the browser to handle setting and passing the auth token in the request. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] QA'd all new/changed functionality manually
This commit is contained in:
parent
3ef6f40a10
commit
084ebe6e16
23 changed files with 87 additions and 46 deletions
1
changes/issue-14401-update-auth-token-storage
Normal file
1
changes/issue-14401-update-auth-token-storage
Normal file
|
|
@ -0,0 +1 @@
|
|||
- update storage of the auth token used in the UI; move if from local storage to a cookie.
|
||||
|
|
@ -13,6 +13,7 @@ const DEFAULT_USER_MOCK: IUser = {
|
|||
global_role: "admin",
|
||||
api_only: false,
|
||||
teams: [],
|
||||
fleets: [],
|
||||
};
|
||||
|
||||
const createMockUser = (overrides?: Partial<IUser>): IUser => {
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 => {
|
|||
</p>
|
||||
</InfoBanner>
|
||||
<InputFieldHiddenContent
|
||||
value={authToken() || ""}
|
||||
value={authToken.get() || ""}
|
||||
helpText={
|
||||
<>
|
||||
This token is intended for SSO users to authenticate in the
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<ILoginResponse> => {
|
||||
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,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export const userStub: IUser = {
|
|||
gravatar_url: "https://image.com",
|
||||
sso_enabled: false,
|
||||
teams: [{ ...userTeamStub }],
|
||||
fleets: [{ ...userTeamStub }],
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
25
frontend/utilities/auth_token/index.ts
Normal file
25
frontend/utilities/auth_token/index.ts
Normal file
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue