@rimelight/auth 0.0.2 → 0.0.3
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/dist/adapters/auth0.d.mts +115 -0
- package/dist/adapters/auth0.mjs +256 -0
- package/dist/adapters/cf-access.d.mts +13 -0
- package/dist/adapters/cf-access.mjs +66 -0
- package/dist/adapters/mock.d.mts +9 -0
- package/dist/adapters/mock.mjs +31 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +8 -0
- package/dist/permissions/index.d.mts +47 -0
- package/dist/permissions/index.mjs +83 -0
- package/dist/plugins/construction-guest/index.d.mts +9 -0
- package/dist/plugins/construction-guest/index.mjs +51 -0
- package/dist/plugins/reserved-usernames/index.d.mts +12 -0
- package/dist/plugins/reserved-usernames/index.mjs +192 -0
- package/dist/types.d.mts +58 -0
- package/dist/types.mjs +1 -0
- package/package.json +43 -18
- package/src/client/index.ts +0 -3
- package/src/index.ts +0 -6
- package/src/permissions/index.ts +0 -27
- package/src/plugins/construction-guest/index.ts +0 -117
- package/src/plugins/discriminator-tag/index.ts +0 -55
- package/src/plugins/reserved-usernames/index.ts +0 -198
- package/src/plugins/synthetic-user/index.ts +0 -50
- package/src/schema/drizzle/index.ts +0 -363
- package/src/server/index.ts +0 -149
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { AuthAdapter, UserSessionContext } from "../types.mjs";
|
|
2
|
+
import { decodeJwt } from "jose";
|
|
3
|
+
//#region src/adapters/auth0.d.ts
|
|
4
|
+
interface Auth0AdapterOptions {
|
|
5
|
+
domain: string;
|
|
6
|
+
audience?: string | undefined;
|
|
7
|
+
clientId?: string | undefined;
|
|
8
|
+
clientSecret?: string | undefined;
|
|
9
|
+
rolesClaim?: string | undefined;
|
|
10
|
+
userTypeClaim?: string | undefined;
|
|
11
|
+
permissionsClaim?: string | undefined;
|
|
12
|
+
sessionCookieName?: string | undefined;
|
|
13
|
+
sessionSecret?: string | undefined;
|
|
14
|
+
loginUrl?: string | undefined;
|
|
15
|
+
defaultRole?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
interface Auth0AuthorizeUrlOptions {
|
|
18
|
+
domain: string;
|
|
19
|
+
clientId: string;
|
|
20
|
+
redirectUri: string;
|
|
21
|
+
audience?: string | undefined;
|
|
22
|
+
scope?: string | undefined;
|
|
23
|
+
state?: string | undefined;
|
|
24
|
+
codeChallenge?: string | undefined;
|
|
25
|
+
codeChallengeMethod?: "S256" | "plain" | undefined;
|
|
26
|
+
prompt?: string | undefined;
|
|
27
|
+
screenHint?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
interface Auth0TokenResponse {
|
|
30
|
+
access_token: string;
|
|
31
|
+
id_token?: string | undefined;
|
|
32
|
+
token_type: string;
|
|
33
|
+
expires_in: number;
|
|
34
|
+
refresh_token?: string | undefined;
|
|
35
|
+
scope?: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
interface Auth0CodeExchangeOptions {
|
|
38
|
+
domain: string;
|
|
39
|
+
clientId: string;
|
|
40
|
+
clientSecret?: string | undefined;
|
|
41
|
+
code: string;
|
|
42
|
+
redirectUri: string;
|
|
43
|
+
codeVerifier?: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
interface Auth0PasswordLoginOptions {
|
|
46
|
+
domain: string;
|
|
47
|
+
clientId: string;
|
|
48
|
+
clientSecret?: string | undefined;
|
|
49
|
+
username: string;
|
|
50
|
+
password: string;
|
|
51
|
+
audience?: string | undefined;
|
|
52
|
+
scope?: string | undefined;
|
|
53
|
+
realm?: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
interface Auth0RefreshTokenOptions {
|
|
56
|
+
domain: string;
|
|
57
|
+
clientId: string;
|
|
58
|
+
clientSecret?: string | undefined;
|
|
59
|
+
refreshToken: string;
|
|
60
|
+
scope?: string | undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Verifies an Auth0 RS256 JWT Access Token or ID Token against Auth0 JWKS endpoint
|
|
64
|
+
*/
|
|
65
|
+
declare function verifyAuth0Jwt(token: string, options: {
|
|
66
|
+
domain: string;
|
|
67
|
+
audience?: string | undefined;
|
|
68
|
+
issuer?: string | undefined;
|
|
69
|
+
}): Promise<Record<string, any> | null>;
|
|
70
|
+
/**
|
|
71
|
+
* Creates a signed compact session token (HS256) for local session cookie storage
|
|
72
|
+
*/
|
|
73
|
+
declare function createSessionToken(session: UserSessionContext, secret: string, expiresIn?: string | number): Promise<string>;
|
|
74
|
+
/**
|
|
75
|
+
* Verifies a signed session token from a session cookie
|
|
76
|
+
*/
|
|
77
|
+
declare function verifySessionToken(token: string, secret: string): Promise<UserSessionContext | null>;
|
|
78
|
+
/**
|
|
79
|
+
* Generates an Auth0 OAuth2 / OIDC authorization URL
|
|
80
|
+
*/
|
|
81
|
+
declare function createAuth0AuthorizeUrl(options: Auth0AuthorizeUrlOptions): string;
|
|
82
|
+
/**
|
|
83
|
+
* Exchanges an authorization code for Auth0 tokens
|
|
84
|
+
*/
|
|
85
|
+
declare function exchangeAuth0Code(options: Auth0CodeExchangeOptions): Promise<Auth0TokenResponse>;
|
|
86
|
+
/**
|
|
87
|
+
* Authenticates directly with Auth0 using Resource Owner Password Credentials (ROPC). Used by
|
|
88
|
+
* headless clients (e.g., Unreal Engine 5) to authenticate without browser redirects.
|
|
89
|
+
*/
|
|
90
|
+
declare function loginWithAuth0Password(options: Auth0PasswordLoginOptions): Promise<Auth0TokenResponse>;
|
|
91
|
+
/**
|
|
92
|
+
* Exchanges a refresh token for a new Auth0 access token. Ensures uninterrupted play sessions in
|
|
93
|
+
* native game clients.
|
|
94
|
+
*/
|
|
95
|
+
declare function refreshAuth0Token(options: Auth0RefreshTokenOptions): Promise<Auth0TokenResponse>;
|
|
96
|
+
/**
|
|
97
|
+
* Generates an Auth0 logout URL
|
|
98
|
+
*/
|
|
99
|
+
declare function createAuth0LogoutUrl(options: {
|
|
100
|
+
domain: string;
|
|
101
|
+
clientId: string;
|
|
102
|
+
returnTo: string;
|
|
103
|
+
}): string;
|
|
104
|
+
/**
|
|
105
|
+
* Auth0 / OIDC Auth Adapter
|
|
106
|
+
*/
|
|
107
|
+
declare function auth0Auth(options: Auth0AdapterOptions): AuthAdapter;
|
|
108
|
+
declare function mapAuth0PayloadToSession(payload: Record<string, any>, options: {
|
|
109
|
+
rolesClaim: string;
|
|
110
|
+
userTypeClaim: string;
|
|
111
|
+
permissionsClaim: string;
|
|
112
|
+
defaultRole: string;
|
|
113
|
+
}): UserSessionContext;
|
|
114
|
+
//#endregion
|
|
115
|
+
export { Auth0AdapterOptions, Auth0AuthorizeUrlOptions, Auth0CodeExchangeOptions, Auth0PasswordLoginOptions, Auth0RefreshTokenOptions, Auth0TokenResponse, auth0Auth, createAuth0AuthorizeUrl, createAuth0LogoutUrl, createSessionToken, decodeJwt, exchangeAuth0Code, loginWithAuth0Password, mapAuth0PayloadToSession, refreshAuth0Token, verifyAuth0Jwt, verifySessionToken };
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { SignJWT, createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
|
|
2
|
+
//#region src/adapters/auth0.ts
|
|
3
|
+
const jwksCache = /* @__PURE__ */ new Map();
|
|
4
|
+
function getOrCreateJwks(domain) {
|
|
5
|
+
const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
6
|
+
if (!jwksCache.has(cleanDomain)) {
|
|
7
|
+
const jwksUrl = new URL(`https://${cleanDomain}/.well-known/jwks.json`);
|
|
8
|
+
jwksCache.set(cleanDomain, createRemoteJWKSet(jwksUrl));
|
|
9
|
+
}
|
|
10
|
+
return jwksCache.get(cleanDomain);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Verifies an Auth0 RS256 JWT Access Token or ID Token against Auth0 JWKS endpoint
|
|
14
|
+
*/
|
|
15
|
+
async function verifyAuth0Jwt(token, options) {
|
|
16
|
+
try {
|
|
17
|
+
const cleanDomain = options.domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
18
|
+
const jwks = getOrCreateJwks(cleanDomain);
|
|
19
|
+
const verifyOpts = { issuer: options.issuer || `https://${cleanDomain}/` };
|
|
20
|
+
if (options.audience) verifyOpts.audience = options.audience;
|
|
21
|
+
const { payload } = await jwtVerify(token, jwks, verifyOpts);
|
|
22
|
+
return payload;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates a signed compact session token (HS256) for local session cookie storage
|
|
29
|
+
*/
|
|
30
|
+
async function createSessionToken(session, secret, expiresIn = "7d") {
|
|
31
|
+
const encSecret = new TextEncoder().encode(secret);
|
|
32
|
+
return new SignJWT({ ...session }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(encSecret);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Verifies a signed session token from a session cookie
|
|
36
|
+
*/
|
|
37
|
+
async function verifySessionToken(token, secret) {
|
|
38
|
+
try {
|
|
39
|
+
const encSecret = new TextEncoder().encode(secret);
|
|
40
|
+
const { payload } = await jwtVerify(token, encSecret, { algorithms: ["HS256"] });
|
|
41
|
+
return payload;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generates an Auth0 OAuth2 / OIDC authorization URL
|
|
48
|
+
*/
|
|
49
|
+
function createAuth0AuthorizeUrl(options) {
|
|
50
|
+
const cleanDomain = options.domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
51
|
+
const url = new URL(`https://${cleanDomain}/authorize`);
|
|
52
|
+
url.searchParams.set("response_type", "code");
|
|
53
|
+
url.searchParams.set("client_id", options.clientId);
|
|
54
|
+
url.searchParams.set("redirect_uri", options.redirectUri);
|
|
55
|
+
url.searchParams.set("scope", options.scope || "openid profile email");
|
|
56
|
+
if (options.audience) url.searchParams.set("audience", options.audience);
|
|
57
|
+
if (options.state) url.searchParams.set("state", options.state);
|
|
58
|
+
if (options.codeChallenge) {
|
|
59
|
+
url.searchParams.set("code_challenge", options.codeChallenge);
|
|
60
|
+
url.searchParams.set("code_challenge_method", options.codeChallengeMethod || "S256");
|
|
61
|
+
}
|
|
62
|
+
if (options.prompt) url.searchParams.set("prompt", options.prompt);
|
|
63
|
+
if (options.screenHint) url.searchParams.set("screen_hint", options.screenHint);
|
|
64
|
+
return url.toString();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Exchanges an authorization code for Auth0 tokens
|
|
68
|
+
*/
|
|
69
|
+
async function exchangeAuth0Code(options) {
|
|
70
|
+
const tokenUrl = `https://${options.domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}/oauth/token`;
|
|
71
|
+
const body = {
|
|
72
|
+
grant_type: "authorization_code",
|
|
73
|
+
client_id: options.clientId,
|
|
74
|
+
code: options.code,
|
|
75
|
+
redirect_uri: options.redirectUri
|
|
76
|
+
};
|
|
77
|
+
if (options.clientSecret) body.client_secret = options.clientSecret;
|
|
78
|
+
if (options.codeVerifier) body.code_verifier = options.codeVerifier;
|
|
79
|
+
const response = await fetch(tokenUrl, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json" },
|
|
82
|
+
body: JSON.stringify(body)
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const errText = await response.text();
|
|
86
|
+
throw new Error(`Auth0 token exchange failed (${response.status}): ${errText}`);
|
|
87
|
+
}
|
|
88
|
+
return await response.json();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Authenticates directly with Auth0 using Resource Owner Password Credentials (ROPC). Used by
|
|
92
|
+
* headless clients (e.g., Unreal Engine 5) to authenticate without browser redirects.
|
|
93
|
+
*/
|
|
94
|
+
async function loginWithAuth0Password(options) {
|
|
95
|
+
const tokenUrl = `https://${options.domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}/oauth/token`;
|
|
96
|
+
const body = {
|
|
97
|
+
grant_type: options.realm ? "http://auth0.com/oauth/grant-type/password-realm" : "password",
|
|
98
|
+
client_id: options.clientId,
|
|
99
|
+
username: options.username,
|
|
100
|
+
password: options.password,
|
|
101
|
+
scope: options.scope || "openid profile email offline_access"
|
|
102
|
+
};
|
|
103
|
+
if (options.realm) body.realm = options.realm;
|
|
104
|
+
if (options.clientSecret) body.client_secret = options.clientSecret;
|
|
105
|
+
if (options.audience) body.audience = options.audience;
|
|
106
|
+
const response = await fetch(tokenUrl, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "Content-Type": "application/json" },
|
|
109
|
+
body: JSON.stringify(body)
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const errText = await response.text();
|
|
113
|
+
throw new Error(`Auth0 password login failed (${response.status}): ${errText}`);
|
|
114
|
+
}
|
|
115
|
+
return await response.json();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Exchanges a refresh token for a new Auth0 access token. Ensures uninterrupted play sessions in
|
|
119
|
+
* native game clients.
|
|
120
|
+
*/
|
|
121
|
+
async function refreshAuth0Token(options) {
|
|
122
|
+
const tokenUrl = `https://${options.domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}/oauth/token`;
|
|
123
|
+
const body = {
|
|
124
|
+
grant_type: "refresh_token",
|
|
125
|
+
client_id: options.clientId,
|
|
126
|
+
refresh_token: options.refreshToken
|
|
127
|
+
};
|
|
128
|
+
if (options.clientSecret) body.client_secret = options.clientSecret;
|
|
129
|
+
if (options.scope) body.scope = options.scope;
|
|
130
|
+
const response = await fetch(tokenUrl, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: { "Content-Type": "application/json" },
|
|
133
|
+
body: JSON.stringify(body)
|
|
134
|
+
});
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
const errText = await response.text();
|
|
137
|
+
throw new Error(`Auth0 token refresh failed (${response.status}): ${errText}`);
|
|
138
|
+
}
|
|
139
|
+
return await response.json();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Generates an Auth0 logout URL
|
|
143
|
+
*/
|
|
144
|
+
function createAuth0LogoutUrl(options) {
|
|
145
|
+
const cleanDomain = options.domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
146
|
+
const url = new URL(`https://${cleanDomain}/v2/logout`);
|
|
147
|
+
url.searchParams.set("client_id", options.clientId);
|
|
148
|
+
url.searchParams.set("returnTo", options.returnTo);
|
|
149
|
+
return url.toString();
|
|
150
|
+
}
|
|
151
|
+
function parseCookie(cookieHeader, name) {
|
|
152
|
+
if (!cookieHeader) return null;
|
|
153
|
+
const cookies = cookieHeader.split(";");
|
|
154
|
+
for (const cookie of cookies) {
|
|
155
|
+
const [k, v] = cookie.trim().split("=");
|
|
156
|
+
if (k === name && v) return decodeURIComponent(v);
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Auth0 / OIDC Auth Adapter
|
|
162
|
+
*/
|
|
163
|
+
function auth0Auth(options) {
|
|
164
|
+
const { domain, audience, rolesClaim = "https://rimelight.com/roles", userTypeClaim = "https://rimelight.com/user_type", permissionsClaim = "permissions", sessionCookieName = "rimelight_session", sessionSecret, loginUrl = "/api/auth/login", defaultRole = "user" } = options;
|
|
165
|
+
return {
|
|
166
|
+
name: "auth0",
|
|
167
|
+
options,
|
|
168
|
+
async getSession(req) {
|
|
169
|
+
if (!req) return null;
|
|
170
|
+
const getHeader = (name) => {
|
|
171
|
+
try {
|
|
172
|
+
if (req.headers && typeof req.headers.get === "function") return req.headers.get(name);
|
|
173
|
+
const h = req?.headers;
|
|
174
|
+
if (h && typeof h === "object") return h[name] || h[name.toLowerCase()] || null;
|
|
175
|
+
} catch {}
|
|
176
|
+
return null;
|
|
177
|
+
};
|
|
178
|
+
const authHeader = getHeader("authorization");
|
|
179
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
180
|
+
const payload = await verifyAuth0Jwt(authHeader.slice(7).trim(), {
|
|
181
|
+
domain,
|
|
182
|
+
audience
|
|
183
|
+
});
|
|
184
|
+
if (payload) return mapAuth0PayloadToSession(payload, {
|
|
185
|
+
rolesClaim,
|
|
186
|
+
userTypeClaim,
|
|
187
|
+
permissionsClaim,
|
|
188
|
+
defaultRole
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const sessionCookie = parseCookie(getHeader("cookie"), sessionCookieName);
|
|
192
|
+
if (sessionCookie) {
|
|
193
|
+
if (sessionSecret) {
|
|
194
|
+
const verified = await verifySessionToken(sessionCookie, sessionSecret);
|
|
195
|
+
if (verified) return verified;
|
|
196
|
+
} else {
|
|
197
|
+
const payload = await verifyAuth0Jwt(sessionCookie, {
|
|
198
|
+
domain,
|
|
199
|
+
audience
|
|
200
|
+
});
|
|
201
|
+
if (payload) return mapAuth0PayloadToSession(payload, {
|
|
202
|
+
rolesClaim,
|
|
203
|
+
userTypeClaim,
|
|
204
|
+
permissionsClaim,
|
|
205
|
+
defaultRole
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
},
|
|
211
|
+
handleUnauthorized(req) {
|
|
212
|
+
if (loginUrl) {
|
|
213
|
+
try {
|
|
214
|
+
if (req && req.url) {
|
|
215
|
+
const origin = new URL(req.url).origin;
|
|
216
|
+
const fullUrl = new URL(loginUrl, origin).toString();
|
|
217
|
+
return Response.redirect(fullUrl, 302);
|
|
218
|
+
}
|
|
219
|
+
} catch {}
|
|
220
|
+
try {
|
|
221
|
+
return Response.redirect(loginUrl, 302);
|
|
222
|
+
} catch {
|
|
223
|
+
return new Response(null, {
|
|
224
|
+
status: 302,
|
|
225
|
+
headers: { Location: loginUrl }
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return new Response("Unauthorized Auth0 Identity", { status: 401 });
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function mapAuth0PayloadToSession(payload, options) {
|
|
234
|
+
const roles = Array.isArray(payload[options.rolesClaim]) ? payload[options.rolesClaim] : Array.isArray(payload.roles) ? payload.roles : [options.defaultRole];
|
|
235
|
+
const permissions = Array.isArray(payload[options.permissionsClaim]) ? payload[options.permissionsClaim] : Array.isArray(payload.permissions) ? payload.permissions : [];
|
|
236
|
+
const isStaff = roles.some((r) => [
|
|
237
|
+
"owner",
|
|
238
|
+
"admin",
|
|
239
|
+
"editor",
|
|
240
|
+
"employee",
|
|
241
|
+
"superadmin"
|
|
242
|
+
].includes(r.toLowerCase()));
|
|
243
|
+
const userType = payload[options.userTypeClaim] || payload.user_type || (isStaff ? "employee" : "user");
|
|
244
|
+
return {
|
|
245
|
+
userId: payload.sub || "anonymous",
|
|
246
|
+
email: payload.email,
|
|
247
|
+
name: payload.name || payload.nickname || payload.email?.split("@")[0] || "User",
|
|
248
|
+
avatar: payload.picture,
|
|
249
|
+
roles,
|
|
250
|
+
permissions,
|
|
251
|
+
userType,
|
|
252
|
+
metadata: payload
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
export { auth0Auth, createAuth0AuthorizeUrl, createAuth0LogoutUrl, createSessionToken, decodeJwt, exchangeAuth0Code, loginWithAuth0Password, mapAuth0PayloadToSession, refreshAuth0Token, verifyAuth0Jwt, verifySessionToken };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AuthAdapter } from "../types.mjs";
|
|
2
|
+
//#region src/adapters/cf-access.d.ts
|
|
3
|
+
interface CfAccessAdapterOptions {
|
|
4
|
+
aud?: string | undefined;
|
|
5
|
+
teamDomain?: string | undefined;
|
|
6
|
+
defaultRole?: string | undefined;
|
|
7
|
+
adminEmails?: string[] | undefined;
|
|
8
|
+
verifyJwt?: boolean | undefined;
|
|
9
|
+
loginUrl?: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
declare function cfAccessAuth(options?: CfAccessAdapterOptions): AuthAdapter;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { CfAccessAdapterOptions, cfAccessAuth };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
2
|
+
//#region src/adapters/cf-access.ts
|
|
3
|
+
function cfAccessAuth(options = {}) {
|
|
4
|
+
const { aud, teamDomain, defaultRole = "admin", adminEmails = [], verifyJwt = false, loginUrl } = options;
|
|
5
|
+
let jwks = null;
|
|
6
|
+
if (teamDomain && verifyJwt) {
|
|
7
|
+
const certsUrl = new URL(`https://${teamDomain.replace(/^https?:\/\//, "")}/cdn-cgi/access/certs`);
|
|
8
|
+
jwks = createRemoteJWKSet(certsUrl);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
name: "cf-access",
|
|
12
|
+
options,
|
|
13
|
+
async getSession(req) {
|
|
14
|
+
const emailHeader = req.headers.get("cf-access-authenticated-user-email");
|
|
15
|
+
const jwtAssertion = req.headers.get("cf-access-jwt-assertion");
|
|
16
|
+
if (!emailHeader && !jwtAssertion) return null;
|
|
17
|
+
let email = emailHeader?.toLowerCase().trim();
|
|
18
|
+
let name;
|
|
19
|
+
let customClaims = {};
|
|
20
|
+
if (jwtAssertion && jwks) try {
|
|
21
|
+
const verifyOpts = {};
|
|
22
|
+
if (aud) verifyOpts.audience = aud;
|
|
23
|
+
if (teamDomain) verifyOpts.issuer = `https://${teamDomain.replace(/^https?:\/\//, "")}`;
|
|
24
|
+
const { payload } = await jwtVerify(jwtAssertion, jwks, verifyOpts);
|
|
25
|
+
if (payload.email && typeof payload.email === "string") email = payload.email.toLowerCase().trim();
|
|
26
|
+
if (payload.name && typeof payload.name === "string") name = payload.name;
|
|
27
|
+
customClaims = payload;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const userEmail = email || "cf-access-user";
|
|
32
|
+
const isAdmin = adminEmails && adminEmails.length > 0 ? adminEmails.map((e) => e.toLowerCase()).includes(userEmail) : true;
|
|
33
|
+
return {
|
|
34
|
+
userId: userEmail,
|
|
35
|
+
email: userEmail,
|
|
36
|
+
name: name || userEmail.split("@")[0],
|
|
37
|
+
roles: isAdmin ? [defaultRole] : ["viewer"],
|
|
38
|
+
permissions: isAdmin ? ["*"] : [],
|
|
39
|
+
userType: "employee",
|
|
40
|
+
metadata: customClaims
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
handleUnauthorized(req) {
|
|
44
|
+
if (loginUrl) {
|
|
45
|
+
try {
|
|
46
|
+
if (req && req.url) {
|
|
47
|
+
const origin = new URL(req.url).origin;
|
|
48
|
+
const fullUrl = new URL(loginUrl, origin).toString();
|
|
49
|
+
return Response.redirect(fullUrl, 302);
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
try {
|
|
53
|
+
return Response.redirect(loginUrl, 302);
|
|
54
|
+
} catch {
|
|
55
|
+
return new Response(null, {
|
|
56
|
+
status: 302,
|
|
57
|
+
headers: { Location: loginUrl }
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return new Response("Unauthorized Cloudflare Access Identity", { status: 401 });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { cfAccessAuth };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AuthAdapter, UserSessionContext } from "../types.mjs";
|
|
2
|
+
//#region src/adapters/mock.d.ts
|
|
3
|
+
interface MockAuthOptions {
|
|
4
|
+
session?: Partial<UserSessionContext> | null;
|
|
5
|
+
unauthorizedResponse?: Response;
|
|
6
|
+
}
|
|
7
|
+
declare function mockAuth(options?: MockAuthOptions | Partial<UserSessionContext>): AuthAdapter;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { MockAuthOptions, mockAuth };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/adapters/mock.ts
|
|
2
|
+
function mockAuth(options = {}) {
|
|
3
|
+
const opts = "session" in options || "unauthorizedResponse" in options ? options : { session: options };
|
|
4
|
+
const defaultSession = {
|
|
5
|
+
userId: "mock-user-id",
|
|
6
|
+
email: "dev@rimelight.com",
|
|
7
|
+
name: "Mock Dev User",
|
|
8
|
+
avatar: "https://cdn.rimelight.com/Images/default_avatar.png",
|
|
9
|
+
roles: ["admin", "editor"],
|
|
10
|
+
permissions: ["*"],
|
|
11
|
+
userType: "employee",
|
|
12
|
+
metadata: { isMock: true }
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
name: "mock",
|
|
16
|
+
options: opts,
|
|
17
|
+
async getSession() {
|
|
18
|
+
if (opts.session === null) return null;
|
|
19
|
+
return {
|
|
20
|
+
...defaultSession,
|
|
21
|
+
...opts.session
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
handleUnauthorized() {
|
|
25
|
+
if (opts.unauthorizedResponse) return opts.unauthorizedResponse;
|
|
26
|
+
return new Response("Unauthorized Mock Session", { status: 401 });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { mockAuth };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AuthAdapter, UserSessionContext, UserType } from "./types.mjs";
|
|
2
|
+
import { Auth0AdapterOptions, Auth0AuthorizeUrlOptions, Auth0CodeExchangeOptions, Auth0PasswordLoginOptions, Auth0RefreshTokenOptions, Auth0TokenResponse, auth0Auth, createAuth0AuthorizeUrl, createAuth0LogoutUrl, createSessionToken, decodeJwt, exchangeAuth0Code, loginWithAuth0Password, mapAuth0PayloadToSession, refreshAuth0Token, verifyAuth0Jwt, verifySessionToken } from "./adapters/auth0.mjs";
|
|
3
|
+
import { CfAccessAdapterOptions, cfAccessAuth } from "./adapters/cf-access.mjs";
|
|
4
|
+
import { MockAuthOptions, mockAuth } from "./adapters/mock.mjs";
|
|
5
|
+
import { AccessControl, AccessRole, StatementMap, adminAc, createAccessControl, createPermissions, defaultStatements, evaluateAccess, hasPermission, hasRole, memberAc, ownerAc } from "./permissions/index.mjs";
|
|
6
|
+
import { RESTRICTED_SET, STANDARD_RESTRICTED_GROUPS, createRestrictedUsernameSet, normalizeUsername } from "./plugins/reserved-usernames/index.mjs";
|
|
7
|
+
import { CONSTRUCTION_GUEST_COOKIE, GuestEnv, isConstructionGuest, signInConstructionGuest } from "./plugins/construction-guest/index.mjs";
|
|
8
|
+
export { AccessControl, AccessRole, Auth0AdapterOptions, Auth0AuthorizeUrlOptions, Auth0CodeExchangeOptions, Auth0PasswordLoginOptions, Auth0RefreshTokenOptions, Auth0TokenResponse, AuthAdapter, CONSTRUCTION_GUEST_COOKIE, CfAccessAdapterOptions, GuestEnv, MockAuthOptions, RESTRICTED_SET, STANDARD_RESTRICTED_GROUPS, StatementMap, UserSessionContext, UserType, adminAc, auth0Auth, cfAccessAuth, createAccessControl, createAuth0AuthorizeUrl, createAuth0LogoutUrl, createPermissions, createRestrictedUsernameSet, createSessionToken, decodeJwt, defaultStatements, evaluateAccess, exchangeAuth0Code, hasPermission, hasRole, isConstructionGuest, loginWithAuth0Password, mapAuth0PayloadToSession, memberAc, mockAuth, normalizeUsername, ownerAc, refreshAuth0Token, signInConstructionGuest, verifyAuth0Jwt, verifySessionToken };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./types.mjs";
|
|
2
|
+
import { cfAccessAuth } from "./adapters/cf-access.mjs";
|
|
3
|
+
import { auth0Auth, createAuth0AuthorizeUrl, createAuth0LogoutUrl, createSessionToken, decodeJwt, exchangeAuth0Code, loginWithAuth0Password, mapAuth0PayloadToSession, refreshAuth0Token, verifyAuth0Jwt, verifySessionToken } from "./adapters/auth0.mjs";
|
|
4
|
+
import { mockAuth } from "./adapters/mock.mjs";
|
|
5
|
+
import { AccessControl, adminAc, createAccessControl, createPermissions, defaultStatements, evaluateAccess, hasPermission, hasRole, memberAc, ownerAc } from "./permissions/index.mjs";
|
|
6
|
+
import { RESTRICTED_SET, STANDARD_RESTRICTED_GROUPS, createRestrictedUsernameSet, normalizeUsername } from "./plugins/reserved-usernames/index.mjs";
|
|
7
|
+
import { CONSTRUCTION_GUEST_COOKIE, isConstructionGuest, signInConstructionGuest } from "./plugins/construction-guest/index.mjs";
|
|
8
|
+
export { AccessControl, CONSTRUCTION_GUEST_COOKIE, RESTRICTED_SET, STANDARD_RESTRICTED_GROUPS, adminAc, auth0Auth, cfAccessAuth, createAccessControl, createAuth0AuthorizeUrl, createAuth0LogoutUrl, createPermissions, createRestrictedUsernameSet, createSessionToken, decodeJwt, defaultStatements, evaluateAccess, exchangeAuth0Code, hasPermission, hasRole, isConstructionGuest, loginWithAuth0Password, mapAuth0PayloadToSession, memberAc, mockAuth, normalizeUsername, ownerAc, refreshAuth0Token, signInConstructionGuest, verifyAuth0Jwt, verifySessionToken };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { UserSessionContext } from "../types.mjs";
|
|
2
|
+
//#region src/permissions/index.d.ts
|
|
3
|
+
declare function hasRole(session: UserSessionContext | null | undefined, requiredRoles: string | string[]): boolean;
|
|
4
|
+
declare function hasPermission(session: UserSessionContext | null | undefined, requiredPermission: string): boolean;
|
|
5
|
+
declare function evaluateAccess(session: UserSessionContext | null | undefined, constraints: {
|
|
6
|
+
roles?: string[];
|
|
7
|
+
permission?: string;
|
|
8
|
+
}): boolean;
|
|
9
|
+
type StatementMap = Record<string, readonly string[]>;
|
|
10
|
+
interface AccessRole<TStatements extends StatementMap> {
|
|
11
|
+
statements: Partial<{ [K in keyof TStatements]: TStatements[K][number][]; }>;
|
|
12
|
+
can(statement: string, action: string): boolean;
|
|
13
|
+
}
|
|
14
|
+
declare class AccessControl<TStatements extends StatementMap> {
|
|
15
|
+
readonly statements: TStatements;
|
|
16
|
+
constructor(statements: TStatements);
|
|
17
|
+
newRole(permissions: Partial<{ [K in keyof TStatements]: TStatements[K][number][]; }>): AccessRole<TStatements>;
|
|
18
|
+
}
|
|
19
|
+
declare function createAccessControl<const TStatements extends StatementMap>(statements: TStatements): AccessControl<TStatements>;
|
|
20
|
+
declare const defaultStatements: {
|
|
21
|
+
user: readonly ["create", "list", "set-role", "ban", "impersonate", "delete", "setRole"];
|
|
22
|
+
};
|
|
23
|
+
declare const ownerAc: {
|
|
24
|
+
statements: {
|
|
25
|
+
user: string[];
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
declare const adminAc: {
|
|
29
|
+
statements: {
|
|
30
|
+
user: string[];
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
declare const memberAc: {
|
|
34
|
+
statements: {
|
|
35
|
+
user: string[];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
declare function createPermissions<const TStatements extends StatementMap>(statements: TStatements): {
|
|
39
|
+
ac: AccessControl<{
|
|
40
|
+
user: readonly ["create", "list", "set-role", "ban", "impersonate", "delete", "setRole"];
|
|
41
|
+
} & TStatements>;
|
|
42
|
+
statements: {
|
|
43
|
+
user: readonly ["create", "list", "set-role", "ban", "impersonate", "delete", "setRole"];
|
|
44
|
+
} & TStatements;
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { AccessControl, AccessRole, StatementMap, adminAc, createAccessControl, createPermissions, defaultStatements, evaluateAccess, hasPermission, hasRole, memberAc, ownerAc };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
//#region src/permissions/index.ts
|
|
2
|
+
function hasRole(session, requiredRoles) {
|
|
3
|
+
if (!session || !session.roles || session.roles.length === 0) return false;
|
|
4
|
+
const normalized = (Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles]).map((r) => r.toLowerCase());
|
|
5
|
+
if (normalized.includes("*")) return true;
|
|
6
|
+
return session.roles.some((r) => normalized.includes(r.toLowerCase()));
|
|
7
|
+
}
|
|
8
|
+
function hasPermission(session, requiredPermission) {
|
|
9
|
+
if (!session || !session.permissions || session.permissions.length === 0) return false;
|
|
10
|
+
if (session.permissions.includes("*") || session.permissions.includes("admin")) return true;
|
|
11
|
+
if (session.permissions.includes(requiredPermission)) return true;
|
|
12
|
+
const [resource] = requiredPermission.split(":");
|
|
13
|
+
if (resource && session.permissions.includes(`${resource}:*`)) return true;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
function evaluateAccess(session, constraints) {
|
|
17
|
+
if (constraints.roles && constraints.roles.length > 0) {
|
|
18
|
+
if (!hasRole(session, constraints.roles)) return false;
|
|
19
|
+
}
|
|
20
|
+
if (constraints.permission) {
|
|
21
|
+
if (!hasPermission(session, constraints.permission)) return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
var AccessControl = class {
|
|
26
|
+
statements;
|
|
27
|
+
constructor(statements) {
|
|
28
|
+
this.statements = statements;
|
|
29
|
+
}
|
|
30
|
+
newRole(permissions) {
|
|
31
|
+
const stmts = permissions;
|
|
32
|
+
return {
|
|
33
|
+
statements: stmts,
|
|
34
|
+
can(statement, action) {
|
|
35
|
+
const allowed = stmts[statement] ?? [];
|
|
36
|
+
return allowed.includes(action) || allowed.includes("*");
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function createAccessControl(statements) {
|
|
42
|
+
return new AccessControl(statements);
|
|
43
|
+
}
|
|
44
|
+
const defaultStatements = { user: [
|
|
45
|
+
"create",
|
|
46
|
+
"list",
|
|
47
|
+
"set-role",
|
|
48
|
+
"ban",
|
|
49
|
+
"impersonate",
|
|
50
|
+
"delete",
|
|
51
|
+
"setRole"
|
|
52
|
+
] };
|
|
53
|
+
const ownerAc = { statements: { user: [
|
|
54
|
+
"create",
|
|
55
|
+
"list",
|
|
56
|
+
"set-role",
|
|
57
|
+
"ban",
|
|
58
|
+
"impersonate",
|
|
59
|
+
"delete",
|
|
60
|
+
"setRole"
|
|
61
|
+
] } };
|
|
62
|
+
const adminAc = { statements: { user: [
|
|
63
|
+
"create",
|
|
64
|
+
"list",
|
|
65
|
+
"set-role",
|
|
66
|
+
"ban",
|
|
67
|
+
"impersonate",
|
|
68
|
+
"delete",
|
|
69
|
+
"setRole"
|
|
70
|
+
] } };
|
|
71
|
+
const memberAc = { statements: { user: ["create", "list"] } };
|
|
72
|
+
function createPermissions(statements) {
|
|
73
|
+
const mergedStatements = {
|
|
74
|
+
...defaultStatements,
|
|
75
|
+
...statements
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
ac: createAccessControl(mergedStatements),
|
|
79
|
+
statements: mergedStatements
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
export { AccessControl, adminAc, createAccessControl, createPermissions, defaultStatements, evaluateAccess, hasPermission, hasRole, memberAc, ownerAc };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/plugins/construction-guest/index.d.ts
|
|
2
|
+
declare const CONSTRUCTION_GUEST_COOKIE = "rimelight-construction-guest";
|
|
3
|
+
type GuestEnv = {
|
|
4
|
+
CONSTRUCTION_PASSPHRASE?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const isConstructionGuest: (c: any) => Promise<boolean>;
|
|
7
|
+
declare const signInConstructionGuest: (c: any, passphrase: string, rememberMe?: boolean) => Promise<boolean>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { CONSTRUCTION_GUEST_COOKIE, GuestEnv, isConstructionGuest, signInConstructionGuest };
|