diff --git a/frontend-new/src/composables/useApi.ts b/frontend-new/src/composables/useApi.ts index 7d55a646d..74dc6f718 100644 --- a/frontend-new/src/composables/useApi.ts +++ b/frontend-new/src/composables/useApi.ts @@ -41,14 +41,9 @@ function request(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( return processAuthStuff(headers, authed, (headers) => request(`internal/${url}`, method, data, headers)); } -export function processAuthStuff(headers: Record, authRequired: boolean, handler: (headers: Record) => Promise) { +export async function processAuthStuff(headers: Record, authRequired: boolean, handler: (headers: Record) => Promise) { if (import.meta.env.SSR) { // forward cookies I guess? let token = useCookies().get("HangarAuth"); @@ -125,11 +120,37 @@ export function processAuthStuff(headers: Record, 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(headers: Record, 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); } diff --git a/frontend-new/src/composables/useAuth.ts b/frontend-new/src/composables/useAuth.ts index b0e5ca09b..3327ba5dc 100644 --- a/frontend-new/src/composables/useAuth.ts +++ b/frontend-new/src/composables/useAuth.ts @@ -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(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 | 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")); diff --git a/frontend-new/src/composables/useCookies.ts b/frontend-new/src/composables/useCookies.ts index e48ed0045..be8936c22 100644 --- a/frontend-new/src/composables/useCookies.ts +++ b/frontend-new/src/composables/useCookies.ts @@ -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 { diff --git a/frontend-new/src/composables/useResReq.ts b/frontend-new/src/composables/useResReq.ts index 0923a6e90..a1711cde1 100644 --- a/frontend-new/src/composables/useResReq.ts +++ b/frontend-new/src/composables/useResReq.ts @@ -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; }; diff --git a/frontend-new/src/pages/api.vue b/frontend-new/src/pages/api.vue index 3a1ae3463..4ec726cb3 100644 --- a/frontend-new/src/pages/api.vue +++ b/frontend-new/src/pages/api.vue @@ -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"); } diff --git a/frontend-new/src/store/auth.ts b/frontend-new/src/store/auth.ts index bdfc796a5..3d6ef697b 100644 --- a/frontend-new/src/store/auth.ts +++ b/frontend-new/src/store/auth.ts @@ -6,7 +6,6 @@ import { authLog } from "~/composables/useLog"; export const useAuthStore = defineStore("auth", () => { const authenticated = ref(false); const user = ref(null); - const token = ref(null); const routePermissions = ref(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 }; }); diff --git a/frontend-new/src/store/settings.ts b/frontend-new/src/store/settings.ts index d342e642a..408a7f916 100644 --- a/frontend-new/src/store/settings.ts +++ b/frontend-new/src/store/settings.ts @@ -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; diff --git a/src/main/java/io/papermc/hangar/service/TokenService.java b/src/main/java/io/papermc/hangar/service/TokenService.java index bb424ae63..cfb9df209 100644 --- a/src/main/java/io/papermc/hangar/service/TokenService.java +++ b/src/main/java/io/papermc/hangar/service/TokenService.java @@ -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) { diff --git a/src/main/java/io/papermc/hangar/service/internal/auth/SSOService.java b/src/main/java/io/papermc/hangar/service/internal/auth/SSOService.java index cefe3e57a..f10e723ac 100644 --- a/src/main/java/io/papermc/hangar/service/internal/auth/SSOService.java +++ b/src/main/java/io/papermc/hangar/service/internal/auth/SSOService.java @@ -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); }