@mastra/auth-auth0 1.0.1 → 1.1.1-alpha.0

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/index.js CHANGED
@@ -1,162 +1,133 @@
1
+ import { MastraAuthProvider } from '@mastra/core/server';
1
2
  import { createRemoteJWKSet, jwtVerify } from 'jose';
2
3
 
3
- // ../../packages/core/dist/chunk-X2WMFSPB.js
4
- var RegisteredLogger = {
5
- LLM: "LLM"};
6
- var LogLevel = {
7
- DEBUG: "debug",
8
- INFO: "info",
9
- WARN: "warn",
10
- ERROR: "error"};
11
- var MastraLogger = class {
12
- name;
13
- level;
14
- transports;
15
- constructor(options = {}) {
16
- this.name = options.name || "Mastra";
17
- this.level = options.level || LogLevel.ERROR;
18
- this.transports = new Map(Object.entries(options.transports || {}));
19
- }
20
- getTransports() {
21
- return this.transports;
22
- }
23
- trackException(_error) {
24
- }
25
- async listLogs(transportId, params) {
26
- if (!transportId || !this.transports.has(transportId)) {
27
- return { logs: [], total: 0, page: params?.page ?? 1, perPage: params?.perPage ?? 100, hasMore: false };
28
- }
29
- return this.transports.get(transportId).listLogs(params) ?? {
30
- logs: [],
31
- total: 0,
32
- page: params?.page ?? 1,
33
- perPage: params?.perPage ?? 100,
34
- hasMore: false
35
- };
36
- }
37
- async listLogsByRunId({
38
- transportId,
39
- runId,
40
- fromDate,
41
- toDate,
42
- logLevel,
43
- filters,
44
- page,
45
- perPage
46
- }) {
47
- if (!transportId || !this.transports.has(transportId) || !runId) {
48
- return { logs: [], total: 0, page: page ?? 1, perPage: perPage ?? 100, hasMore: false };
49
- }
50
- return this.transports.get(transportId).listLogsByRunId({ runId, fromDate, toDate, logLevel, filters, page, perPage }) ?? {
51
- logs: [],
52
- total: 0,
53
- page: page ?? 1,
54
- perPage: perPage ?? 100,
55
- hasMore: false
56
- };
57
- }
58
- };
59
- var ConsoleLogger = class extends MastraLogger {
60
- constructor(options = {}) {
61
- super(options);
62
- }
63
- debug(message, ...args) {
64
- if (this.level === LogLevel.DEBUG) {
65
- console.info(message, ...args);
66
- }
67
- }
68
- info(message, ...args) {
69
- if (this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) {
70
- console.info(message, ...args);
71
- }
4
+ // src/index.ts
5
+ var DEFAULT_COOKIE_NAME = "auth0_session";
6
+ var DEFAULT_COOKIE_MAX_AGE = 86400;
7
+ var DEFAULT_SCOPES = ["openid", "profile", "email"];
8
+ var SALT_LENGTH = 16;
9
+ var IV_LENGTH = 12;
10
+ async function deriveKey(password, salt, usage) {
11
+ const encoder = new TextEncoder();
12
+ const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, [
13
+ "deriveBits",
14
+ "deriveKey"
15
+ ]);
16
+ return crypto.subtle.deriveKey(
17
+ { name: "PBKDF2", salt, iterations: 1e5, hash: "SHA-256" },
18
+ keyMaterial,
19
+ { name: "AES-GCM", length: 256 },
20
+ false,
21
+ [usage]
22
+ );
23
+ }
24
+ async function encryptSession(data, password) {
25
+ const encoder = new TextEncoder();
26
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
27
+ const key = await deriveKey(password, salt, "encrypt");
28
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
29
+ const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoder.encode(JSON.stringify(data)));
30
+ const combined = new Uint8Array(salt.length + iv.length + new Uint8Array(encrypted).length);
31
+ combined.set(salt);
32
+ combined.set(iv, salt.length);
33
+ combined.set(new Uint8Array(encrypted), salt.length + iv.length);
34
+ return btoa(String.fromCharCode(...combined));
35
+ }
36
+ async function decryptSession(encrypted, password) {
37
+ const combined = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0));
38
+ if (combined.length < SALT_LENGTH + IV_LENGTH + 1) {
39
+ throw new Error("Invalid encrypted session data");
72
40
  }
73
- warn(message, ...args) {
74
- if (this.level === LogLevel.WARN || this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) {
75
- console.info(message, ...args);
76
- }
77
- }
78
- error(message, ...args) {
79
- if (this.level === LogLevel.ERROR || this.level === LogLevel.WARN || this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) {
80
- console.error(message, ...args);
81
- }
41
+ const salt = combined.slice(0, SALT_LENGTH);
42
+ const iv = combined.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
43
+ const data = combined.slice(SALT_LENGTH + IV_LENGTH);
44
+ const key = await deriveKey(password, salt, "decrypt");
45
+ const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data);
46
+ return JSON.parse(new TextDecoder().decode(decrypted));
47
+ }
48
+ var STATE_TOKEN_EXPIRY_MS = 10 * 60 * 1e3;
49
+ function createStateToken(originalState, redirectUri, secret) {
50
+ const payload = {
51
+ s: originalState,
52
+ r: redirectUri,
53
+ e: Date.now() + STATE_TOKEN_EXPIRY_MS
54
+ };
55
+ const payloadB64 = btoa(JSON.stringify(payload));
56
+ const signature = hmacSign(payloadB64, secret);
57
+ return `${payloadB64}.${signature}`;
58
+ }
59
+ function verifyStateToken(stateToken, secret) {
60
+ const parts = stateToken.split(".");
61
+ if (parts.length !== 2) {
62
+ throw new Error("Invalid state token format");
82
63
  }
83
- async listLogs(_transportId, _params) {
84
- return { logs: [], total: 0, page: _params?.page ?? 1, perPage: _params?.perPage ?? 100, hasMore: false };
64
+ const [payloadB64, signature] = parts;
65
+ const expectedSig = hmacSign(payloadB64, secret);
66
+ if (!timingSafeEqual(signature, expectedSig)) {
67
+ throw new Error("Invalid or tampered state token");
85
68
  }
86
- async listLogsByRunId(_args) {
87
- return { logs: [], total: 0, page: _args.page ?? 1, perPage: _args.perPage ?? 100, hasMore: false };
88
- }
89
- };
90
-
91
- // ../../packages/core/dist/chunk-WCAFTXGK.js
92
- var MastraBase = class {
93
- component = RegisteredLogger.LLM;
94
- logger;
95
- name;
96
- #rawConfig;
97
- constructor({
98
- component,
99
- name,
100
- rawConfig
101
- }) {
102
- this.component = component || RegisteredLogger.LLM;
103
- this.name = name;
104
- this.#rawConfig = rawConfig;
105
- this.logger = new ConsoleLogger({ name: `${this.component} - ${this.name}` });
69
+ let payload;
70
+ try {
71
+ payload = JSON.parse(atob(payloadB64));
72
+ } catch {
73
+ throw new Error("Invalid state token payload");
106
74
  }
107
- /**
108
- * Returns the raw storage configuration this primitive was created from,
109
- * or undefined if it was created from code.
110
- */
111
- toRawConfig() {
112
- return this.#rawConfig;
113
- }
114
- /**
115
- * Sets the raw storage configuration for this primitive.
116
- * @internal
117
- */
118
- __setRawConfig(rawConfig) {
119
- this.#rawConfig = rawConfig;
75
+ if (payload.e < Date.now()) {
76
+ throw new Error("State token has expired");
120
77
  }
121
- /**
122
- * Set the logger for the agent
123
- * @param logger
124
- */
125
- __setLogger(logger) {
126
- this.logger = logger;
127
- if (this.component !== RegisteredLogger.LLM) {
128
- this.logger.debug(`Logger updated [component=${this.component}] [name=${this.name}]`);
129
- }
78
+ return {
79
+ originalState: payload.s,
80
+ redirectUri: payload.r
81
+ };
82
+ }
83
+ function hmacSign(data, secret) {
84
+ const encoder = new TextEncoder();
85
+ const keyBytes = encoder.encode(secret);
86
+ const dataBytes = encoder.encode(data);
87
+ const combined = new Uint8Array(keyBytes.length + dataBytes.length + keyBytes.length);
88
+ combined.set(keyBytes);
89
+ combined.set(dataBytes, keyBytes.length);
90
+ combined.set(keyBytes, keyBytes.length + dataBytes.length);
91
+ let h1 = 2166136261;
92
+ let h2 = 16777619;
93
+ for (let i = 0; i < combined.length; i++) {
94
+ h1 ^= combined[i];
95
+ h1 = Math.imul(h1, 16777619);
96
+ h2 ^= combined[i];
97
+ h2 = Math.imul(h2, 2246822507);
130
98
  }
131
- };
132
-
133
- // ../../packages/core/dist/server/index.js
134
- var MastraAuthProvider = class extends MastraBase {
135
- protected;
136
- public;
137
- constructor(options) {
138
- super({ component: "AUTH", name: options?.name });
139
- if (options?.authorizeUser) {
140
- this.authorizeUser = options.authorizeUser.bind(this);
141
- }
142
- this.protected = options?.protected;
143
- this.public = options?.public;
99
+ const sigBytes = new Uint8Array(8);
100
+ const view = new DataView(sigBytes.buffer);
101
+ view.setUint32(0, h1 >>> 0);
102
+ view.setUint32(4, h2 >>> 0);
103
+ return btoa(String.fromCharCode(...sigBytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
104
+ }
105
+ function timingSafeEqual(a, b) {
106
+ if (a.length !== b.length) {
107
+ return false;
144
108
  }
145
- registerOptions(opts) {
146
- if (opts?.authorizeUser) {
147
- this.authorizeUser = opts.authorizeUser.bind(this);
148
- }
149
- if (opts?.protected) {
150
- this.protected = opts.protected;
151
- }
152
- if (opts?.public) {
153
- this.public = opts.public;
154
- }
109
+ let result = 0;
110
+ for (let i = 0; i < a.length; i++) {
111
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
155
112
  }
156
- };
113
+ return result === 0;
114
+ }
115
+ function escapeRegex(str) {
116
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
117
+ }
157
118
  var MastraAuthAuth0 = class extends MastraAuthProvider {
158
119
  domain;
159
120
  audience;
121
+ // SSO fields
122
+ clientId;
123
+ clientSecret;
124
+ _redirectUri;
125
+ scopes;
126
+ cookieName;
127
+ cookieMaxAge;
128
+ cookiePassword;
129
+ secureCookies;
130
+ ssoEnabled;
160
131
  constructor(options) {
161
132
  super({ name: options?.name ?? "auth0" });
162
133
  const domain = options?.domain ?? process.env.AUTH0_DOMAIN;
@@ -168,9 +139,43 @@ var MastraAuthAuth0 = class extends MastraAuthProvider {
168
139
  }
169
140
  this.domain = domain;
170
141
  this.audience = audience;
142
+ const clientId = options?.clientId ?? process.env.AUTH0_CLIENT_ID;
143
+ const clientSecret = options?.clientSecret ?? process.env.AUTH0_CLIENT_SECRET;
144
+ const redirectUri = options?.redirectUri ?? process.env.AUTH0_REDIRECT_URI;
145
+ const cookiePassword = options?.session?.cookiePassword ?? process.env.AUTH0_COOKIE_PASSWORD ?? crypto.randomUUID() + crypto.randomUUID();
146
+ this.clientId = clientId ?? null;
147
+ this.clientSecret = clientSecret ?? null;
148
+ this._redirectUri = redirectUri ?? null;
149
+ this.scopes = options?.scopes ?? DEFAULT_SCOPES;
150
+ this.cookieName = options?.session?.cookieName ?? DEFAULT_COOKIE_NAME;
151
+ this.cookieMaxAge = options?.session?.cookieMaxAge ?? DEFAULT_COOKIE_MAX_AGE;
152
+ this.cookiePassword = cookiePassword;
153
+ this.secureCookies = options?.session?.secureCookies ?? process.env.NODE_ENV === "production";
154
+ this.ssoEnabled = !!(clientId && clientSecret);
155
+ if (this.ssoEnabled) {
156
+ if (cookiePassword.length < 32) {
157
+ throw new Error(
158
+ "Cookie password must be at least 32 characters for SSO. Set AUTH0_COOKIE_PASSWORD environment variable."
159
+ );
160
+ }
161
+ if (!options?.session?.cookiePassword && !process.env.AUTH0_COOKIE_PASSWORD) {
162
+ console.warn(
163
+ "[MastraAuthAuth0] No cookie password set \u2014 using auto-generated value. Sessions will not survive restarts. Set AUTH0_COOKIE_PASSWORD for production use."
164
+ );
165
+ }
166
+ this._attachSSOProvider();
167
+ this._attachSessionProvider();
168
+ }
171
169
  this.registerOptions(options);
172
170
  }
173
- async authenticateToken(token) {
171
+ // ============================================================================
172
+ // MastraAuthProvider Implementation
173
+ // ============================================================================
174
+ async authenticateToken(token, request) {
175
+ if (this.ssoEnabled && request) {
176
+ const sessionUser = await this.getUserFromSessionCookie(request);
177
+ if (sessionUser) return sessionUser;
178
+ }
174
179
  if (!token || typeof token !== "string") {
175
180
  return null;
176
181
  }
@@ -187,12 +192,249 @@ var MastraAuthAuth0 = class extends MastraAuthProvider {
187
192
  }
188
193
  }
189
194
  async authorizeUser(user) {
190
- if (!user || !user.sub) return false;
195
+ if (!user || !(user.sub || user.id)) return false;
191
196
  if (user.exp && user.exp * 1e3 < Date.now()) {
192
197
  return false;
193
198
  }
194
199
  return true;
195
200
  }
201
+ // ============================================================================
202
+ // IUserProvider Implementation
203
+ // ============================================================================
204
+ /**
205
+ * Extract the bearer token from the request's Authorization header.
206
+ */
207
+ extractToken(request) {
208
+ const authHeader = request.headers.get("Authorization");
209
+ if (authHeader) {
210
+ const token = authHeader.replace(/^Bearer\s+/i, "").trim();
211
+ if (token) return token;
212
+ }
213
+ return null;
214
+ }
215
+ async getCurrentUser(request) {
216
+ if (this.ssoEnabled) {
217
+ const sessionUser = await this.getUserFromSessionCookie(request);
218
+ if (sessionUser) return sessionUser;
219
+ }
220
+ const token = this.extractToken(request);
221
+ if (!token) return null;
222
+ try {
223
+ const payload = await this.authenticateToken(token);
224
+ if (!payload?.sub) return null;
225
+ return {
226
+ id: payload.sub,
227
+ email: payload.email ?? void 0,
228
+ name: payload.name ?? void 0,
229
+ avatarUrl: payload.picture ?? void 0
230
+ };
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+ async getUser(userId) {
236
+ return {
237
+ id: userId
238
+ };
239
+ }
240
+ getUserProfileUrl(user) {
241
+ return `/user/${user.id}`;
242
+ }
243
+ // ============================================================================
244
+ // Helper Methods
245
+ // ============================================================================
246
+ /**
247
+ * Check if SSO is enabled (OAuth credentials are configured).
248
+ */
249
+ isSSOEnabled() {
250
+ return this.ssoEnabled;
251
+ }
252
+ /**
253
+ * Build consistent cookie attribute string for set/clear operations.
254
+ */
255
+ cookieFlags(maxAge) {
256
+ const flags = `Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}`;
257
+ return this.secureCookies ? `${flags}; Secure` : flags;
258
+ }
259
+ /**
260
+ * Extract user from the encrypted SSO session cookie.
261
+ */
262
+ async getUserFromSessionCookie(request) {
263
+ const cookie = "header" in request && typeof request.header === "function" ? request.header("cookie") : request.headers?.get("cookie");
264
+ if (!cookie) return null;
265
+ const match = cookie.match(new RegExp(`(?:^|;\\s*)${escapeRegex(this.cookieName)}=([^;]+)`));
266
+ if (!match?.[1]) return null;
267
+ try {
268
+ const sessionData = await decryptSession(decodeURIComponent(match[1]), this.cookiePassword);
269
+ if (sessionData.expiresAt < Date.now()) {
270
+ return null;
271
+ }
272
+ return sessionData.user;
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+ // ============================================================================
278
+ // Dynamic ISSOProvider attachment (only when OAuth is configured)
279
+ // ============================================================================
280
+ /**
281
+ * Dynamically attach ISSOProvider methods to this instance.
282
+ * This ensures duck-typing detection only finds these methods when SSO is configured.
283
+ */
284
+ _attachSSOProvider() {
285
+ const self = this;
286
+ this.getLoginUrl = function(redirectUri, state) {
287
+ const actualRedirectUri = redirectUri ?? self._redirectUri;
288
+ if (!actualRedirectUri) {
289
+ throw new Error("Redirect URI is required for SSO. Set AUTH0_REDIRECT_URI or pass redirectUri option.");
290
+ }
291
+ const signedState = createStateToken(state, actualRedirectUri, self.cookiePassword);
292
+ const params = new URLSearchParams({
293
+ client_id: self.clientId,
294
+ response_type: "code",
295
+ scope: self.scopes.join(" "),
296
+ redirect_uri: actualRedirectUri,
297
+ state: signedState
298
+ });
299
+ return `https://${self.domain}/authorize?${params.toString()}`;
300
+ };
301
+ this.handleCallback = async function(code, signedState) {
302
+ const { redirectUri } = verifyStateToken(signedState, self.cookiePassword);
303
+ const tokenResponse = await fetch(`https://${self.domain}/oauth/token`, {
304
+ method: "POST",
305
+ headers: { "Content-Type": "application/json" },
306
+ body: JSON.stringify({
307
+ grant_type: "authorization_code",
308
+ client_id: self.clientId,
309
+ client_secret: self.clientSecret,
310
+ code,
311
+ redirect_uri: redirectUri
312
+ }),
313
+ signal: AbortSignal.timeout(1e4)
314
+ // 10 second timeout
315
+ });
316
+ if (!tokenResponse.ok) {
317
+ const error = await tokenResponse.text();
318
+ throw new Error(`Token exchange failed: ${error}`);
319
+ }
320
+ const tokens = await tokenResponse.json();
321
+ let user;
322
+ if (tokens.id_token) {
323
+ try {
324
+ const JWKS = createRemoteJWKSet(new URL(`https://${self.domain}/.well-known/jwks.json`));
325
+ const { payload } = await jwtVerify(tokens.id_token, JWKS, {
326
+ issuer: `https://${self.domain}/`,
327
+ audience: self.clientId
328
+ // Validate token was issued for this client
329
+ });
330
+ user = {
331
+ id: payload.sub,
332
+ email: payload.email ?? void 0,
333
+ name: payload.name ?? void 0,
334
+ avatarUrl: payload.picture ?? void 0
335
+ };
336
+ } catch {
337
+ user = await self._fetchUserInfo(tokens.access_token);
338
+ }
339
+ } else {
340
+ user = await self._fetchUserInfo(tokens.access_token);
341
+ }
342
+ const sessionData = {
343
+ user,
344
+ expiresAt: Date.now() + self.cookieMaxAge * 1e3
345
+ };
346
+ const encryptedSession = await encryptSession(sessionData, self.cookiePassword);
347
+ const cookieValue = `${self.cookieName}=${encodeURIComponent(encryptedSession)}; ${self.cookieFlags(self.cookieMaxAge)}`;
348
+ return {
349
+ user,
350
+ tokens: {
351
+ accessToken: tokens.access_token,
352
+ refreshToken: tokens.refresh_token,
353
+ idToken: tokens.id_token,
354
+ expiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
355
+ },
356
+ cookies: [cookieValue]
357
+ };
358
+ };
359
+ this.getLoginButtonConfig = function() {
360
+ return {
361
+ provider: "auth0",
362
+ text: "Sign in with Auth0",
363
+ description: "Sign in using your Auth0 account"
364
+ };
365
+ };
366
+ this.getLoginCookies = function(_state) {
367
+ return [];
368
+ };
369
+ this.getLogoutUrl = async function(redirectUri, _request) {
370
+ const params = new URLSearchParams({
371
+ client_id: self.clientId,
372
+ returnTo: redirectUri
373
+ });
374
+ return `https://${self.domain}/v2/logout?${params.toString()}`;
375
+ };
376
+ }
377
+ /**
378
+ * Fetch user info from Auth0's /userinfo endpoint.
379
+ */
380
+ async _fetchUserInfo(accessToken) {
381
+ const userInfoResponse = await fetch(`https://${this.domain}/userinfo`, {
382
+ headers: { Authorization: `Bearer ${accessToken}` },
383
+ signal: AbortSignal.timeout(1e4)
384
+ // 10 second timeout
385
+ });
386
+ if (!userInfoResponse.ok) {
387
+ throw new Error("Failed to fetch user info from Auth0");
388
+ }
389
+ const userInfo = await userInfoResponse.json();
390
+ return {
391
+ id: userInfo.sub,
392
+ email: userInfo.email,
393
+ name: userInfo.name,
394
+ avatarUrl: userInfo.picture
395
+ };
396
+ }
397
+ // ============================================================================
398
+ // Dynamic ISessionProvider attachment (only when OAuth is configured)
399
+ // ============================================================================
400
+ /**
401
+ * Dynamically attach ISessionProvider methods to this instance.
402
+ */
403
+ _attachSessionProvider() {
404
+ const self = this;
405
+ this.createSession = async function(userId, metadata) {
406
+ const now = /* @__PURE__ */ new Date();
407
+ return {
408
+ id: crypto.randomUUID(),
409
+ userId,
410
+ createdAt: now,
411
+ expiresAt: new Date(now.getTime() + self.cookieMaxAge * 1e3),
412
+ metadata
413
+ };
414
+ };
415
+ this.validateSession = async function(_sessionId) {
416
+ return null;
417
+ };
418
+ this.destroySession = async function(_sessionId) {
419
+ };
420
+ this.refreshSession = async function(_sessionId) {
421
+ return null;
422
+ };
423
+ this.getSessionIdFromRequest = function(request) {
424
+ const cookie = request.headers.get("Cookie");
425
+ if (!cookie) return null;
426
+ const match = cookie.match(new RegExp(`(?:^|;\\s*)${escapeRegex(self.cookieName)}=([^;]+)`));
427
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
428
+ };
429
+ this.getSessionHeaders = function(_session) {
430
+ return {};
431
+ };
432
+ this.getClearSessionHeaders = function() {
433
+ return {
434
+ "Set-Cookie": `${self.cookieName}=; ${self.cookieFlags(0)}`
435
+ };
436
+ };
437
+ }
196
438
  };
197
439
 
198
440
  export { MastraAuthAuth0 };