@thecodeorigin/auth 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -72
- package/dist/contract/index.d.mts +80 -3
- package/dist/contract/index.d.ts +80 -3
- package/dist/contract/index.mjs +34 -2
- package/dist/module.d.mts +4 -0
- package/dist/module.d.ts +4 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +33 -12
- package/dist/runtime/app/composables/useAuth.d.ts +5 -0
- package/dist/runtime/app/composables/useAuth.js +50 -10
- package/dist/runtime/app/composables/useAuthLogin.d.ts +11 -0
- package/dist/runtime/app/composables/useAuthLogin.js +73 -0
- package/dist/runtime/app/middleware/auth.global.js +2 -0
- package/dist/runtime/app/plugins/session-sync.client.d.ts +2 -0
- package/dist/runtime/app/plugins/session-sync.client.js +47 -0
- package/dist/runtime/app/utils/return-path.d.ts +1 -0
- package/dist/runtime/app/utils/return-path.js +18 -0
- package/dist/runtime/app/utils/session-recovery.d.ts +5 -0
- package/dist/runtime/app/utils/session-recovery.js +23 -0
- package/dist/runtime/server/api/_auth/impersonate.post.d.ts +1 -0
- package/dist/runtime/server/api/_auth/impersonate.post.js +47 -27
- package/dist/runtime/server/api/_auth/organizations/switch.post.d.ts +1 -0
- package/dist/runtime/server/api/_auth/organizations/switch.post.js +12 -17
- package/dist/runtime/server/api/_auth/session-restore.get.d.ts +5 -0
- package/dist/runtime/server/api/_auth/session-restore.get.js +6 -0
- package/dist/runtime/server/api/_auth/session.get.d.ts +1 -0
- package/dist/runtime/server/api/_auth/sign-out.post.js +3 -0
- package/dist/runtime/server/api/_auth/stop-impersonating.post.d.ts +1 -0
- package/dist/runtime/server/api/_auth/stop-impersonating.post.js +18 -16
- package/dist/runtime/server/index.d.ts +2 -2
- package/dist/runtime/server/index.js +1 -1
- package/dist/runtime/server/routes/callback.get.js +62 -32
- package/dist/runtime/server/routes/sign-in.get.js +8 -6
- package/dist/runtime/server/routes/sign-out.get.d.ts +1 -0
- package/dist/runtime/server/routes/sign-out.get.js +4 -8
- package/dist/runtime/server/utils/idp.d.ts +2 -7
- package/dist/runtime/server/utils/idp.js +16 -64
- package/dist/runtime/server/utils/integration-key.d.ts +18 -0
- package/dist/runtime/server/utils/local-auth.d.ts +351 -0
- package/dist/runtime/server/utils/local-auth.js +43 -0
- package/dist/runtime/server/utils/oidc.d.ts +23 -2
- package/dist/runtime/server/utils/oidc.js +82 -5
- package/dist/runtime/server/utils/session.d.ts +30 -9
- package/dist/runtime/server/utils/session.js +189 -61
- package/dist/runtime/types.d.ts +12 -0
- package/package.json +9 -33
|
@@ -35,6 +35,7 @@ export declare function useAuth(): {
|
|
|
35
35
|
name: string | null;
|
|
36
36
|
picture: string | null;
|
|
37
37
|
} | null;
|
|
38
|
+
expiresAt?: number | undefined;
|
|
38
39
|
} | null, {
|
|
39
40
|
user: {
|
|
40
41
|
sub: string;
|
|
@@ -69,6 +70,7 @@ export declare function useAuth(): {
|
|
|
69
70
|
name: string | null;
|
|
70
71
|
picture: string | null;
|
|
71
72
|
} | null;
|
|
73
|
+
expiresAt?: number | undefined;
|
|
72
74
|
} | null>;
|
|
73
75
|
user: import("vue").ComputedRef<{
|
|
74
76
|
sub: string;
|
|
@@ -118,5 +120,8 @@ export declare function useAuth(): {
|
|
|
118
120
|
prompt?: SignInPrompt;
|
|
119
121
|
}) => string | false | void | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric | Promise<false | void | import("vue-router").NavigationFailure>;
|
|
120
122
|
signOut: () => Promise<void>;
|
|
123
|
+
logout: () => Promise<string | false | void | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric | import("vue-router").NavigationFailure>;
|
|
124
|
+
switchAccount: (redirect?: string) => string | false | void | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric | Promise<false | void | import("vue-router").NavigationFailure>;
|
|
121
125
|
refresh: () => Promise<void>;
|
|
126
|
+
recoverSession: () => Promise<boolean>;
|
|
122
127
|
};
|
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
|
-
import { navigateTo, useRuntimeConfig, useState } from "#app";
|
|
2
|
+
import { clearNuxtData, navigateTo, useNuxtApp, useRequestFetch, useRuntimeConfig, useState } from "#app";
|
|
3
|
+
import { safeReturnPath } from "../utils/return-path.js";
|
|
4
|
+
import { recoverLocalSession } from "../utils/session-recovery.js";
|
|
5
|
+
const pendingRecovery = /* @__PURE__ */ new WeakMap();
|
|
3
6
|
export function useAuth() {
|
|
4
7
|
const session = useState("tco-auth-session", () => null);
|
|
5
|
-
const
|
|
8
|
+
const config = useRuntimeConfig().public.auth;
|
|
9
|
+
const routes = { login: "/auth/login", signIn: "/auth/sign-in", home: "/", ...config?.routes };
|
|
10
|
+
const requestFetch = useRequestFetch();
|
|
11
|
+
const nuxtApp = useNuxtApp();
|
|
12
|
+
function broadcastSession() {
|
|
13
|
+
if (!import.meta.client)
|
|
14
|
+
return;
|
|
15
|
+
try {
|
|
16
|
+
localStorage.setItem(`${config.logoutIntentCookieName}:changed`, crypto.randomUUID());
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
6
20
|
const user = computed(() => session.value?.user ?? null);
|
|
7
21
|
const loggedIn = computed(() => !!session.value);
|
|
8
22
|
const abilities = computed(() => session.value?.abilities ?? []);
|
|
9
23
|
const impersonator = computed(() => session.value?.impersonator ?? null);
|
|
10
24
|
const isImpersonating = computed(() => !!session.value?.impersonator);
|
|
11
25
|
async function refresh() {
|
|
12
|
-
|
|
26
|
+
const previous = session.value;
|
|
27
|
+
const checked = await requestFetch("/api/_auth/session");
|
|
28
|
+
if (session.value === previous)
|
|
29
|
+
session.value = checked;
|
|
30
|
+
}
|
|
31
|
+
async function recoverSession() {
|
|
32
|
+
const pending = pendingRecovery.get(nuxtApp);
|
|
33
|
+
if (pending)
|
|
34
|
+
return pending;
|
|
35
|
+
const recovery = recoverLocalSession(session, () => requestFetch("/api/_auth/session")).finally(() => pendingRecovery.delete(nuxtApp));
|
|
36
|
+
pendingRecovery.set(nuxtApp, recovery);
|
|
37
|
+
return recovery;
|
|
13
38
|
}
|
|
14
39
|
function getOrganizations() {
|
|
15
40
|
return session.value?.organizations ?? [];
|
|
@@ -19,6 +44,8 @@ export function useAuth() {
|
|
|
19
44
|
method: "POST",
|
|
20
45
|
body: { organizationId: orgId }
|
|
21
46
|
});
|
|
47
|
+
clearNuxtData();
|
|
48
|
+
broadcastSession();
|
|
22
49
|
}
|
|
23
50
|
function getOrganizationUsers(organizationId, query) {
|
|
24
51
|
return $fetch(
|
|
@@ -34,24 +61,34 @@ export function useAuth() {
|
|
|
34
61
|
method: "POST",
|
|
35
62
|
body: { userId }
|
|
36
63
|
});
|
|
64
|
+
clearNuxtData();
|
|
65
|
+
broadcastSession();
|
|
37
66
|
}
|
|
38
67
|
async function stopImpersonating() {
|
|
39
68
|
session.value = await $fetch("/api/_auth/stop-impersonating", { method: "POST" });
|
|
69
|
+
clearNuxtData();
|
|
70
|
+
broadcastSession();
|
|
40
71
|
}
|
|
41
72
|
function signIn(redirect, options) {
|
|
42
73
|
const params = new URLSearchParams();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (options?.prompt)
|
|
46
|
-
params.set("prompt", options.prompt);
|
|
74
|
+
params.set("redirect", safeReturnPath(redirect, routes.home));
|
|
75
|
+
params.set("prompt", options?.prompt ?? "select_account");
|
|
47
76
|
const qs = params.toString();
|
|
48
77
|
return navigateTo(qs ? `${routes.signIn}?${qs}` : routes.signIn, { external: true });
|
|
49
78
|
}
|
|
50
79
|
async function signOut() {
|
|
51
80
|
const signOutRoute = "/api/_auth/sign-out";
|
|
52
|
-
await $fetch(signOutRoute, { method: "POST" })
|
|
53
|
-
});
|
|
81
|
+
await $fetch(signOutRoute, { method: "POST" });
|
|
54
82
|
session.value = null;
|
|
83
|
+
clearNuxtData();
|
|
84
|
+
broadcastSession();
|
|
85
|
+
}
|
|
86
|
+
async function logout() {
|
|
87
|
+
await signOut();
|
|
88
|
+
return navigateTo(`${routes.login}?loggedout=1`, { external: true });
|
|
89
|
+
}
|
|
90
|
+
function switchAccount(redirect) {
|
|
91
|
+
return signIn(redirect, { prompt: "select_account" });
|
|
55
92
|
}
|
|
56
93
|
return {
|
|
57
94
|
session,
|
|
@@ -68,6 +105,9 @@ export function useAuth() {
|
|
|
68
105
|
stopImpersonating,
|
|
69
106
|
signIn,
|
|
70
107
|
signOut,
|
|
71
|
-
|
|
108
|
+
logout,
|
|
109
|
+
switchAccount,
|
|
110
|
+
refresh,
|
|
111
|
+
recoverSession
|
|
72
112
|
};
|
|
73
113
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Shared behavior; each application keeps its own presentation and optional demo UI. */
|
|
2
|
+
export declare function useAuthLogin(): {
|
|
3
|
+
error: import("vue").ComputedRef<string | null>;
|
|
4
|
+
loggedOut: import("vue").ComputedRef<boolean>;
|
|
5
|
+
loading: import("vue").Ref<boolean, boolean>;
|
|
6
|
+
redirectTo: import("vue").ComputedRef<string>;
|
|
7
|
+
restoreAvailable: import("vue").ComputedRef<boolean>;
|
|
8
|
+
handleRestoreSession: () => Promise<void>;
|
|
9
|
+
handleSignIn: () => Promise<void>;
|
|
10
|
+
handleUseAnotherAccount: () => Promise<void>;
|
|
11
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import { navigateTo, useAsyncData, useCookie, useRequestFetch, useRoute, useRuntimeConfig } from "#app";
|
|
3
|
+
import { safeReturnPath } from "../utils/return-path.js";
|
|
4
|
+
import { useAuth } from "./useAuth.js";
|
|
5
|
+
const ERROR_MESSAGES = {
|
|
6
|
+
access_denied: "Sign-in was canceled or this account cannot access this application.",
|
|
7
|
+
invalid_state: "This sign-in attempt expired or was replaced. Please try again.",
|
|
8
|
+
invalid_id_token: "Your identity could not be verified. Please sign in again.",
|
|
9
|
+
id_token_invalid: "Your identity could not be verified. Please sign in again.",
|
|
10
|
+
id_token_missing: "Your identity could not be verified. Please sign in again.",
|
|
11
|
+
token_exchange_failed: "We could not complete sign-in. Please try again.",
|
|
12
|
+
userinfo_invalid: "Your account information could not be verified. Please try again.",
|
|
13
|
+
email_unverified: "Verify your email address in THECODEORIGIN ID, then try again."
|
|
14
|
+
};
|
|
15
|
+
export function useAuthLogin() {
|
|
16
|
+
const route = useRoute();
|
|
17
|
+
const { signIn, switchAccount, stopImpersonating } = useAuth();
|
|
18
|
+
const config = useRuntimeConfig().public.auth;
|
|
19
|
+
const logoutIntent = useCookie(config.logoutIntentCookieName);
|
|
20
|
+
const loading = ref(false);
|
|
21
|
+
const localError = ref(null);
|
|
22
|
+
const requestFetch = useRequestFetch();
|
|
23
|
+
const { data: restore } = useAsyncData("tco-auth-session-restore", async () => {
|
|
24
|
+
try {
|
|
25
|
+
return await requestFetch("/api/_auth/session-restore");
|
|
26
|
+
} catch {
|
|
27
|
+
return { available: false };
|
|
28
|
+
}
|
|
29
|
+
}, { default: () => ({ available: false }) });
|
|
30
|
+
const restoreAvailable = computed(() => restore.value.available);
|
|
31
|
+
const error = computed(() => localError.value ?? (typeof route.query.error === "string" ? ERROR_MESSAGES[route.query.error] ?? "Sign-in could not be completed. Please try again." : null));
|
|
32
|
+
const loggedOut = computed(() => route.query.loggedout != null || logoutIntent.value === "1");
|
|
33
|
+
const redirectTo = computed(() => {
|
|
34
|
+
const target = safeReturnPath(route.query.redirect, config.routes.home);
|
|
35
|
+
const pathname = target.split(/[?#]/u)[0];
|
|
36
|
+
return [config.routes.login, config.routes.signIn, config.routes.callback, config.routes.signOut].includes(pathname) ? config.routes.home : target;
|
|
37
|
+
});
|
|
38
|
+
async function start(anotherAccount) {
|
|
39
|
+
if (loading.value)
|
|
40
|
+
return;
|
|
41
|
+
loading.value = true;
|
|
42
|
+
localError.value = null;
|
|
43
|
+
try {
|
|
44
|
+
await (anotherAccount ? switchAccount(redirectTo.value) : signIn(redirectTo.value));
|
|
45
|
+
} catch {
|
|
46
|
+
localError.value = "Unable to open sign-in. Please try again.";
|
|
47
|
+
loading.value = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function handleRestoreSession() {
|
|
51
|
+
if (loading.value)
|
|
52
|
+
return;
|
|
53
|
+
loading.value = true;
|
|
54
|
+
localError.value = null;
|
|
55
|
+
try {
|
|
56
|
+
await stopImpersonating();
|
|
57
|
+
await navigateTo(redirectTo.value, { external: true });
|
|
58
|
+
} catch {
|
|
59
|
+
localError.value = "Unable to restore your session. Please sign in again.";
|
|
60
|
+
loading.value = false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
error,
|
|
65
|
+
loggedOut,
|
|
66
|
+
loading,
|
|
67
|
+
redirectTo,
|
|
68
|
+
restoreAvailable,
|
|
69
|
+
handleRestoreSession,
|
|
70
|
+
handleSignIn: () => start(false),
|
|
71
|
+
handleUseAnotherAccount: () => start(true)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -6,6 +6,8 @@ export default defineNuxtRouteMiddleware((to) => {
|
|
|
6
6
|
const routes = authConfig?.routes;
|
|
7
7
|
const logoutIntentCookieName = authConfig?.logoutIntentCookieName ?? "tco_auth_logout";
|
|
8
8
|
const logoutIntent = useCookie(logoutIntentCookieName);
|
|
9
|
+
if (session.value?.expiresAt && session.value.expiresAt <= Date.now())
|
|
10
|
+
session.value = null;
|
|
9
11
|
const authed = !!session.value;
|
|
10
12
|
const isPublic = to.meta.public === true || to.meta.unauthenticatedOnly === true;
|
|
11
13
|
if (authed && to.meta.unauthenticatedOnly)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { watch } from "vue";
|
|
2
|
+
import { defineNuxtPlugin, useRuntimeConfig, useState } from "#app";
|
|
3
|
+
function identity(session) {
|
|
4
|
+
return JSON.stringify([session?.user.sub, session?.activeOrg, session?.impersonator?.sub, session?.expiresAt]);
|
|
5
|
+
}
|
|
6
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
7
|
+
const session = useState("tco-auth-session", () => null);
|
|
8
|
+
const config = useRuntimeConfig().public.auth;
|
|
9
|
+
const key = `${config.logoutIntentCookieName}:changed`;
|
|
10
|
+
let checking = false;
|
|
11
|
+
let expiryTimer;
|
|
12
|
+
watch(() => session.value?.expiresAt, (expiresAt) => {
|
|
13
|
+
clearTimeout(expiryTimer);
|
|
14
|
+
if (expiresAt)
|
|
15
|
+
expiryTimer = setTimeout(() => window.location.reload(), Math.max(0, expiresAt - Date.now()));
|
|
16
|
+
}, { immediate: true });
|
|
17
|
+
async function revalidate() {
|
|
18
|
+
if (checking || document.visibilityState === "hidden")
|
|
19
|
+
return;
|
|
20
|
+
checking = true;
|
|
21
|
+
const previous = session.value;
|
|
22
|
+
try {
|
|
23
|
+
const checked = await $fetch("/api/_auth/session");
|
|
24
|
+
if (session.value === previous && identity(checked) !== identity(previous))
|
|
25
|
+
window.location.reload();
|
|
26
|
+
} catch {
|
|
27
|
+
} finally {
|
|
28
|
+
checking = false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
window.addEventListener("focus", revalidate);
|
|
32
|
+
document.addEventListener("visibilitychange", revalidate);
|
|
33
|
+
window.addEventListener("storage", (event) => {
|
|
34
|
+
if (event.key === key)
|
|
35
|
+
void revalidate();
|
|
36
|
+
});
|
|
37
|
+
nuxtApp.hook("app:mounted", () => {
|
|
38
|
+
try {
|
|
39
|
+
const current = identity(session.value);
|
|
40
|
+
if (localStorage.getItem(`${key}:identity`) !== current) {
|
|
41
|
+
localStorage.setItem(`${key}:identity`, current);
|
|
42
|
+
localStorage.setItem(key, crypto.randomUUID());
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function safeReturnPath(target: unknown, fallback?: string): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function containsUnsafeCharacters(value) {
|
|
2
|
+
return [...value].some((character) => character === "\\" || character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
3
|
+
}
|
|
4
|
+
export function safeReturnPath(target, fallback = "/") {
|
|
5
|
+
if (typeof target !== "string" || !target.startsWith("/") || target.startsWith("//") || containsUnsafeCharacters(target))
|
|
6
|
+
return fallback;
|
|
7
|
+
try {
|
|
8
|
+
const decoded = decodeURIComponent(target);
|
|
9
|
+
if (decoded.startsWith("//") || containsUnsafeCharacters(decoded))
|
|
10
|
+
return fallback;
|
|
11
|
+
const url = new URL(target, "https://application.invalid");
|
|
12
|
+
if (url.origin !== "https://application.invalid")
|
|
13
|
+
return fallback;
|
|
14
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
15
|
+
} catch {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PublicSession } from '../../../contract/index.js';
|
|
2
|
+
/** A failed request cannot prove that authentication has ended. */
|
|
3
|
+
export declare function recoverLocalSession(state: {
|
|
4
|
+
value: PublicSession | null;
|
|
5
|
+
}, fetchSession: () => Promise<PublicSession | null>, wait?: (ms: number) => Promise<void>): Promise<boolean>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export async function recoverLocalSession(state, fetchSession, wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
2
|
+
const previous = state.value;
|
|
3
|
+
for (const delay of [0, 250, 750]) {
|
|
4
|
+
if (delay)
|
|
5
|
+
await wait(delay);
|
|
6
|
+
if (state.value !== previous)
|
|
7
|
+
return state.value !== null;
|
|
8
|
+
let checked;
|
|
9
|
+
try {
|
|
10
|
+
checked = await fetchSession();
|
|
11
|
+
} catch {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (state.value !== previous)
|
|
15
|
+
return state.value !== null;
|
|
16
|
+
if (checked) {
|
|
17
|
+
state.value = checked;
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
state.value = null;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
@@ -1,38 +1,58 @@
|
|
|
1
1
|
import { createError, defineEventHandler, readValidatedBody } from "h3";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { UserinfoClaimsSchema } from "../../../../contract";
|
|
3
4
|
import { idpFetch } from "../../utils/idp.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
5
|
+
import { requireSameOrigin } from "../../utils/oidc.js";
|
|
6
|
+
import { deleteSessionBackup, issueSession, newSessionId, readSessionRecord, setSessionRestoreCookie, toPublicSession, writeSessionRecord } from "../../utils/session.js";
|
|
7
|
+
const bodySchema = z.object({ userId: z.string().min(1) });
|
|
8
|
+
const responseSchema = z.object({
|
|
9
|
+
accessToken: z.string().min(1),
|
|
10
|
+
expiresAt: z.number().finite(),
|
|
11
|
+
user: z.object({ sub: z.string().min(1), email: z.email(), name: z.string().nullable().optional(), picture: z.string().nullable().optional() }),
|
|
12
|
+
claims: UserinfoClaimsSchema
|
|
13
|
+
});
|
|
6
14
|
export default defineEventHandler(async (event) => {
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
requireSameOrigin(event);
|
|
16
|
+
const session = await readSessionRecord(event);
|
|
17
|
+
if (!session)
|
|
9
18
|
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
10
|
-
if (
|
|
19
|
+
if (session.rec.isImpersonation)
|
|
11
20
|
throw createError({ statusCode: 400, statusMessage: "Already impersonating" });
|
|
21
|
+
if (session.rec.systemRole !== "admin")
|
|
22
|
+
throw createError({ statusCode: 403, statusMessage: "Forbidden" });
|
|
12
23
|
const { userId } = await readValidatedBody(event, bodySchema.parse);
|
|
13
|
-
const res = await idpFetch(event,
|
|
24
|
+
const res = responseSchema.parse(await idpFetch(event, session.id, session.rec, "/api/auth/rp/impersonate", {
|
|
14
25
|
method: "POST",
|
|
15
26
|
body: { userId }
|
|
16
|
-
});
|
|
27
|
+
}));
|
|
28
|
+
if (res.user.sub !== userId)
|
|
29
|
+
throw createError({ statusCode: 502, statusMessage: "Identity service returned a different user" });
|
|
17
30
|
const backupId = newSessionId();
|
|
18
|
-
await writeSessionRecord(backupId,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
await writeSessionRecord(backupId, session.rec);
|
|
32
|
+
try {
|
|
33
|
+
const organizationAuthorizations = { ...res.claims.organizationAuthorizations };
|
|
34
|
+
if (res.claims.org && !Object.hasOwn(organizationAuthorizations, res.claims.org))
|
|
35
|
+
organizationAuthorizations[res.claims.org] = res.claims.abilities;
|
|
36
|
+
const next = await issueSession(event, {
|
|
37
|
+
sub: res.user.sub,
|
|
38
|
+
user: { sub: res.user.sub, email: res.user.email, name: res.user.name ?? null, picture: res.user.picture ?? null },
|
|
39
|
+
abilities: res.claims.abilities,
|
|
40
|
+
organizationAuthorizations,
|
|
41
|
+
systemRole: null,
|
|
42
|
+
organizations: res.claims.organizations,
|
|
43
|
+
activeOrg: res.claims.org,
|
|
44
|
+
entitlement: res.claims.entitlement,
|
|
45
|
+
accessToken: res.accessToken,
|
|
46
|
+
accessExpiresAt: res.expiresAt,
|
|
47
|
+
expiresAt: Math.min(session.rec.expiresAt, res.expiresAt),
|
|
48
|
+
isImpersonation: true,
|
|
49
|
+
impersonator: session.rec.user,
|
|
50
|
+
backupId
|
|
51
|
+
}, { expectedPreviousId: session.id });
|
|
52
|
+
await setSessionRestoreCookie(event, backupId, session.rec);
|
|
53
|
+
return toPublicSession(next.rec);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
await deleteSessionBackup(backupId);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
38
58
|
});
|
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import { createError, defineEventHandler, readValidatedBody } from "h3";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import { readSessionRecord, toPublicSession
|
|
3
|
+
import { requireSameOrigin } from "../../../utils/oidc.js";
|
|
4
|
+
import { issueSession, readSessionRecord, toPublicSession } from "../../../utils/session.js";
|
|
5
5
|
const bodySchema = z.object({ organizationId: z.string().min(1) });
|
|
6
6
|
export default defineEventHandler(async (event) => {
|
|
7
|
+
requireSameOrigin(event);
|
|
7
8
|
const session = await readSessionRecord(event);
|
|
8
9
|
if (!session)
|
|
9
10
|
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
10
11
|
const { organizationId } = await readValidatedBody(event, bodySchema.parse);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
session.rec.abilities = context.abilities;
|
|
21
|
-
session.rec.systemRole = context.systemRole;
|
|
22
|
-
session.rec.entitlement = context.entitlement;
|
|
23
|
-
session.rec.authorizationRefreshedAt = Date.now();
|
|
24
|
-
await writeSessionRecord(session.id, session.rec);
|
|
25
|
-
return toPublicSession(session.rec);
|
|
12
|
+
if (!session.rec.organizations.some((organization) => organization.id === organizationId) || !Object.hasOwn(session.rec.organizationAuthorizations, organizationId)) {
|
|
13
|
+
throw createError({ statusCode: 403, statusMessage: "Sign in again to access this organization" });
|
|
14
|
+
}
|
|
15
|
+
const next = await issueSession(event, {
|
|
16
|
+
...session.rec,
|
|
17
|
+
activeOrg: organizationId,
|
|
18
|
+
abilities: session.rec.organizationAuthorizations[organizationId]
|
|
19
|
+
}, { expectedPreviousId: session.id });
|
|
20
|
+
return toPublicSession(next.rec);
|
|
26
21
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { defineEventHandler, setResponseHeader } from "h3";
|
|
2
|
+
import { readSessionRestore } from "../../utils/session.js";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
setResponseHeader(event, "cache-control", "no-store");
|
|
5
|
+
return { available: Boolean(await readSessionRestore(event)) };
|
|
6
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { defineEventHandler } from "h3";
|
|
2
2
|
import { setLogoutIntent } from "../../utils/logout-intent.js";
|
|
3
|
+
import { clearPendingAuthentication, requireSameOrigin } from "../../utils/oidc.js";
|
|
3
4
|
import { destroySession } from "../../utils/session.js";
|
|
4
5
|
export default defineEventHandler(async (event) => {
|
|
6
|
+
requireSameOrigin(event);
|
|
7
|
+
await clearPendingAuthentication(event);
|
|
5
8
|
await destroySession(event);
|
|
6
9
|
setLogoutIntent(event);
|
|
7
10
|
return { ok: true };
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { createError, defineEventHandler } from "h3";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { readSessionRecord, readSessionRecordById, toPublicSession, writeSessionRecord } from "../../utils/session.js";
|
|
2
|
+
import { requireSameOrigin } from "../../utils/oidc.js";
|
|
3
|
+
import { clearSessionRestoreCookie, deleteSessionBackup, issueSession, readSessionRecord, readSessionRestore, toPublicSession } from "../../utils/session.js";
|
|
5
4
|
export default defineEventHandler(async (event) => {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
requireSameOrigin(event);
|
|
6
|
+
const session = await readSessionRecord(event);
|
|
7
|
+
const restore = await readSessionRestore(event);
|
|
8
|
+
if (session && !session.rec.isImpersonation)
|
|
8
9
|
throw createError({ statusCode: 400, statusMessage: "Not impersonating" });
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
await
|
|
19
|
-
|
|
10
|
+
if (!restore || session && (session.rec.backupId !== restore.id || session.rec.impersonator?.sub !== restore.rec.sub))
|
|
11
|
+
throw createError({ statusCode: 401, statusMessage: "Original session expired; sign in again" });
|
|
12
|
+
const next = await issueSession(event, restore.rec, {
|
|
13
|
+
expectedPreviousId: session?.id ?? null,
|
|
14
|
+
validate: async () => {
|
|
15
|
+
if ((await readSessionRestore(event))?.id !== restore.id)
|
|
16
|
+
throw createError({ statusCode: 409, statusMessage: "Original session was revoked" });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
await deleteSessionBackup(restore.id);
|
|
20
|
+
clearSessionRestoreCookie(event);
|
|
21
|
+
return toPublicSession(next.rec);
|
|
20
22
|
});
|
|
@@ -3,5 +3,5 @@ export type { ServerAbility } from './utils/ability.js';
|
|
|
3
3
|
export { resolveOrganizationUser } from './utils/directory.js';
|
|
4
4
|
export { defineAuthorizedHandler, defineSensitiveAuthorizedHandler, } from './utils/handlers.js';
|
|
5
5
|
export { verifyOrganizationIntegrationKey } from './utils/integration-key.js';
|
|
6
|
-
export { getFreshServerAuthSession, getServerAuthSession } from './utils/session.js';
|
|
7
|
-
export type { ServerAuthSession } from './utils/session.js';
|
|
6
|
+
export { getFreshServerAuthSession, getServerAuthSession, issueSession } from './utils/session.js';
|
|
7
|
+
export type { ServerAuthSession, SessionInput, SessionRecord } from './utils/session.js';
|
|
@@ -10,4 +10,4 @@ export {
|
|
|
10
10
|
defineSensitiveAuthorizedHandler
|
|
11
11
|
} from "./utils/handlers.js";
|
|
12
12
|
export { verifyOrganizationIntegrationKey } from "./utils/integration-key.js";
|
|
13
|
-
export { getFreshServerAuthSession, getServerAuthSession } from "./utils/session.js";
|
|
13
|
+
export { getFreshServerAuthSession, getServerAuthSession, issueSession } from "./utils/session.js";
|