@@ -5,6 +5,7 @@ import * as lambda from 'aws-cdk-lib/aws-lambda';
55import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs' ;
66import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2' ;
77import * as apigatewayv2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations' ;
8+ import * as apigatewayv2Authorizers from 'aws-cdk-lib/aws-apigatewayv2-authorizers' ;
89import * as logs from 'aws-cdk-lib/aws-logs' ;
910import * as route53 from 'aws-cdk-lib/aws-route53' ;
1011import * as route53Targets from 'aws-cdk-lib/aws-route53-targets' ;
@@ -230,63 +231,56 @@ export class SmalrubyAdminStack extends cdk.Stack {
230231 handlerFn ,
231232 ) ;
232233
233- this . api . addRoutes ( {
234- path : '/admin/me' ,
235- methods : [ apigatewayv2 . HttpMethod . GET ] ,
236- integration,
237- } ) ;
234+ // DoS cost defense (source is public — attackers hammer the known
235+ // endpoint). In prod, a gateway-level Google JWT authorizer rejects any
236+ // request without a validly-signed token for our admin audience BEFORE
237+ // the Lambda runs, so an unauthenticated flood cannot burn Lambda
238+ // invocations or Lambda log ingestion — only (throttled) API-GW requests.
239+ // The Lambda still re-verifies + checks the allowlist (defense in depth).
240+ //
241+ // stg keeps NO authorizer so the dev-bypass token (not a JWT) still works
242+ // for automated E2E. stg is the same fail-closed Lambda auth, just without
243+ // the extra gateway filter.
244+ const authorizer = ( stage === 'prod' && adminGoogleClientId )
245+ ? new apigatewayv2Authorizers . HttpJwtAuthorizer (
246+ 'AdminJwtAuthorizer' ,
247+ 'https://accounts.google.com' ,
248+ {
249+ identitySource : [ '$request.header.Authorization' ] ,
250+ jwtAudience : [ adminGoogleClientId ] ,
251+ } ,
252+ )
253+ : undefined ;
254+
255+ const addRoute = ( routePath : string , methods : apigatewayv2 . HttpMethod [ ] ) => {
256+ this . api . addRoutes ( { path : routePath , methods, integration, authorizer } ) ;
257+ } ;
258+
259+ addRoute ( '/admin/me' , [ apigatewayv2 . HttpMethod . GET ] ) ;
238260
239261 // みんなの課題 moderation (S3 #1083)
240- this . api . addRoutes ( {
241- path : '/admin/shared-assignments' ,
242- methods : [ apigatewayv2 . HttpMethod . GET ] ,
243- integration,
244- } ) ;
245- this . api . addRoutes ( {
246- path : '/admin/shared-assignments/reports' ,
247- methods : [ apigatewayv2 . HttpMethod . GET ] ,
248- integration,
249- } ) ;
250- this . api . addRoutes ( {
251- path : '/admin/shared-assignments/{sharedId}' ,
252- methods : [ apigatewayv2 . HttpMethod . GET , apigatewayv2 . HttpMethod . PATCH ] ,
253- integration,
254- } ) ;
262+ addRoute ( '/admin/shared-assignments' , [ apigatewayv2 . HttpMethod . GET ] ) ;
263+ addRoute ( '/admin/shared-assignments/reports' , [ apigatewayv2 . HttpMethod . GET ] ) ;
264+ addRoute ( '/admin/shared-assignments/{sharedId}' ,
265+ [ apigatewayv2 . HttpMethod . GET , apigatewayv2 . HttpMethod . PATCH ] ) ;
255266
256267 // Classroom management + expired restore (S4 #1084). HTTP API prefers
257268 // the literal restore-candidates route over {classroomId} by specificity.
258- this . api . addRoutes ( {
259- path : '/admin/classrooms' ,
260- methods : [ apigatewayv2 . HttpMethod . GET ] ,
261- integration,
262- } ) ;
263- this . api . addRoutes ( {
264- path : '/admin/classrooms/restore-candidates' ,
265- methods : [ apigatewayv2 . HttpMethod . GET ] ,
266- integration,
267- } ) ;
268- this . api . addRoutes ( {
269- path : '/admin/classrooms/{classroomId}' ,
270- methods : [ apigatewayv2 . HttpMethod . GET , apigatewayv2 . HttpMethod . PATCH ] ,
271- integration,
272- } ) ;
273- this . api . addRoutes ( {
274- path : '/admin/classrooms/{classroomId}/restore-plan' ,
275- methods : [ apigatewayv2 . HttpMethod . GET ] ,
276- integration,
277- } ) ;
278- this . api . addRoutes ( {
279- path : '/admin/classrooms/{classroomId}/restore' ,
280- methods : [ apigatewayv2 . HttpMethod . POST ] ,
281- integration,
282- } ) ;
283-
284- // Single-operator tool: keep throttling tight.
269+ addRoute ( '/admin/classrooms' , [ apigatewayv2 . HttpMethod . GET ] ) ;
270+ addRoute ( '/admin/classrooms/restore-candidates' , [ apigatewayv2 . HttpMethod . GET ] ) ;
271+ addRoute ( '/admin/classrooms/{classroomId}' ,
272+ [ apigatewayv2 . HttpMethod . GET , apigatewayv2 . HttpMethod . PATCH ] ) ;
273+ addRoute ( '/admin/classrooms/{classroomId}/restore-plan' , [ apigatewayv2 . HttpMethod . GET ] ) ;
274+ addRoute ( '/admin/classrooms/{classroomId}/restore' , [ apigatewayv2 . HttpMethod . POST ] ) ;
275+
276+ // Single-operator tool: a human never needs more than a couple of
277+ // requests per second, so throttle hard to cap the API-GW request bill an
278+ // attacker can run up (the JWT authorizer already caps Lambda/log cost).
285279 const defaultStage = this . api . defaultStage ?. node . defaultChild as apigatewayv2 . CfnStage ;
286280 if ( defaultStage ) {
287281 defaultStage . defaultRouteSettings = {
288- throttlingRateLimit : 10 ,
289- throttlingBurstLimit : 20 ,
282+ throttlingRateLimit : 5 ,
283+ throttlingBurstLimit : 10 ,
290284 } ;
291285 }
292286
0 commit comments