@snokam/auth 5.5.4 → 5.5.5
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/CHANGELOG.md +6 -0
- package/package.json +3 -3
- package/src/app-router/index.tsx +2 -1
- package/src/hooks.ts +47 -26
- package/src/identity/own.tsx +17 -0
- package/src/roles/context.tsx +14 -6
- package/src/route-handler/token.ts +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [5.5.5](https://github.com/snokam/components/compare/@snokam/auth@5.5.4...@snokam/auth@5.5.5) (2026-10-28)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **platform-web:** platform sections run as yourself, also while viewing as a persona ([#3046](https://github.com/snokam/components/issues/3046)) ([3ef73be](https://github.com/snokam/components/commit/3ef73be782f088cb4fe2174ab754d172aecb1bb1))
|
|
11
|
+
|
|
6
12
|
## [5.5.4](https://github.com/snokam/components/compare/@snokam/auth@5.5.3...@snokam/auth@5.5.4) (2026-10-28)
|
|
7
13
|
|
|
8
14
|
**Note:** Version bump only for package @snokam/auth
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snokam/auth",
|
|
3
3
|
"description": "Snøkam auth components",
|
|
4
|
-
"version": "5.5.
|
|
4
|
+
"version": "5.5.5",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
7
7
|
"files": [
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"@snokam/employees-client": "^1.3.0",
|
|
27
27
|
"next-auth": "4.24.15",
|
|
28
28
|
"path-to-regexp": "^8.4.2",
|
|
29
|
+
"@snokam/identity": "1.1.0",
|
|
29
30
|
"@snokam/platform-client": "5.3.2",
|
|
30
31
|
"@snokam/react-query": "1.1.0",
|
|
31
32
|
"@snokam/sanity-page-content": "6.8.1",
|
|
32
33
|
"@snokam/tenant": "4.4.1",
|
|
33
|
-
"@snokam/ui": "6.4.1"
|
|
34
|
-
"@snokam/identity": "1.1.0"
|
|
34
|
+
"@snokam/ui": "6.4.1"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"next": "16.3.8",
|
package/src/app-router/index.tsx
CHANGED
|
@@ -21,7 +21,8 @@ export {
|
|
|
21
21
|
PLATFORM_ADMIN,
|
|
22
22
|
PLATFORM_OPERATOR,
|
|
23
23
|
} from "../roles/context";
|
|
24
|
-
export type { RolesState } from "../roles/context";
|
|
24
|
+
export type { RolesState, PlatformRolesOptions } from "../roles/context";
|
|
25
|
+
export { OwnIdentity } from "../identity/own";
|
|
25
26
|
export type { ScopesState } from "../scopes";
|
|
26
27
|
export {
|
|
27
28
|
scopeGranted,
|
package/src/hooks.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { signIn, signOut } from "next-auth/react";
|
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { IMP_ACTIVE_COOKIE } from "./impersonation/cookies";
|
|
4
4
|
import { useImpersonation } from "./impersonation/context";
|
|
5
|
+
import { useOwnIdentity } from "./identity/own";
|
|
5
6
|
import { useSession } from "./session/useSession";
|
|
6
7
|
|
|
7
8
|
class ConsentRequiredError extends Error {}
|
|
@@ -14,8 +15,16 @@ interface CachedToken {
|
|
|
14
15
|
impKey: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
type TokenKind = "session" | "own";
|
|
19
|
+
|
|
20
|
+
const cachedTokens: Record<TokenKind, CachedToken | null> = {
|
|
21
|
+
session: null,
|
|
22
|
+
own: null,
|
|
23
|
+
};
|
|
24
|
+
const inFlightTokens: Record<
|
|
25
|
+
TokenKind,
|
|
26
|
+
{ impKey: string; promise: Promise<string> } | null
|
|
27
|
+
> = { session: null, own: null };
|
|
19
28
|
|
|
20
29
|
function readImpersonationKey(): string {
|
|
21
30
|
if (typeof document === "undefined") return "";
|
|
@@ -38,14 +47,16 @@ function readImpersonationKey(): string {
|
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
export function clearAuthTokenCache(): void {
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
cachedTokens.session = null;
|
|
51
|
+
cachedTokens.own = null;
|
|
52
|
+
inFlightTokens.session = null;
|
|
53
|
+
inFlightTokens.own = null;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
|
-
async function fetchToken(impKey: string): Promise<string> {
|
|
56
|
+
async function fetchToken(kind: TokenKind, impKey: string): Promise<string> {
|
|
46
57
|
const res = await fetch("/api/auth/token", {
|
|
47
58
|
method: "POST",
|
|
48
|
-
body: JSON.stringify({}),
|
|
59
|
+
body: JSON.stringify(kind === "own" ? { own: true } : {}),
|
|
49
60
|
credentials: "include",
|
|
50
61
|
headers: {
|
|
51
62
|
"Content-Type": "application/json",
|
|
@@ -68,39 +79,57 @@ async function fetchToken(impKey: string): Promise<string> {
|
|
|
68
79
|
|
|
69
80
|
sessionStorage.removeItem("auth-consent-retried");
|
|
70
81
|
const bearer = `Bearer ${accessToken}`;
|
|
71
|
-
|
|
82
|
+
cachedTokens[kind] = { bearer, expiresOn, impKey };
|
|
72
83
|
return bearer;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
|
-
async function getFreshToken(): Promise<string> {
|
|
76
|
-
const impKey = readImpersonationKey();
|
|
86
|
+
async function getFreshToken(kind: TokenKind): Promise<string> {
|
|
87
|
+
const impKey = kind === "own" ? "" : readImpersonationKey();
|
|
77
88
|
const now = Date.now() / 1000;
|
|
89
|
+
const cached = cachedTokens[kind];
|
|
78
90
|
|
|
79
91
|
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
now <
|
|
92
|
+
cached &&
|
|
93
|
+
cached.impKey === impKey &&
|
|
94
|
+
now < cached.expiresOn - REFRESH_MARGIN_SECONDS
|
|
83
95
|
) {
|
|
84
|
-
return
|
|
96
|
+
return cached.bearer;
|
|
85
97
|
}
|
|
86
98
|
|
|
99
|
+
const inFlight = inFlightTokens[kind];
|
|
87
100
|
if (inFlight && inFlight.impKey === impKey) {
|
|
88
101
|
return inFlight.promise;
|
|
89
102
|
}
|
|
90
103
|
|
|
91
|
-
const promise = fetchToken(impKey);
|
|
92
|
-
|
|
104
|
+
const promise = fetchToken(kind, impKey);
|
|
105
|
+
inFlightTokens[kind] = { impKey, promise };
|
|
93
106
|
promise
|
|
94
107
|
.catch(() => undefined)
|
|
95
108
|
.finally(() => {
|
|
96
|
-
if (
|
|
109
|
+
if (inFlightTokens[kind]?.promise === promise)
|
|
110
|
+
inFlightTokens[kind] = null;
|
|
97
111
|
});
|
|
98
112
|
return promise;
|
|
99
113
|
}
|
|
100
114
|
|
|
115
|
+
export function fetchOwnToken(): Promise<string> {
|
|
116
|
+
return tokenOrLogin("own");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function tokenOrLogin(kind: TokenKind): Promise<string> {
|
|
120
|
+
return getFreshToken(kind).catch((error) => {
|
|
121
|
+
console.error(error);
|
|
122
|
+
const onAuthPage = window.location.pathname.startsWith("/auth/");
|
|
123
|
+
if (onAuthPage) return;
|
|
124
|
+
const reason = error instanceof ConsentRequiredError ? "consent" : "true";
|
|
125
|
+
window.location.href = `/auth/login?error=${reason}`;
|
|
126
|
+
}) as Promise<string>;
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
export function useAuth() {
|
|
102
130
|
const { data: rawSession, status, update } = useSession();
|
|
103
131
|
const { active } = useImpersonation();
|
|
132
|
+
const own = useOwnIdentity();
|
|
104
133
|
|
|
105
134
|
const session = useMemo(() => {
|
|
106
135
|
if (!active) return rawSession;
|
|
@@ -123,15 +152,7 @@ export function useAuth() {
|
|
|
123
152
|
signIn,
|
|
124
153
|
signOut,
|
|
125
154
|
session,
|
|
126
|
-
getToken: () =>
|
|
127
|
-
getFreshToken().catch((error) => {
|
|
128
|
-
console.error(error);
|
|
129
|
-
const onAuthPage = window.location.pathname.startsWith("/auth/");
|
|
130
|
-
if (onAuthPage) return;
|
|
131
|
-
const reason =
|
|
132
|
-
error instanceof ConsentRequiredError ? "consent" : "true";
|
|
133
|
-
window.location.href = `/auth/login?error=${reason}`;
|
|
134
|
-
}) as Promise<string>,
|
|
155
|
+
getToken: () => tokenOrLogin(own ? "own" : "session"),
|
|
135
156
|
inProgress: status === "loading",
|
|
136
157
|
isAuthenticated:
|
|
137
158
|
status === "authenticated" ||
|
|
@@ -139,7 +160,7 @@ export function useAuth() {
|
|
|
139
160
|
(status === "loading" && !!session),
|
|
140
161
|
}),
|
|
141
162
|
|
|
142
|
-
[update, status, session, active]
|
|
163
|
+
[update, status, session, active, own]
|
|
143
164
|
);
|
|
144
165
|
|
|
145
166
|
return value;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
const OwnIdentityContext = createContext(false);
|
|
6
|
+
|
|
7
|
+
export function OwnIdentity({ children }: { children: ReactNode }) {
|
|
8
|
+
return (
|
|
9
|
+
<OwnIdentityContext.Provider value={true}>
|
|
10
|
+
{children}
|
|
11
|
+
</OwnIdentityContext.Provider>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useOwnIdentity(): boolean {
|
|
16
|
+
return useContext(OwnIdentityContext);
|
|
17
|
+
}
|
package/src/roles/context.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useState } from "react";
|
|
4
|
-
import { useAuth } from "../hooks";
|
|
4
|
+
import { fetchOwnToken, useAuth } from "../hooks";
|
|
5
5
|
import { decodeJwtClaims } from "../token/claims";
|
|
6
6
|
|
|
7
7
|
export const PLATFORM_OPERATOR = "platform-operator";
|
|
@@ -20,8 +20,13 @@ function decodeTokenRoles(bearer: string): string[] {
|
|
|
20
20
|
return [];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export
|
|
23
|
+
export interface PlatformRolesOptions {
|
|
24
|
+
own?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function usePlatformRoles(options?: PlatformRolesOptions): RolesState {
|
|
24
28
|
const { getToken, isAuthenticated } = useAuth();
|
|
29
|
+
const fetchToken = options?.own ? fetchOwnToken : getToken;
|
|
25
30
|
const [roles, setRoles] = useState<string[] | null>(null);
|
|
26
31
|
|
|
27
32
|
useEffect(() => {
|
|
@@ -30,7 +35,7 @@ export function usePlatformRoles(): RolesState {
|
|
|
30
35
|
return;
|
|
31
36
|
}
|
|
32
37
|
let cancelled = false;
|
|
33
|
-
|
|
38
|
+
fetchToken()
|
|
34
39
|
.then((bearer) => {
|
|
35
40
|
if (!cancelled) setRoles(decodeTokenRoles(bearer));
|
|
36
41
|
})
|
|
@@ -40,7 +45,7 @@ export function usePlatformRoles(): RolesState {
|
|
|
40
45
|
return () => {
|
|
41
46
|
cancelled = true;
|
|
42
47
|
};
|
|
43
|
-
}, [isAuthenticated,
|
|
48
|
+
}, [isAuthenticated, fetchToken]);
|
|
44
49
|
|
|
45
50
|
return useMemo(
|
|
46
51
|
() => ({ roles: roles ?? [], isLoading: roles === null }),
|
|
@@ -48,7 +53,10 @@ export function usePlatformRoles(): RolesState {
|
|
|
48
53
|
);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
export function useHasPlatformRole(
|
|
52
|
-
|
|
56
|
+
export function useHasPlatformRole(
|
|
57
|
+
role: string,
|
|
58
|
+
options?: PlatformRolesOptions
|
|
59
|
+
): boolean {
|
|
60
|
+
const { roles } = usePlatformRoles(options);
|
|
53
61
|
return useMemo(() => roles.includes(role), [roles, role]);
|
|
54
62
|
}
|
|
@@ -95,9 +95,18 @@ export async function resolveAccessToken(
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
async function wantsOwnToken(request: Request): Promise<boolean> {
|
|
99
|
+
const body = (await request.json().catch(() => null)) as {
|
|
100
|
+
own?: unknown;
|
|
101
|
+
} | null;
|
|
102
|
+
return body?.own === true;
|
|
103
|
+
}
|
|
104
|
+
|
|
98
105
|
export function createTokenHandler(authConfig: Auth.Config) {
|
|
99
106
|
return async function POST(request: Request) {
|
|
100
|
-
const impersonation =
|
|
107
|
+
const impersonation = (await wantsOwnToken(request))
|
|
108
|
+
? null
|
|
109
|
+
: readImpersonationSession(request);
|
|
101
110
|
if (impersonation && impersonation.expiresOn * 1000 > Date.now()) {
|
|
102
111
|
return Response.json({
|
|
103
112
|
accessToken: impersonation.accessToken,
|