@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.
- package/docs/guides/fastapi.mdx +210 -0
- package/docs/guides/trpc.mdx +4 -4
- package/docs/guides/ts-smithy-api.mdx +145 -0
- package/package.json +1 -1
package/docs/guides/fastapi.mdx
CHANGED
|
@@ -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
|
|
package/docs/guides/trpc.mdx
CHANGED
|
@@ -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
|
|
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?.
|
|
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[
|
|
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,
|
|
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:
|