@aws/nx-plugin-mcp 1.0.0-rc.20 → 1.0.0-rc.21

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.
@@ -185,6 +185,216 @@ Unhandled exceptions are caught by the middleware and:
185
185
  It's recommended to specify response models for your API operations for better code generation if using the `connection` generator. <Link path="guides/connection/react-fastapi#errors">See here for more details</Link>.
186
186
  :::
187
187
 
188
+ ### Accessing the Calling User
189
+
190
+ When your API is protected by authentication, your route handlers often need to know who is calling. The generated FastAPI runs inside AWS Lambda via the [Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter), which forwards the API Gateway request context as JSON on the `x-amzn-request-context` header. You can read it from the FastAPI `Request` to extract the caller's identity.
191
+
192
+ As an example, let's add a `/me` endpoint that returns details about the calling user. We'll implement the extraction as a [FastAPI dependency](https://fastapi.tiangolo.com/tutorial/dependencies/) so it can be reused across routes. The shape of the request context — and therefore how you extract the identity — depends on both your selected `auth` method and whether you deployed a REST or HTTP API.
193
+
194
+ <OptionFilter when={{ auth: 'iam' }} description="Identity extraction for IAM-authenticated APIs">
195
+ For `IAM` authentication, we look up the caller in Cognito using the sub extracted from the API Gateway request context. Create `identity.py` alongside `main.py`:
196
+
197
+ <Tabs syncKey="http-rest">
198
+ <TabItem label="REST API" _filter={{ infra: 'rest-lambda' }}>
199
+ ```python
200
+ import json
201
+ import os
202
+ from typing import Annotated
203
+
204
+ from boto3 import client
205
+ from fastapi import Depends, HTTPException, Request
206
+ from pydantic import BaseModel
207
+
208
+ cognito = client("cognito-idp")
209
+
210
+
211
+ class Identity(BaseModel):
212
+ sub: str
213
+ username: str
214
+
215
+
216
+ def get_identity(request: Request) -> Identity:
217
+ # The Lambda Web Adapter forwards the API Gateway request context as JSON
218
+ request_context_header = request.headers.get("x-amzn-request-context")
219
+ if not request_context_header:
220
+ raise HTTPException(status_code=403, detail="Unable to determine calling user")
221
+
222
+ request_context = json.loads(request_context_header)
223
+ provider = request_context.get("identity", {}).get("cognitoAuthenticationProvider")
224
+
225
+ sub = provider.split(":")[-1] if provider else None
226
+ if not sub:
227
+ raise HTTPException(status_code=403, detail="Unable to determine calling user")
228
+
229
+ users = cognito.list_users(
230
+ # Assumes user pool id is configured in lambda environment
231
+ UserPoolId=os.environ["USER_POOL_ID"],
232
+ Limit=1,
233
+ Filter=f'sub="{sub}"',
234
+ ).get("Users", [])
235
+
236
+ if len(users) != 1:
237
+ raise HTTPException(status_code=403, detail=f"No user found with subjectId {sub}")
238
+
239
+ return Identity(sub=sub, username=users[0]["Username"])
240
+
241
+
242
+ CurrentUser = Annotated[Identity, Depends(get_identity)]
243
+ ```
244
+ </TabItem>
245
+ <TabItem label="HTTP API" _filter={{ infra: 'http-lambda' }}>
246
+ ```python
247
+ import json
248
+ import os
249
+ from typing import Annotated
250
+
251
+ from boto3 import client
252
+ from fastapi import Depends, HTTPException, Request
253
+ from pydantic import BaseModel
254
+
255
+ cognito = client("cognito-idp")
256
+
257
+
258
+ class Identity(BaseModel):
259
+ sub: str
260
+ username: str
261
+
262
+
263
+ def get_identity(request: Request) -> Identity:
264
+ # The Lambda Web Adapter forwards the API Gateway request context as JSON
265
+ request_context_header = request.headers.get("x-amzn-request-context")
266
+ if not request_context_header:
267
+ raise HTTPException(status_code=403, detail="Unable to determine calling user")
268
+
269
+ request_context = json.loads(request_context_header)
270
+ amr = (
271
+ request_context.get("authorizer", {})
272
+ .get("iam", {})
273
+ .get("cognitoIdentity", {})
274
+ .get("amr", [])
275
+ )
276
+ sign_in = next((s for s in amr if ":CognitoSignIn:" in s), None)
277
+ sub = sign_in.split(":")[-1] if sign_in else None
278
+
279
+ if not sub:
280
+ raise HTTPException(status_code=403, detail="Unable to determine calling user")
281
+
282
+ users = cognito.list_users(
283
+ # Assumes user pool id is configured in lambda environment
284
+ UserPoolId=os.environ["USER_POOL_ID"],
285
+ Limit=1,
286
+ Filter=f'sub="{sub}"',
287
+ ).get("Users", [])
288
+
289
+ if len(users) != 1:
290
+ raise HTTPException(status_code=403, detail=f"No user found with subjectId {sub}")
291
+
292
+ return Identity(sub=sub, username=users[0]["Username"])
293
+
294
+
295
+ CurrentUser = Annotated[Identity, Depends(get_identity)]
296
+ ```
297
+ </TabItem>
298
+ </Tabs>
299
+ </OptionFilter>
300
+
301
+ <OptionFilter when={{ auth: 'cognito' }} description="Identity extraction for Cognito-authenticated APIs">
302
+ With `auth: 'cognito'`, the API Gateway Cognito User Pools authorizer verifies the JWT that the caller supplies in the `Authorization` header and places the verified claims on the request context.
303
+
304
+ Create `identity.py` alongside `main.py`:
305
+
306
+ <Tabs syncKey="http-rest">
307
+ <TabItem label="REST API" _filter={{ infra: 'rest-lambda' }}>
308
+ ```python
309
+ import json
310
+ from typing import Annotated
311
+
312
+ from fastapi import Depends, HTTPException, Request
313
+ from pydantic import BaseModel
314
+
315
+
316
+ class Identity(BaseModel):
317
+ sub: str
318
+ username: str
319
+
320
+
321
+ def get_identity(request: Request) -> Identity:
322
+ # The Lambda Web Adapter forwards the API Gateway request context as JSON
323
+ request_context_header = request.headers.get("x-amzn-request-context")
324
+ if not request_context_header:
325
+ raise HTTPException(status_code=403, detail="Unable to determine calling user")
326
+
327
+ request_context = json.loads(request_context_header)
328
+ claims = request_context.get("authorizer", {}).get("claims", {})
329
+
330
+ sub = claims.get("sub")
331
+ username = claims.get("username")
332
+
333
+ if not sub or not username:
334
+ raise HTTPException(status_code=403, detail="Unable to determine calling user")
335
+
336
+ return Identity(sub=sub, username=username)
337
+
338
+
339
+ CurrentUser = Annotated[Identity, Depends(get_identity)]
340
+ ```
341
+ </TabItem>
342
+ <TabItem label="HTTP API" _filter={{ infra: 'http-lambda' }}>
343
+ HTTP APIs use a JWT authorizer which places the verified claims under `authorizer.jwt.claims`:
344
+
345
+ ```python
346
+ import json
347
+ from typing import Annotated
348
+
349
+ from fastapi import Depends, HTTPException, Request
350
+ from pydantic import BaseModel
351
+
352
+
353
+ class Identity(BaseModel):
354
+ sub: str
355
+ username: str
356
+
357
+
358
+ def get_identity(request: Request) -> Identity:
359
+ # The Lambda Web Adapter forwards the API Gateway request context as JSON
360
+ request_context_header = request.headers.get("x-amzn-request-context")
361
+ if not request_context_header:
362
+ raise HTTPException(status_code=403, detail="Unable to determine calling user")
363
+
364
+ request_context = json.loads(request_context_header)
365
+ claims = request_context.get("authorizer", {}).get("jwt", {}).get("claims", {})
366
+
367
+ sub = claims.get("sub")
368
+ username = claims.get("username")
369
+
370
+ if not sub or not username:
371
+ raise HTTPException(status_code=403, detail="Unable to determine calling user")
372
+
373
+ return Identity(sub=sub, username=username)
374
+
375
+
376
+ CurrentUser = Annotated[Identity, Depends(get_identity)]
377
+ ```
378
+ </TabItem>
379
+ </Tabs>
380
+
381
+ :::tip[No token verification required]
382
+ You don't need any JWT-verification library here — the API Gateway Cognito User Pools authorizer has already verified the signature, issuer, scopes, and expiry by the time your Lambda runs. If any of those checks fail, API Gateway returns `401 Unauthorized` and your handler is never invoked.
383
+ :::
384
+ </OptionFilter>
385
+
386
+ You can then inject the `CurrentUser` dependency into any route that needs the caller's identity:
387
+
388
+ ```python
389
+ from .identity import CurrentUser, Identity
390
+ from .init import app, tracer
391
+
392
+ @app.get("/me")
393
+ @tracer.capture_method
394
+ def me(identity: CurrentUser) -> Identity:
395
+ return identity
396
+ ```
397
+
188
398
  <OptionFilter when={{ infra: 'rest-lambda' }} description="Streaming — REST API only">
189
399
  ### Streaming
190
400
 
@@ -478,7 +478,7 @@ export interface IIdentityContext {
478
478
 
479
479
  Note that we define an additional _optional_ property on the context. tRPC manages ensuring that this is defined in procedures which have correctly configured this middleware.
480
480
 
481
- Next, the middleware itself. The generator wires the authorizer to accept ID tokens, so `cognito:username` is the canonical username claim; we fall back to `username` for access tokens in case you reconfigure the authorizer:
481
+ Next, the middleware itself:
482
482
 
483
483
  ```ts
484
484
  import { initTRPC, TRPCError } from '@trpc/server';
@@ -503,7 +503,7 @@ export const createIdentityPlugin = () => {
503
503
  | undefined;
504
504
 
505
505
  const sub = claims?.sub;
506
- const username = claims?.['cognito:username'] ?? claims?.username;
506
+ const username = claims?.username;
507
507
 
508
508
  if (!sub || !username) {
509
509
  throw new TRPCError({
@@ -541,8 +541,8 @@ export const me = publicProcedure
541
541
  }));
542
542
  ```
543
543
 
544
- :::tip[Verifying the token]
545
- You don't need `aws-jwt-verify` or any other JWT-verification library here — the API Gateway Cognito User Pools authorizer has already verified the signature, issuer, audience, and expiry by the time your Lambda runs. If any of those checks fail, API Gateway returns `401 Unauthorized` and your handler is never invoked.
544
+ :::tip[No token verification required]
545
+ You don't need `aws-jwt-verify` or any other JWT-verification library here — the API Gateway Cognito User Pools authorizer has already verified the signature, issuer, scopes, and expiry by the time your Lambda runs. If any of those checks fail, API Gateway returns `401 Unauthorized` and your handler is never invoked.
546
546
  :::
547
547
  </OptionFilter>
548
548
 
@@ -401,6 +401,151 @@ export const MyOperation: MyOperationHandler<ServiceContext> = async (input) =>
401
401
  };
402
402
  ```
403
403
 
404
+ ### Accessing the Calling User
405
+
406
+ When your API is protected by authentication, your operations often need to know who is calling. The recommended approach is to resolve the caller's identity once in the handler and pass it through the [service context](#service-context) for consumption by specific operations.
407
+
408
+ We'll model the unauthorized case as a Smithy error so it serializes to a proper `403` response. Add it to your model, for example in `model/src/operations/errors.smithy`, and reference it on any operation that requires identity:
409
+
410
+ ```smithy
411
+ $version: "2.0"
412
+
413
+ namespace your.namespace
414
+
415
+ /// Thrown when the calling user cannot be determined
416
+ @error("client")
417
+ @httpError(403)
418
+ structure UnauthorizedError {
419
+ @required
420
+ message: String
421
+ }
422
+ ```
423
+
424
+ First, expose the resolved identity on the service context in `src/context.ts`. We provide it as a function so that the `UnauthorizedError` is thrown from within an operation (where the Server SDK serializes it to a `403`), rather than from the handler:
425
+
426
+ ```ts {4-7,15} ins={4-7,15}
427
+ import { Logger } from '@aws-lambda-powertools/logger';
428
+ import { Metrics } from '@aws-lambda-powertools/metrics';
429
+ import { Tracer } from '@aws-lambda-powertools/tracer';
430
+
431
+ export interface Identity {
432
+ sub: string;
433
+ username: string;
434
+ }
435
+
436
+ /**
437
+ * Context provided to all operations.
438
+ */
439
+ export interface ServiceContext {
440
+ tracer: Tracer;
441
+ logger: Logger;
442
+ metrics: Metrics;
443
+ getIdentity: () => Promise<Identity>;
444
+ }
445
+ ```
446
+
447
+ Next, write the resolver in `src/identity.ts`. It throws `UnauthorizedError` when the caller cannot be determined. The implementation depends on your selected `auth` method:
448
+
449
+ <OptionFilter when={{ auth: 'iam' }} description="Identity resolution for IAM-authenticated APIs">
450
+ For `IAM` authentication, we look up the caller in Cognito using the sub extracted from the API Gateway event:
451
+
452
+ ```ts
453
+ import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';
454
+ import type { APIGatewayProxyEvent } from 'aws-lambda';
455
+ import { Identity } from './context.js';
456
+ import { UnauthorizedError } from './generated/ssdk/index.js';
457
+
458
+ const cognito = new CognitoIdentityProvider();
459
+
460
+ export const getIdentity = async (
461
+ event: APIGatewayProxyEvent,
462
+ ): Promise<Identity> => {
463
+ const cognitoAuthenticationProvider =
464
+ event.requestContext?.identity?.cognitoAuthenticationProvider;
465
+
466
+ let sub: string | undefined = undefined;
467
+ if (cognitoAuthenticationProvider) {
468
+ const providerParts = cognitoAuthenticationProvider.split(':');
469
+ sub = providerParts[providerParts.length - 1];
470
+ }
471
+
472
+ if (!sub) {
473
+ throw new UnauthorizedError({ message: 'Unable to determine calling user' });
474
+ }
475
+
476
+ const { Users } = await cognito.listUsers({
477
+ // Assumes user pool id is configured in lambda environment
478
+ UserPoolId: process.env.USER_POOL_ID!,
479
+ Limit: 1,
480
+ Filter: `sub="${sub}"`,
481
+ });
482
+
483
+ if (!Users || Users.length !== 1) {
484
+ throw new UnauthorizedError({ message: `No user found with subjectId ${sub}` });
485
+ }
486
+
487
+ return { sub, username: Users[0].Username! };
488
+ };
489
+ ```
490
+ </OptionFilter>
491
+
492
+ <OptionFilter when={{ auth: 'cognito' }} description="Identity resolution for Cognito-authenticated APIs">
493
+ With `auth: 'cognito'`, the API Gateway Cognito User Pools authorizer verifies the JWT that the caller supplies in the `Authorization` header and places the verified claims on the event at `event.requestContext.authorizer.claims`:
494
+
495
+ ```ts
496
+ import type { APIGatewayProxyEvent } from 'aws-lambda';
497
+ import { Identity } from './context.js';
498
+ import { UnauthorizedError } from './generated/ssdk/index.js';
499
+
500
+ export const getIdentity = async (
501
+ event: APIGatewayProxyEvent,
502
+ ): Promise<Identity> => {
503
+ const claims = event.requestContext?.authorizer?.claims as
504
+ | Record<string, string>
505
+ | undefined;
506
+
507
+ const sub = claims?.sub;
508
+ const username = claims?.username;
509
+
510
+ if (!sub || !username) {
511
+ throw new UnauthorizedError({ message: 'Unable to determine calling user' });
512
+ }
513
+
514
+ return { sub, username };
515
+ };
516
+ ```
517
+
518
+ :::tip[No token verification required]
519
+ You don't need `aws-jwt-verify` or any other JWT-verification library here — the API Gateway Cognito User Pools authorizer has already verified the signature, issuer, scopes, and expiry by the time your Lambda runs. If any of those checks fail, API Gateway returns `401 Unauthorized` and your handler is never invoked.
520
+ :::
521
+ </OptionFilter>
522
+
523
+ Then wire the resolver into the context in `src/handler.ts`:
524
+
525
+ ```ts {2,8} ins={2,8}
526
+ import { Service } from './service.js';
527
+ import { getIdentity } from './identity.js';
528
+ // ...
529
+ const httpResponse = await serviceHandler.handle(httpRequest, {
530
+ tracer,
531
+ logger,
532
+ metrics,
533
+ getIdentity: () => getIdentity(event),
534
+ });
535
+ ```
536
+
537
+ We can now use the resolved identity in an operation, for example in `src/operations/echo.ts`:
538
+
539
+ ```ts
540
+ import { ServiceContext } from '../context.js';
541
+ import { Echo as EchoOperation } from '../generated/ssdk/index.js';
542
+
543
+ export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => {
544
+ const identity = await ctx.getIdentity();
545
+ return { message: `${identity.username} says ${input.message}` };
546
+ };
547
+ ```
548
+
404
549
  ## Building and Code Generation
405
550
 
406
551
  The Smithy model project uses [Docker](https://www.docker.com/) to build the Smithy artifacts and generate the TypeScript Server SDK:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws/nx-plugin-mcp",
3
- "version": "1.0.0-rc.20",
3
+ "version": "1.0.0-rc.21",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/awslabs/nx-plugin-for-aws.git",