further improve token handling, closes #679

This commit is contained in:
MiniDigger | Martin 2022-06-17 11:54:02 +02:00
parent 44487e4dc5
commit 51e8816fa4
9 changed files with 79 additions and 31 deletions

View File

@ -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);
}

View File

@ -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"));

View File

@ -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 {

View File

@ -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;
};

View File

@ -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");
}

View File

@ -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 };
});

View File

@ -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;

View File

@ -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) {

View File

@ -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);
}