mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-02-17 15:01:42 +08:00
further improve token handling, closes #679
This commit is contained in:
parent
44487e4dc5
commit
51e8816fa4
@ -41,14 +41,9 @@ function request<T>(url: string, method: AxiosRequestConfig["method"], data: obj
|
||||
if (headers["set-cookie"]) {
|
||||
const statString = headers["set-cookie"].find((c: string) => c.startsWith("hangar_stats"));
|
||||
if (statString) {
|
||||
const statCookie: StatCookie = new Cookies("statString") as unknown as StatCookie;
|
||||
|
||||
cookies.set("hangar_stats", statCookie.hangar_stats, {
|
||||
path: statCookie.Path,
|
||||
expires: new Date(statCookie.Expires),
|
||||
sameSite: "strict",
|
||||
secure: statCookie.Secure,
|
||||
});
|
||||
const parsedCookies = new Cookies(statString);
|
||||
const statCookie = parsedCookies.get("hangar_stats");
|
||||
cookies.set("hangar_stats", statCookie); // TODO verify that this all works
|
||||
}
|
||||
}
|
||||
resolve(data);
|
||||
@ -112,7 +107,7 @@ export async function useInternalApi<T>(
|
||||
return processAuthStuff(headers, authed, (headers) => request(`internal/${url}`, method, data, headers));
|
||||
}
|
||||
|
||||
export function processAuthStuff<T>(headers: Record<string, string>, authRequired: boolean, handler: (headers: Record<string, string>) => Promise<T>) {
|
||||
export async function processAuthStuff<T>(headers: Record<string, string>, authRequired: boolean, handler: (headers: Record<string, string>) => Promise<T>) {
|
||||
if (import.meta.env.SSR) {
|
||||
// forward cookies I guess?
|
||||
let token = useCookies().get("HangarAuth");
|
||||
@ -125,11 +120,37 @@ export function processAuthStuff<T>(headers: Record<string, string>, authRequire
|
||||
}
|
||||
if (token) {
|
||||
authLog("forward token from cookie");
|
||||
|
||||
// make sure our token is still valid, else refresh
|
||||
if (!useAuth.validateToken(token)) {
|
||||
authLog("token no longer valid, lets refresh");
|
||||
const result = await useAuth.refreshToken();
|
||||
if (result) {
|
||||
authLog("refreshed");
|
||||
token = result;
|
||||
} else {
|
||||
authLog("could not refresh, invalidate");
|
||||
await useAuth.invalidate();
|
||||
throw {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
data: {
|
||||
isHangarApiException: true,
|
||||
httpError: {
|
||||
statusCode: 401,
|
||||
},
|
||||
message: "You must be logged in",
|
||||
} as HangarApiException,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
headers = { ...headers, Authorization: `HangarAuth ${token}` };
|
||||
} else {
|
||||
authLog("can't forward token, no cookie");
|
||||
if (authRequired) {
|
||||
return Promise.reject({
|
||||
throw {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
data: {
|
||||
@ -140,11 +161,11 @@ export function processAuthStuff<T>(headers: Record<string, string>, authRequire
|
||||
message: "You must be logged in",
|
||||
} as HangarApiException,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// don't need to do anything, cookies are handled by the browser
|
||||
// don't need to do anything, cookies are handled by the browser, we can't even access it to validate it
|
||||
}
|
||||
return handler(headers);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useAuthStore } from "~/store/auth";
|
||||
import { useCookies } from "~/composables/useCookies";
|
||||
import { processAuthStuff, useInternalApi } from "~/composables/useApi";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
import { useAxios } from "~/composables/useAxios";
|
||||
import { authLog } from "~/composables/useLog";
|
||||
import { HangarUser } from "hangar-internal";
|
||||
@ -9,6 +9,7 @@ import { Pinia } from "pinia";
|
||||
import { AxiosError, AxiosRequestHeaders } from "axios";
|
||||
import { useResponse } from "~/composables/useResReq";
|
||||
import Cookies from "universal-cookie";
|
||||
import jwtDecode, { JwtPayload } from "jwt-decode";
|
||||
|
||||
class Auth {
|
||||
loginUrl(redirectUrl: string): string {
|
||||
@ -22,6 +23,17 @@ class Auth {
|
||||
location.replace(`/logout?returnUrl=${import.meta.env.HANGAR_PUBLIC_HOST}?loggedOut`);
|
||||
}
|
||||
|
||||
validateToken(token: string) {
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
const decoded = jwtDecode<JwtPayload>(token);
|
||||
if (!decoded.exp) {
|
||||
return false;
|
||||
}
|
||||
return decoded.exp * 1000 > Date.now() - 10 * 1000; // check against 10 seconds earlier to mitigate tokens expiring mid-request
|
||||
}
|
||||
|
||||
// TODO do we need to scope this to the user?
|
||||
refreshPromise: Promise<boolean | string> | null = null;
|
||||
|
||||
@ -40,8 +52,9 @@ class Auth {
|
||||
authLog("do request");
|
||||
const headers: AxiosRequestHeaders = {};
|
||||
if (import.meta.env.SSR) {
|
||||
headers.cookie = "HangarAuth_REFRESH=" + useCookies().get("HangarAuth_REFRESH");
|
||||
authLog("pass refresh cookie");
|
||||
const refreshToken = useCookies().get("HangarAuth_REFRESH");
|
||||
headers.cookie = "HangarAuth_REFRESH=" + refreshToken;
|
||||
authLog("pass refresh cookie", refreshToken);
|
||||
}
|
||||
const response = await useAxios.get("/refresh", { headers });
|
||||
if (import.meta.env.SSR) {
|
||||
@ -77,7 +90,6 @@ class Auth {
|
||||
async invalidate() {
|
||||
useAuthStore(this.usePiniaIfPresent()).$patch({
|
||||
user: null,
|
||||
token: null,
|
||||
authenticated: false,
|
||||
});
|
||||
await useAxios.get("/invalidate").catch(() => console.log("invalidate failed"));
|
||||
|
@ -16,11 +16,11 @@ export const useCookies = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (change.value === undefined) {
|
||||
res.setHeader("Set-Cookie", change.name + "=");
|
||||
} else {
|
||||
res.setHeader("Set-Cookie", cookie.serialize(change.name, change.value, change.options));
|
||||
}
|
||||
const old = res.getHeader("set-cookie");
|
||||
const newCookie = change.value === undefined ? change.name + "=" : cookie.serialize(change.name, change.value, change.options);
|
||||
const val = old ? old + "; " + newCookie : newCookie;
|
||||
console.log("setting cookie header to " + val);
|
||||
res.setHeader("set-cookie", val);
|
||||
});
|
||||
return cookies;
|
||||
} else {
|
||||
|
@ -29,7 +29,12 @@ export const useResponse = () => {
|
||||
if (ctx) {
|
||||
return ctx.response;
|
||||
}
|
||||
console.error("response null!");
|
||||
console.trace();
|
||||
return null;
|
||||
}
|
||||
|
||||
console.error("useResponse called on client?!");
|
||||
console.trace();
|
||||
return null;
|
||||
};
|
||||
|
@ -27,7 +27,6 @@ onMounted(() => {
|
||||
layout: "BaseLayout",
|
||||
requestInterceptor: (req) => {
|
||||
if (!req.loadSpec) {
|
||||
req.headers.authorization = "HangarAuth " + authStore.token;
|
||||
if (req.url.startsWith("http://localhost:8080")) {
|
||||
req.url = req.url.replace("http://localhost:8080", "http://localhost:3333");
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import { authLog } from "~/composables/useLog";
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
const authenticated = ref<boolean>(false);
|
||||
const user = ref<HangarUser | null>(null);
|
||||
const token = ref<string | null>(null);
|
||||
const routePermissions = ref<string | null>(null);
|
||||
|
||||
authLog("create authStore");
|
||||
@ -19,5 +18,5 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
routePermissions.value = routePerms;
|
||||
}
|
||||
|
||||
return { authenticated, user, token, routePermissions, setUser, setRoutePerms };
|
||||
return { authenticated, user, routePermissions, setUser, setRoutePerms };
|
||||
});
|
||||
|
@ -43,7 +43,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
mobile.value = false;
|
||||
}
|
||||
|
||||
async function loadSettingsServer(request: Context["request"], response: Context["response"], head) {
|
||||
async function loadSettingsServer(request: Context["request"], response: Context["response"]) {
|
||||
if (!import.meta.env.SSR) return;
|
||||
const authStore = useAuthStore();
|
||||
let newLocale;
|
||||
|
@ -56,7 +56,8 @@ public class TokenService extends HangarComponent {
|
||||
UserRefreshToken userRefreshToken = userRefreshTokenDAO.insert(new UserRefreshToken(userTable.getId(), UUID.randomUUID(), UUID.randomUUID()));
|
||||
addCookie(SecurityConfig.REFRESH_COOKIE_NAME, userRefreshToken.getToken().toString(), config.security.getRefreshTokenExpiry().toSeconds());
|
||||
String accessToken = newToken0(userTable);
|
||||
addCookie(SecurityConfig.AUTH_NAME, accessToken, config.security.getTokenExpiry().toSeconds());
|
||||
// let the access token cookie be around for longer, so we can more nicely detect expired tokens via the response code
|
||||
addCookie(SecurityConfig.AUTH_NAME, accessToken, config.security.getTokenExpiry().toSeconds() * 2);
|
||||
}
|
||||
|
||||
private void addCookie(String name, String value, long maxAge) {
|
||||
@ -67,12 +68,18 @@ public class TokenService extends HangarComponent {
|
||||
if (refreshToken == null) {
|
||||
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "No refresh token found");
|
||||
}
|
||||
UserRefreshToken userRefreshToken = userRefreshTokenDAO.getByToken(UUID.fromString(refreshToken));
|
||||
UUID uuid;
|
||||
try {
|
||||
uuid = UUID.fromString(refreshToken);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "Invalid refresh token " + refreshToken);
|
||||
}
|
||||
UserRefreshToken userRefreshToken = userRefreshTokenDAO.getByToken(uuid);
|
||||
if (userRefreshToken == null) {
|
||||
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "Unrecognized refresh token");
|
||||
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "Unrecognized refresh token " + uuid);
|
||||
}
|
||||
if (userRefreshToken.getLastUpdated().isBefore(OffsetDateTime.now().minus(config.security.getRefreshTokenExpiry()))) {
|
||||
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "Expired refresh token");
|
||||
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "Expired refresh token" + uuid);
|
||||
}
|
||||
UserTable userTable = userService.getUserTable(userRefreshToken.getUserId());
|
||||
if (userTable == null) {
|
||||
|
@ -136,8 +136,13 @@ public class SSOService {
|
||||
user.setFullName(traits.getName().getFirst() + " " + traits.getName().getLast());
|
||||
user.setName(traits.getUsername());
|
||||
user.setEmail(traits.getEmail());
|
||||
user.setLanguage(traits.getLanguage());
|
||||
user.setTheme(traits.getTheme());
|
||||
// only sync if set
|
||||
if (traits.getLanguage() != null) {
|
||||
user.setLanguage(traits.getLanguage());
|
||||
}
|
||||
if (traits.getTheme() != null) {
|
||||
user.setTheme(traits.getTheme());
|
||||
}
|
||||
userDAO.update(user);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user