@leanmcp/auth 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,11 +5,12 @@ Authentication module for LeanMCP providing token-based authentication decorator
5
5
  ## Features
6
6
 
7
7
  - **@Authenticated decorator** - Protect MCP tools, prompts, and resources with token authentication
8
+ - **Automatic authUser injection** - Access decoded user info via global `authUser` variable in protected methods
8
9
  - **Multi-provider support** - AWS Cognito, Clerk, Auth0
9
10
  - **Method or class-level protection** - Apply to individual methods or entire services
10
11
  - **Automatic token validation** - Validates tokens before method execution
11
12
  - **Custom error handling** - Detailed error codes for different auth failures
12
- - **Type-safe** - Full TypeScript support with type inference
13
+ - **Type-safe** - Full TypeScript support with type inference and global type declarations
13
14
  - **OAuth & Session modes** - Support for both session-based and OAuth refresh token flows
14
15
 
15
16
  ## Installation
@@ -59,13 +60,20 @@ import { Tool } from "@leanmcp/core";
59
60
  import { Authenticated } from "@leanmcp/auth";
60
61
 
61
62
  export class SentimentService {
62
- // This method requires authentication
63
+ // This method requires authentication with automatic user info
63
64
  @Tool({ description: 'Analyze sentiment (requires auth)' })
64
- @Authenticated(authProvider)
65
+ @Authenticated(authProvider) // getUser: true by default
65
66
  async analyzeSentiment(input: { text: string }) {
66
67
  // Token is automatically validated from _meta.authorization.token
67
- // Only business arguments are passed to the method
68
- return { sentiment: 'positive', score: 0.8 };
68
+ // authUser is automatically available with decoded JWT payload
69
+ console.log('User ID:', authUser.sub);
70
+ console.log('Email:', authUser.email);
71
+
72
+ return {
73
+ sentiment: 'positive',
74
+ score: 0.8,
75
+ analyzedBy: authUser.sub
76
+ };
69
77
  }
70
78
 
71
79
  // This method is public
@@ -86,16 +94,134 @@ import { Authenticated } from "@leanmcp/auth";
86
94
  export class SecureService {
87
95
  @Tool({ description: 'Protected tool 1' })
88
96
  async tool1(input: { data: string }) {
89
- // All methods require authentication via _meta
97
+ // authUser is automatically available in all methods
98
+ console.log('Authenticated user:', authUser.email);
99
+ return { data: input.data, userId: authUser.sub };
90
100
  }
91
101
 
92
102
  @Tool({ description: 'Protected tool 2' })
93
103
  async tool2(input: { data: string }) {
94
- // All methods require authentication via _meta
104
+ // authUser is available here too
105
+ return { data: input.data, userId: authUser.sub };
95
106
  }
96
107
  }
97
108
  ```
98
109
 
110
+ ## authUser Variable
111
+
112
+ ### Automatic User Information Injection
113
+
114
+ When you use the `@Authenticated` decorator, a global `authUser` variable is automatically injected into your protected methods containing the decoded JWT token payload.
115
+
116
+ ```typescript
117
+ @Tool({ description: 'Create post' })
118
+ @Authenticated(authProvider)
119
+ async createPost(input: { title: string, content: string }) {
120
+ // authUser is automatically available - no need to pass it as a parameter
121
+ console.log('User ID:', authUser.sub);
122
+ console.log('Email:', authUser.email);
123
+
124
+ return {
125
+ id: generateId(),
126
+ title: input.title,
127
+ content: input.content,
128
+ authorId: authUser.sub,
129
+ authorEmail: authUser.email
130
+ };
131
+ }
132
+ ```
133
+
134
+ ### Controlling User Data Fetching
135
+
136
+ You can control whether user information is fetched using the `getUser` option:
137
+
138
+ ```typescript
139
+ // Fetch user info (default behavior)
140
+ @Authenticated(authProvider, { getUser: true })
141
+ async methodWithUserInfo(input: any) {
142
+ // authUser is available
143
+ console.log(authUser);
144
+ }
145
+
146
+ // Only verify token, don't fetch user info
147
+ @Authenticated(authProvider, { getUser: false })
148
+ async methodWithoutUserInfo(input: any) {
149
+ // authUser is undefined
150
+ // Faster execution, use when you only need token validation
151
+ }
152
+ ```
153
+
154
+ ### Provider-Specific User Data
155
+
156
+ The structure of `authUser` depends on your authentication provider:
157
+
158
+ **AWS Cognito:**
159
+ ```typescript
160
+ {
161
+ sub: 'user-uuid',
162
+ email: 'user@example.com',
163
+ email_verified: true,
164
+ 'cognito:username': 'username',
165
+ 'cognito:groups': ['admin', 'users'],
166
+ // ... other Cognito claims
167
+ }
168
+ ```
169
+
170
+ **Clerk:**
171
+ ```typescript
172
+ {
173
+ sub: 'user_2abc123xyz',
174
+ userId: 'user_2abc123xyz',
175
+ email: 'user@example.com',
176
+ firstName: 'John',
177
+ lastName: 'Doe',
178
+ imageUrl: 'https://img.clerk.com/...',
179
+ // ... other Clerk claims
180
+ }
181
+ ```
182
+
183
+ **Auth0:**
184
+ ```typescript
185
+ {
186
+ sub: 'auth0|507f1f77bcf86cd799439011',
187
+ email: 'user@example.com',
188
+ email_verified: true,
189
+ name: 'John Doe',
190
+ picture: 'https://s.gravatar.com/avatar/...',
191
+ // ... other Auth0 claims
192
+ }
193
+ ```
194
+
195
+ ### TypeScript Support
196
+
197
+ The `authUser` variable is globally declared and available without TypeScript errors:
198
+
199
+ ```typescript
200
+ // No need to import or declare authUser
201
+ @Authenticated(authProvider)
202
+ async myMethod(input: any) {
203
+ // TypeScript knows about authUser
204
+ const userId: string = authUser.sub;
205
+ const email: string = authUser.email;
206
+ }
207
+ ```
208
+
209
+ For better type safety, you can create a typed interface:
210
+
211
+ ```typescript
212
+ interface MyAuthUser {
213
+ sub: string;
214
+ email: string;
215
+ name?: string;
216
+ }
217
+
218
+ @Authenticated(authProvider)
219
+ async myMethod(input: any) {
220
+ const user = authUser as MyAuthUser;
221
+ console.log(user.email); // Fully typed
222
+ }
223
+ ```
224
+
99
225
  ## Usage
100
226
 
101
227
  ### Client Side - Calling Protected Methods
@@ -345,13 +471,24 @@ class AuthProvider {
345
471
  ### @Authenticated Decorator
346
472
 
347
473
  ```typescript
348
- function Authenticated(authProvider: AuthProvider): ClassDecorator | MethodDecorator;
474
+ function Authenticated(
475
+ authProvider: AuthProvider,
476
+ options?: AuthenticatedOptions
477
+ ): ClassDecorator | MethodDecorator;
478
+
479
+ interface AuthenticatedOptions {
480
+ getUser?: boolean; // Default: true
481
+ }
349
482
  ```
350
483
 
351
484
  Can be applied to:
352
485
  - **Classes** - Protects all methods in the class
353
486
  - **Methods** - Protects individual methods
354
487
 
488
+ **Options:**
489
+ - `getUser: true` (default) - Fetches user info and injects `authUser` variable
490
+ - `getUser: false` - Only validates token, skips user info fetch (faster)
491
+
355
492
  ### AuthenticationError
356
493
 
357
494
  ```typescript
@@ -415,9 +552,17 @@ await authProvider.init();
415
552
  // Create service with protected methods
416
553
  @Authenticated(authProvider)
417
554
  class MyService {
418
- @Tool()
555
+ @Tool({ description: 'Process protected data' })
419
556
  async protectedTool(input: { data: string }) {
420
- return { result: "Protected data" };
557
+ // authUser is automatically available
558
+ console.log('User:', authUser.sub);
559
+ console.log('Email:', authUser.email);
560
+
561
+ return {
562
+ result: "Protected data",
563
+ processedBy: authUser.sub,
564
+ userEmail: authUser.email
565
+ };
421
566
  }
422
567
  }
423
568
 
@@ -457,9 +602,27 @@ await authProviderOAuth.init();
457
602
  @Authenticated(authProvider)
458
603
  class UserService {
459
604
  @Tool({ description: 'Get user profile' })
460
- async getProfile(input: { userId: string }) {
461
- // Token is automatically validated
462
- return { userId: input.userId, name: "John Doe" };
605
+ async getProfile() {
606
+ // authUser is automatically available with Clerk user data
607
+ return {
608
+ userId: authUser.userId || authUser.sub,
609
+ email: authUser.email,
610
+ firstName: authUser.firstName,
611
+ lastName: authUser.lastName,
612
+ imageUrl: authUser.imageUrl
613
+ };
614
+ }
615
+
616
+ @Tool({ description: 'Create user post' })
617
+ async createPost(input: { title: string, content: string }) {
618
+ // Access authUser in any protected method
619
+ return {
620
+ id: generateId(),
621
+ title: input.title,
622
+ content: input.content,
623
+ authorId: authUser.userId,
624
+ authorEmail: authUser.email
625
+ };
463
626
  }
464
627
  }
465
628
 
@@ -492,13 +655,27 @@ await authProvider.init();
492
655
  class SecureAPIService {
493
656
  @Tool({ description: 'Get sensitive data' })
494
657
  async getSensitiveData(input: { dataId: string }) {
495
- // Token is automatically validated
496
- return { dataId: input.dataId, data: "Sensitive information" };
658
+ // authUser is automatically available with Auth0 user data
659
+ console.log('User:', authUser.sub);
660
+ console.log('Email:', authUser.email);
661
+
662
+ return {
663
+ dataId: input.dataId,
664
+ data: "Sensitive information",
665
+ accessedBy: authUser.sub,
666
+ userName: authUser.name
667
+ };
497
668
  }
498
669
 
499
670
  @Tool({ description: 'Update user settings' })
500
671
  async updateSettings(input: { settings: Record<string, any> }) {
501
- return { success: true, settings: input.settings };
672
+ // Access authUser in any protected method
673
+ return {
674
+ success: true,
675
+ settings: input.settings,
676
+ userId: authUser.sub,
677
+ updatedBy: authUser.email
678
+ };
502
679
  }
503
680
  }
504
681
 
@@ -533,17 +710,27 @@ await auth0Auth.init();
533
710
  // Use different providers for different services
534
711
  @Authenticated(clerkAuth)
535
712
  class UserService {
536
- @Tool()
537
- async getUserData(input: { userId: string }) {
538
- return { userId: input.userId };
713
+ @Tool({ description: 'Get user data' })
714
+ async getUserData() {
715
+ // authUser contains Clerk user data
716
+ return {
717
+ userId: authUser.userId,
718
+ email: authUser.email,
719
+ firstName: authUser.firstName
720
+ };
539
721
  }
540
722
  }
541
723
 
542
724
  @Authenticated(auth0Auth)
543
725
  class AdminService {
544
- @Tool()
545
- async getAdminData(input: { adminId: string }) {
546
- return { adminId: input.adminId };
726
+ @Tool({ description: 'Get admin data' })
727
+ async getAdminData() {
728
+ // authUser contains Auth0 user data
729
+ return {
730
+ adminId: authUser.sub,
731
+ email: authUser.email,
732
+ name: authUser.name
733
+ };
547
734
  }
548
735
  }
549
736
  ```
@@ -554,14 +741,19 @@ class AdminService {
554
741
  2. **Decorator intercepts** the method call before execution
555
742
  3. **Token is extracted** from `_meta.authorization.token`
556
743
  4. **Token is validated** using the configured auth provider
557
- 5. **Method executes** with clean business arguments (no token)
558
- 6. **Response returns** to client
744
+ 5. **User info is fetched** (if `getUser: true`) and decoded from JWT
745
+ 6. **authUser is injected** as a global variable in method scope
746
+ 7. **Method executes** with clean business arguments and access to `authUser`
747
+ 8. **authUser is cleaned up** after method execution
748
+ 9. **Response returns** to client
559
749
 
560
750
  **Key Benefits:**
561
751
  - **Clean separation** - Authentication metadata separate from business data
562
752
  - **MCP compliant** - Follows standard `_meta` pattern
563
753
  - **Type-safe** - Input classes don't need token fields
754
+ - **Automatic user injection** - Access user data via `authUser` without manual extraction
564
755
  - **Reusable** - Same input classes work for authenticated and public methods
756
+ - **Secure** - `authUser` is scoped to method execution and cleaned up after
565
757
 
566
758
  ## Best Practices
567
759
 
@@ -575,6 +767,9 @@ class AdminService {
575
767
  8. **Choose the right mode** - Use Session mode for simpler setups, OAuth mode for refresh tokens
576
768
  9. **Test token expiration** - Ensure your app handles expired tokens gracefully
577
769
  10. **Monitor JWKS cache** - Providers cache JWKS keys for performance
770
+ 11. **Use authUser for user context** - Access user data via `authUser` instead of parsing tokens manually
771
+ 12. **Type authUser when needed** - Cast `authUser` to a typed interface for better type safety
772
+ 13. **Use getUser: false for performance** - Skip user fetch when you only need token validation
578
773
 
579
774
  ## Quick Reference
580
775
 
@@ -634,15 +829,28 @@ const type = authProvider.getProviderType(); // 'cognito' | 'clerk' | 'auth0'
634
829
  ### Decorator Usage
635
830
 
636
831
  ```typescript
637
- // Protect single method
832
+ // Protect single method with authUser (default)
638
833
  @Authenticated(authProvider)
639
- async myMethod(input: { data: string }) { }
834
+ async myMethod(input: { data: string }) {
835
+ console.log(authUser.sub); // User ID available
836
+ }
837
+
838
+ // Protect method without fetching user (faster)
839
+ @Authenticated(authProvider, { getUser: false })
840
+ async fastMethod(input: { data: string }) {
841
+ // Only token validation, no authUser
842
+ }
640
843
 
641
844
  // Protect entire class
642
845
  @Authenticated(authProvider)
643
846
  class MyService {
644
- @Tool() async method1() { }
645
- @Tool() async method2() { }
847
+ @Tool() async method1() {
848
+ // authUser available in all methods
849
+ return { userId: authUser.sub };
850
+ }
851
+ @Tool() async method2() {
852
+ return { email: authUser.email };
853
+ }
646
854
  }
647
855
 
648
856
  // Check if authentication required
@@ -652,6 +860,39 @@ const required = isAuthenticationRequired(target, 'methodName');
652
860
  const provider = getAuthProvider(target, 'methodName');
653
861
  ```
654
862
 
863
+ ### authUser Quick Reference
864
+
865
+ ```typescript
866
+ // Access user data in protected methods
867
+ @Authenticated(authProvider)
868
+ async createPost(input: { title: string }) {
869
+ // authUser is automatically available
870
+ const userId = authUser.sub; // User ID (all providers)
871
+ const email = authUser.email; // Email (all providers)
872
+
873
+ // Provider-specific fields
874
+ const clerkId = authUser.userId; // Clerk only
875
+ const firstName = authUser.firstName; // Clerk only
876
+ const cognitoGroups = authUser['cognito:groups']; // Cognito only
877
+ const auth0Name = authUser.name; // Auth0 only
878
+
879
+ return { authorId: userId, authorEmail: email };
880
+ }
881
+
882
+ // Type authUser for better type safety
883
+ interface MyUser {
884
+ sub: string;
885
+ email: string;
886
+ name?: string;
887
+ }
888
+
889
+ @Authenticated(authProvider)
890
+ async typedMethod(input: any) {
891
+ const user = authUser as MyUser;
892
+ console.log(user.email); // Fully typed
893
+ }
894
+ ```
895
+
655
896
  ## License
656
897
 
657
898
  MIT
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  AuthProviderBase,
3
3
  __name
4
- } from "./chunk-KKO5VLFX.mjs";
4
+ } from "./chunk-EVD2TRPR.mjs";
5
5
 
6
6
  // src/providers/auth0.ts
7
7
  import axios from "axios";
@@ -6,6 +6,21 @@ import "reflect-metadata";
6
6
 
7
7
  // src/decorators.ts
8
8
  import "reflect-metadata";
9
+ import { AsyncLocalStorage } from "async_hooks";
10
+ var authUserStorage = new AsyncLocalStorage();
11
+ function getAuthUser() {
12
+ return authUserStorage.getStore();
13
+ }
14
+ __name(getAuthUser, "getAuthUser");
15
+ if (typeof globalThis !== "undefined" && !Object.getOwnPropertyDescriptor(globalThis, "authUser")) {
16
+ Object.defineProperty(globalThis, "authUser", {
17
+ get() {
18
+ return authUserStorage.getStore();
19
+ },
20
+ configurable: true,
21
+ enumerable: false
22
+ });
23
+ }
9
24
  var AuthenticationError = class extends Error {
10
25
  static {
11
26
  __name(this, "AuthenticationError");
@@ -16,11 +31,16 @@ var AuthenticationError = class extends Error {
16
31
  this.name = "AuthenticationError";
17
32
  }
18
33
  };
19
- function Authenticated(authProvider) {
34
+ function Authenticated(authProvider, options) {
35
+ const authOptions = {
36
+ getUser: true,
37
+ ...options
38
+ };
20
39
  return function(target, propertyKey, descriptor) {
21
40
  if (!propertyKey && !descriptor) {
22
41
  Reflect.defineMetadata("auth:provider", authProvider, target);
23
42
  Reflect.defineMetadata("auth:required", true, target);
43
+ Reflect.defineMetadata("auth:options", authOptions, target);
24
44
  const prototype = target.prototype;
25
45
  const methodNames = Object.getOwnPropertyNames(prototype).filter((name) => name !== "constructor" && typeof prototype[name] === "function");
26
46
  for (const methodName of methodNames) {
@@ -29,7 +49,8 @@ function Authenticated(authProvider) {
29
49
  const originalMethod = originalDescriptor.value;
30
50
  Reflect.defineMetadata("auth:provider", authProvider, originalMethod);
31
51
  Reflect.defineMetadata("auth:required", true, originalMethod);
32
- prototype[methodName] = createAuthenticatedMethod(originalMethod, authProvider);
52
+ Reflect.defineMetadata("auth:options", authOptions, originalMethod);
53
+ prototype[methodName] = createAuthenticatedMethod(originalMethod, authProvider, authOptions);
33
54
  copyMetadata(originalMethod, prototype[methodName]);
34
55
  }
35
56
  }
@@ -39,7 +60,8 @@ function Authenticated(authProvider) {
39
60
  const originalMethod = descriptor.value;
40
61
  Reflect.defineMetadata("auth:provider", authProvider, originalMethod);
41
62
  Reflect.defineMetadata("auth:required", true, originalMethod);
42
- descriptor.value = createAuthenticatedMethod(originalMethod, authProvider);
63
+ Reflect.defineMetadata("auth:options", authOptions, originalMethod);
64
+ descriptor.value = createAuthenticatedMethod(originalMethod, authProvider, authOptions);
43
65
  copyMetadata(originalMethod, descriptor.value);
44
66
  return descriptor;
45
67
  }
@@ -47,7 +69,7 @@ function Authenticated(authProvider) {
47
69
  };
48
70
  }
49
71
  __name(Authenticated, "Authenticated");
50
- function createAuthenticatedMethod(originalMethod, authProvider) {
72
+ function createAuthenticatedMethod(originalMethod, authProvider, options) {
51
73
  return async function(args, meta) {
52
74
  const token = meta?.authorization?.token;
53
75
  if (!token) {
@@ -64,9 +86,29 @@ function createAuthenticatedMethod(originalMethod, authProvider) {
64
86
  }
65
87
  throw new AuthenticationError(`Token verification failed: ${error instanceof Error ? error.message : String(error)}`, "VERIFICATION_FAILED");
66
88
  }
67
- return originalMethod.apply(this, [
68
- args
69
- ]);
89
+ if (options.getUser !== false) {
90
+ try {
91
+ const user = await authProvider.getUser(token);
92
+ return await authUserStorage.run(user, async () => {
93
+ return await originalMethod.apply(this, [
94
+ args
95
+ ]);
96
+ });
97
+ } catch (error) {
98
+ console.warn("Failed to fetch user information:", error);
99
+ return await authUserStorage.run(void 0, async () => {
100
+ return await originalMethod.apply(this, [
101
+ args
102
+ ]);
103
+ });
104
+ }
105
+ } else {
106
+ return await authUserStorage.run(void 0, async () => {
107
+ return await originalMethod.apply(this, [
108
+ args
109
+ ]);
110
+ });
111
+ }
70
112
  };
71
113
  }
72
114
  __name(createAuthenticatedMethod, "createAuthenticatedMethod");
@@ -123,19 +165,19 @@ var AuthProvider = class extends AuthProviderBase {
123
165
  const finalConfig = config || this.config;
124
166
  switch (this.providerType) {
125
167
  case "cognito": {
126
- const { AuthCognito } = await import("./cognito-2RRSLFKI.mjs");
168
+ const { AuthCognito } = await import("./cognito-I6V5YNYM.mjs");
127
169
  this.providerInstance = new AuthCognito();
128
170
  await this.providerInstance.init(finalConfig);
129
171
  break;
130
172
  }
131
173
  case "auth0": {
132
- const { AuthAuth0 } = await import("./auth0-A25F3NOH.mjs");
174
+ const { AuthAuth0 } = await import("./auth0-54GZT2EI.mjs");
133
175
  this.providerInstance = new AuthAuth0();
134
176
  await this.providerInstance.init(finalConfig);
135
177
  break;
136
178
  }
137
179
  case "clerk": {
138
- const { AuthClerk } = await import("./clerk-XNMGPYSN.mjs");
180
+ const { AuthClerk } = await import("./clerk-FR7ITM33.mjs");
139
181
  this.providerInstance = new AuthClerk();
140
182
  await this.providerInstance.init(finalConfig);
141
183
  break;
@@ -181,6 +223,7 @@ var AuthProvider = class extends AuthProviderBase {
181
223
 
182
224
  export {
183
225
  __name,
226
+ getAuthUser,
184
227
  AuthenticationError,
185
228
  Authenticated,
186
229
  isAuthenticationRequired,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  AuthProviderBase,
3
3
  __name
4
- } from "./chunk-KKO5VLFX.mjs";
4
+ } from "./chunk-EVD2TRPR.mjs";
5
5
 
6
6
  // src/providers/clerk.ts
7
7
  import axios from "axios";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  AuthProviderBase,
3
3
  __name
4
- } from "./chunk-KKO5VLFX.mjs";
4
+ } from "./chunk-EVD2TRPR.mjs";
5
5
 
6
6
  // src/providers/cognito.ts
7
7
  import { CognitoIdentityProviderClient, InitiateAuthCommand } from "@aws-sdk/client-cognito-identity-provider";
package/dist/index.d.mts CHANGED
@@ -1,3 +1,35 @@
1
+ /**
2
+ * Shared types for @leanmcp/auth
3
+ */
4
+ /**
5
+ * Options for the Authenticated decorator
6
+ */
7
+ interface AuthenticatedOptions {
8
+ /**
9
+ * Whether to fetch and attach user information to authUser variable
10
+ * @default true
11
+ */
12
+ getUser?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Global authUser type declaration
17
+ * This makes authUser available in @Authenticated methods without explicit declaration
18
+ */
19
+ declare global {
20
+ /**
21
+ * Authenticated user object automatically available in @Authenticated methods
22
+ *
23
+ * Implemented as a getter that reads from AsyncLocalStorage for concurrency safety.
24
+ * Each request has its own isolated context - 100% safe for concurrent requests.
25
+ */
26
+ const authUser: any;
27
+ }
28
+ /**
29
+ * Get the current authenticated user from the async context
30
+ * This is safe for concurrent requests as each request has its own context
31
+ */
32
+ declare function getAuthUser(): any;
1
33
  /**
2
34
  * Authentication error class for better error handling
3
35
  */
@@ -8,24 +40,40 @@ declare class AuthenticationError extends Error {
8
40
  /**
9
41
  * Decorator to protect MCP tools, prompts, resources, or entire services with authentication
10
42
  *
43
+ * CONCURRENCY SAFE: Uses AsyncLocalStorage to ensure each request has its own isolated
44
+ * authUser context, preventing race conditions in high-concurrency scenarios.
45
+ *
11
46
  * Usage:
12
47
  *
13
- * 1. Protect individual methods:
48
+ * 1. Protect individual methods with automatic user info:
14
49
  * ```typescript
15
50
  * @Tool({ description: 'Analyze sentiment' })
16
- * @Authenticated(authProvider)
51
+ * @Authenticated(authProvider, { getUser: true })
17
52
  * async analyzeSentiment(args: AnalyzeSentimentInput): Promise<AnalyzeSentimentOutput> {
18
- * // This method requires authentication
53
+ * // authUser is automatically available in method scope
54
+ * console.log('User:', authUser);
55
+ * console.log('User ID:', authUser.sub);
56
+ * }
57
+ * ```
58
+ *
59
+ * 2. Protect without fetching user info:
60
+ * ```typescript
61
+ * @Tool({ description: 'Public tool' })
62
+ * @Authenticated(authProvider, { getUser: false })
63
+ * async publicTool(args: PublicToolInput): Promise<PublicToolOutput> {
64
+ * // Only verifies token, doesn't fetch user info
19
65
  * }
20
66
  * ```
21
67
  *
22
- * 2. Protect entire service (all tools/prompts/resources):
68
+ * 3. Protect entire service (all tools/prompts/resources):
23
69
  * ```typescript
24
70
  * @Authenticated(authProvider)
25
71
  * export class SentimentAnalysisService {
26
72
  * @Tool({ description: 'Analyze sentiment' })
27
73
  * async analyzeSentiment(args: AnalyzeSentimentInput) {
28
74
  * // All methods in this service require authentication
75
+ * // authUser is automatically available in all methods
76
+ * console.log('User:', authUser);
29
77
  * }
30
78
  * }
31
79
  * ```
@@ -48,8 +96,9 @@ declare class AuthenticationError extends Error {
48
96
  * ```
49
97
  *
50
98
  * @param authProvider - Instance of AuthProviderBase to use for token verification
99
+ * @param options - Optional configuration for authentication behavior
51
100
  */
52
- declare function Authenticated(authProvider: AuthProviderBase): (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => any;
101
+ declare function Authenticated(authProvider: AuthProviderBase, options?: AuthenticatedOptions): (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => any;
53
102
  /**
54
103
  * Check if a method or class requires authentication
55
104
  */
@@ -118,4 +167,4 @@ declare class AuthProvider extends AuthProviderBase {
118
167
  getProviderType(): string;
119
168
  }
120
169
 
121
- export { AuthProvider, AuthProviderBase, Authenticated, AuthenticationError, getAuthProvider, isAuthenticationRequired };
170
+ export { AuthProvider, AuthProviderBase, Authenticated, type AuthenticatedOptions, AuthenticationError, getAuthProvider, getAuthUser, isAuthenticationRequired };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,35 @@
1
+ /**
2
+ * Shared types for @leanmcp/auth
3
+ */
4
+ /**
5
+ * Options for the Authenticated decorator
6
+ */
7
+ interface AuthenticatedOptions {
8
+ /**
9
+ * Whether to fetch and attach user information to authUser variable
10
+ * @default true
11
+ */
12
+ getUser?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Global authUser type declaration
17
+ * This makes authUser available in @Authenticated methods without explicit declaration
18
+ */
19
+ declare global {
20
+ /**
21
+ * Authenticated user object automatically available in @Authenticated methods
22
+ *
23
+ * Implemented as a getter that reads from AsyncLocalStorage for concurrency safety.
24
+ * Each request has its own isolated context - 100% safe for concurrent requests.
25
+ */
26
+ const authUser: any;
27
+ }
28
+ /**
29
+ * Get the current authenticated user from the async context
30
+ * This is safe for concurrent requests as each request has its own context
31
+ */
32
+ declare function getAuthUser(): any;
1
33
  /**
2
34
  * Authentication error class for better error handling
3
35
  */
@@ -8,24 +40,40 @@ declare class AuthenticationError extends Error {
8
40
  /**
9
41
  * Decorator to protect MCP tools, prompts, resources, or entire services with authentication
10
42
  *
43
+ * CONCURRENCY SAFE: Uses AsyncLocalStorage to ensure each request has its own isolated
44
+ * authUser context, preventing race conditions in high-concurrency scenarios.
45
+ *
11
46
  * Usage:
12
47
  *
13
- * 1. Protect individual methods:
48
+ * 1. Protect individual methods with automatic user info:
14
49
  * ```typescript
15
50
  * @Tool({ description: 'Analyze sentiment' })
16
- * @Authenticated(authProvider)
51
+ * @Authenticated(authProvider, { getUser: true })
17
52
  * async analyzeSentiment(args: AnalyzeSentimentInput): Promise<AnalyzeSentimentOutput> {
18
- * // This method requires authentication
53
+ * // authUser is automatically available in method scope
54
+ * console.log('User:', authUser);
55
+ * console.log('User ID:', authUser.sub);
56
+ * }
57
+ * ```
58
+ *
59
+ * 2. Protect without fetching user info:
60
+ * ```typescript
61
+ * @Tool({ description: 'Public tool' })
62
+ * @Authenticated(authProvider, { getUser: false })
63
+ * async publicTool(args: PublicToolInput): Promise<PublicToolOutput> {
64
+ * // Only verifies token, doesn't fetch user info
19
65
  * }
20
66
  * ```
21
67
  *
22
- * 2. Protect entire service (all tools/prompts/resources):
68
+ * 3. Protect entire service (all tools/prompts/resources):
23
69
  * ```typescript
24
70
  * @Authenticated(authProvider)
25
71
  * export class SentimentAnalysisService {
26
72
  * @Tool({ description: 'Analyze sentiment' })
27
73
  * async analyzeSentiment(args: AnalyzeSentimentInput) {
28
74
  * // All methods in this service require authentication
75
+ * // authUser is automatically available in all methods
76
+ * console.log('User:', authUser);
29
77
  * }
30
78
  * }
31
79
  * ```
@@ -48,8 +96,9 @@ declare class AuthenticationError extends Error {
48
96
  * ```
49
97
  *
50
98
  * @param authProvider - Instance of AuthProviderBase to use for token verification
99
+ * @param options - Optional configuration for authentication behavior
51
100
  */
52
- declare function Authenticated(authProvider: AuthProviderBase): (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => any;
101
+ declare function Authenticated(authProvider: AuthProviderBase, options?: AuthenticatedOptions): (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => any;
53
102
  /**
54
103
  * Check if a method or class requires authentication
55
104
  */
@@ -118,4 +167,4 @@ declare class AuthProvider extends AuthProviderBase {
118
167
  getProviderType(): string;
119
168
  }
120
169
 
121
- export { AuthProvider, AuthProviderBase, Authenticated, AuthenticationError, getAuthProvider, isAuthenticationRequired };
170
+ export { AuthProvider, AuthProviderBase, Authenticated, type AuthenticatedOptions, AuthenticationError, getAuthProvider, getAuthUser, isAuthenticationRequired };
package/dist/index.js CHANGED
@@ -32,11 +32,19 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
32
32
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
33
 
34
34
  // src/decorators.ts
35
- function Authenticated(authProvider) {
35
+ function getAuthUser() {
36
+ return authUserStorage.getStore();
37
+ }
38
+ function Authenticated(authProvider, options) {
39
+ const authOptions = {
40
+ getUser: true,
41
+ ...options
42
+ };
36
43
  return function(target, propertyKey, descriptor) {
37
44
  if (!propertyKey && !descriptor) {
38
45
  Reflect.defineMetadata("auth:provider", authProvider, target);
39
46
  Reflect.defineMetadata("auth:required", true, target);
47
+ Reflect.defineMetadata("auth:options", authOptions, target);
40
48
  const prototype = target.prototype;
41
49
  const methodNames = Object.getOwnPropertyNames(prototype).filter((name) => name !== "constructor" && typeof prototype[name] === "function");
42
50
  for (const methodName of methodNames) {
@@ -45,7 +53,8 @@ function Authenticated(authProvider) {
45
53
  const originalMethod = originalDescriptor.value;
46
54
  Reflect.defineMetadata("auth:provider", authProvider, originalMethod);
47
55
  Reflect.defineMetadata("auth:required", true, originalMethod);
48
- prototype[methodName] = createAuthenticatedMethod(originalMethod, authProvider);
56
+ Reflect.defineMetadata("auth:options", authOptions, originalMethod);
57
+ prototype[methodName] = createAuthenticatedMethod(originalMethod, authProvider, authOptions);
49
58
  copyMetadata(originalMethod, prototype[methodName]);
50
59
  }
51
60
  }
@@ -55,14 +64,15 @@ function Authenticated(authProvider) {
55
64
  const originalMethod = descriptor.value;
56
65
  Reflect.defineMetadata("auth:provider", authProvider, originalMethod);
57
66
  Reflect.defineMetadata("auth:required", true, originalMethod);
58
- descriptor.value = createAuthenticatedMethod(originalMethod, authProvider);
67
+ Reflect.defineMetadata("auth:options", authOptions, originalMethod);
68
+ descriptor.value = createAuthenticatedMethod(originalMethod, authProvider, authOptions);
59
69
  copyMetadata(originalMethod, descriptor.value);
60
70
  return descriptor;
61
71
  }
62
72
  throw new Error("@Authenticated can only be applied to classes or methods");
63
73
  };
64
74
  }
65
- function createAuthenticatedMethod(originalMethod, authProvider) {
75
+ function createAuthenticatedMethod(originalMethod, authProvider, options) {
66
76
  return async function(args, meta) {
67
77
  const token = meta?.authorization?.token;
68
78
  if (!token) {
@@ -79,9 +89,29 @@ function createAuthenticatedMethod(originalMethod, authProvider) {
79
89
  }
80
90
  throw new AuthenticationError(`Token verification failed: ${error instanceof Error ? error.message : String(error)}`, "VERIFICATION_FAILED");
81
91
  }
82
- return originalMethod.apply(this, [
83
- args
84
- ]);
92
+ if (options.getUser !== false) {
93
+ try {
94
+ const user = await authProvider.getUser(token);
95
+ return await authUserStorage.run(user, async () => {
96
+ return await originalMethod.apply(this, [
97
+ args
98
+ ]);
99
+ });
100
+ } catch (error) {
101
+ console.warn("Failed to fetch user information:", error);
102
+ return await authUserStorage.run(void 0, async () => {
103
+ return await originalMethod.apply(this, [
104
+ args
105
+ ]);
106
+ });
107
+ }
108
+ } else {
109
+ return await authUserStorage.run(void 0, async () => {
110
+ return await originalMethod.apply(this, [
111
+ args
112
+ ]);
113
+ });
114
+ }
85
115
  };
86
116
  }
87
117
  function copyMetadata(source, target) {
@@ -108,11 +138,23 @@ function isAuthenticationRequired(target) {
108
138
  function getAuthProvider(target) {
109
139
  return Reflect.getMetadata("auth:provider", target);
110
140
  }
111
- var import_reflect_metadata, AuthenticationError;
141
+ var import_reflect_metadata, import_async_hooks, authUserStorage, AuthenticationError;
112
142
  var init_decorators = __esm({
113
143
  "src/decorators.ts"() {
114
144
  "use strict";
115
145
  import_reflect_metadata = require("reflect-metadata");
146
+ import_async_hooks = require("async_hooks");
147
+ authUserStorage = new import_async_hooks.AsyncLocalStorage();
148
+ __name(getAuthUser, "getAuthUser");
149
+ if (typeof globalThis !== "undefined" && !Object.getOwnPropertyDescriptor(globalThis, "authUser")) {
150
+ Object.defineProperty(globalThis, "authUser", {
151
+ get() {
152
+ return authUserStorage.getStore();
153
+ },
154
+ configurable: true,
155
+ enumerable: false
156
+ });
157
+ }
116
158
  AuthenticationError = class extends Error {
117
159
  static {
118
160
  __name(this, "AuthenticationError");
@@ -513,6 +555,7 @@ __export(index_exports, {
513
555
  Authenticated: () => Authenticated,
514
556
  AuthenticationError: () => AuthenticationError,
515
557
  getAuthProvider: () => getAuthProvider,
558
+ getAuthUser: () => getAuthUser,
516
559
  isAuthenticationRequired: () => isAuthenticationRequired
517
560
  });
518
561
  module.exports = __toCommonJS(index_exports);
@@ -610,5 +653,6 @@ init_index();
610
653
  Authenticated,
611
654
  AuthenticationError,
612
655
  getAuthProvider,
656
+ getAuthUser,
613
657
  isAuthenticationRequired
614
658
  });
package/dist/index.mjs CHANGED
@@ -4,13 +4,15 @@ import {
4
4
  Authenticated,
5
5
  AuthenticationError,
6
6
  getAuthProvider,
7
+ getAuthUser,
7
8
  isAuthenticationRequired
8
- } from "./chunk-KKO5VLFX.mjs";
9
+ } from "./chunk-EVD2TRPR.mjs";
9
10
  export {
10
11
  AuthProvider,
11
12
  AuthProviderBase,
12
13
  Authenticated,
13
14
  AuthenticationError,
14
15
  getAuthProvider,
16
+ getAuthUser,
15
17
  isAuthenticationRequired
16
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanmcp/auth",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Authentication and identity module supporting multiple providers",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -24,7 +24,7 @@
24
24
  "test:watch": "jest --watch"
25
25
  },
26
26
  "dependencies": {
27
- "@leanmcp/core": "^0.2.0",
27
+ "@leanmcp/core": "^0.3.0",
28
28
  "reflect-metadata": "^0.2.1"
29
29
  },
30
30
  "devDependencies": {