@rimelight/auth 0.0.2 → 0.0.4
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 +12 -0
- package/dist/adapters/cf-access.mjs +66 -0
- package/dist/adapters/mock.d.mts +8 -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 +46 -0
- package/dist/permissions/index.mjs +83 -0
- package/dist/plugins/construction-guest/index.d.mts +8 -0
- package/dist/plugins/construction-guest/index.mjs +51 -0
- package/dist/plugins/reserved-usernames/index.d.mts +11 -0
- package/dist/plugins/reserved-usernames/index.mjs +192 -0
- package/dist/types.d.mts +57 -0
- package/dist/types.mjs +1 -0
- package/package.json +44 -19
- 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
|
+
export 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
|
+
export 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
|
+
export 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
|
+
export interface Auth0CodeExchangeOptions {
|
|
38
|
+
domain: string;
|
|
39
|
+
clientId: string;
|
|
40
|
+
clientSecret?: string | undefined;
|
|
41
|
+
code: string;
|
|
42
|
+
redirectUri: string;
|
|
43
|
+
codeVerifier?: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
export 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
|
+
export 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
|
+
export 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
|
+
export 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
|
+
export declare function verifySessionToken(token: string, secret: string): Promise<UserSessionContext | null>;
|
|
78
|
+
/**
|
|
79
|
+
* Generates an Auth0 OAuth2 / OIDC authorization URL
|
|
80
|
+
*/
|
|
81
|
+
export declare function createAuth0AuthorizeUrl(options: Auth0AuthorizeUrlOptions): string;
|
|
82
|
+
/**
|
|
83
|
+
* Exchanges an authorization code for Auth0 tokens
|
|
84
|
+
*/
|
|
85
|
+
export 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
|
+
export 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
|
+
export declare function refreshAuth0Token(options: Auth0RefreshTokenOptions): Promise<Auth0TokenResponse>;
|
|
96
|
+
/**
|
|
97
|
+
* Generates an Auth0 logout URL
|
|
98
|
+
*/
|
|
99
|
+
export declare function createAuth0LogoutUrl(options: {
|
|
100
|
+
domain: string;
|
|
101
|
+
clientId: string;
|
|
102
|
+
returnTo: string;
|
|
103
|
+
}): string;
|
|
104
|
+
/**
|
|
105
|
+
* Auth0 / OIDC Auth Adapter
|
|
106
|
+
*/
|
|
107
|
+
export declare function auth0Auth(options: Auth0AdapterOptions): AuthAdapter;
|
|
108
|
+
export 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 { decodeJwt };
|
|
@@ -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,12 @@
|
|
|
1
|
+
import { AuthAdapter } from "../types.mjs";
|
|
2
|
+
//#region src/adapters/cf-access.d.ts
|
|
3
|
+
export 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
|
+
export declare function cfAccessAuth(options?: CfAccessAdapterOptions): AuthAdapter;
|
|
12
|
+
//#endregion
|
|
@@ -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,8 @@
|
|
|
1
|
+
import { AuthAdapter, UserSessionContext } from "../types.mjs";
|
|
2
|
+
//#region src/adapters/mock.d.ts
|
|
3
|
+
export interface MockAuthOptions {
|
|
4
|
+
session?: Partial<UserSessionContext> | null;
|
|
5
|
+
unauthorizedResponse?: Response;
|
|
6
|
+
}
|
|
7
|
+
export declare function mockAuth(options?: MockAuthOptions | Partial<UserSessionContext>): AuthAdapter;
|
|
8
|
+
//#endregion
|
|
@@ -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 { auth0Auth, createAuth0AuthorizeUrl, createAuth0LogoutUrl, createSessionToken, decodeJwt, exchangeAuth0Code, loginWithAuth0Password, mapAuth0PayloadToSession, refreshAuth0Token, verifyAuth0Jwt, verifySessionToken } from "./adapters/auth0.mjs";
|
|
2
|
+
import { cfAccessAuth } from "./adapters/cf-access.mjs";
|
|
3
|
+
import { mockAuth } from "./adapters/mock.mjs";
|
|
4
|
+
import "./types.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,46 @@
|
|
|
1
|
+
import { UserSessionContext } from "../types.mjs";
|
|
2
|
+
//#region src/permissions/index.d.ts
|
|
3
|
+
export declare function hasRole(session: UserSessionContext | null | undefined, requiredRoles: string | string[]): boolean;
|
|
4
|
+
export declare function hasPermission(session: UserSessionContext | null | undefined, requiredPermission: string): boolean;
|
|
5
|
+
export declare function evaluateAccess(session: UserSessionContext | null | undefined, constraints: {
|
|
6
|
+
roles?: string[];
|
|
7
|
+
permission?: string;
|
|
8
|
+
}): boolean;
|
|
9
|
+
export type StatementMap = Record<string, readonly string[]>;
|
|
10
|
+
export interface AccessRole<TStatements extends StatementMap> {
|
|
11
|
+
statements: Partial<{ [K in keyof TStatements]: TStatements[K][number][]; }>;
|
|
12
|
+
can(statement: string, action: string): boolean;
|
|
13
|
+
}
|
|
14
|
+
export 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
|
+
export declare function createAccessControl<const TStatements extends StatementMap>(statements: TStatements): AccessControl<TStatements>;
|
|
20
|
+
export declare const defaultStatements: {
|
|
21
|
+
user: readonly ["create", "list", "set-role", "ban", "impersonate", "delete", "setRole"];
|
|
22
|
+
};
|
|
23
|
+
export declare const ownerAc: {
|
|
24
|
+
statements: {
|
|
25
|
+
user: string[];
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export declare const adminAc: {
|
|
29
|
+
statements: {
|
|
30
|
+
user: string[];
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export declare const memberAc: {
|
|
34
|
+
statements: {
|
|
35
|
+
user: string[];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export 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
|
|
@@ -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,8 @@
|
|
|
1
|
+
//#region src/plugins/construction-guest/index.d.ts
|
|
2
|
+
export declare const CONSTRUCTION_GUEST_COOKIE = "rimelight-construction-guest";
|
|
3
|
+
export type GuestEnv = {
|
|
4
|
+
CONSTRUCTION_PASSPHRASE?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const isConstructionGuest: (c: any) => Promise<boolean>;
|
|
7
|
+
export declare const signInConstructionGuest: (c: any, passphrase: string, rememberMe?: boolean) => Promise<boolean>;
|
|
8
|
+
//#endregion
|