AWS
stephen-ade.github.io
AWS Amplify Hosting for ML/AI Services Guide
Technical Guide
AWS Architecture Guide
· Part 1 of 2

AWS Amplify Hosting for ML/AI Services Guide

Architecture and access patterns for enterprise Angular SPAs accessing AWS ML services with multi-layer security. For CDK stacks, Lambda code, and CI/CD pipelines, see Part 2.

Version 1.1
Updated March 2026
Audience Solution Architects · Cloud Engineers · DevOps
Covers Amplify · Cognito · API GW · SageMaker · Bedrock
Two-part series. This guide covers architecture & access patterns. For the full implementation reference with CDK, Lambda, and CI/CD, see the companion guide. Part 2: ML Services Integration Guide
Section 1

1Introduction and Overview

Purpose and Scope

This comprehensive guide provides a detailed roadmap for designing, developing, integrating, and deploying AWS Amplify hosting for Single Page Applications (SPAs) that enable corporate users to access AWS Machine Learning services. The guide focuses on establishing a secure, scalable, and efficient architecture that integrates with enterprise identity systems while leveraging AWS's powerful ML offerings including SageMaker, Bedrock, and Lambda processors for AI capabilities.

This guide serves as a blueprint for organizations looking to provide their employees with secure access to advanced AI capabilities while maintaining corporate security standards and governance requirements.

The scope encompasses:

  • Front-end development using the Angular framework
  • Authentication via Entra ID integration through AWS Cognito
  • Secure API access to internal ML endpoints
  • Multi-layered security architecture
  • Implementation and deployment workflows
  • Best practices for corporate environments

Target Audience

This guide is intended for the following roles. Readers should have intermediate to advanced knowledge of AWS services, authentication mechanisms, and modern web application development practices.

  • Solution Architects responsible for designing ML service access solutions
  • Cloud Engineers implementing AWS services and security controls
  • Full Stack Developers working on Angular SPAs and AWS integrations
  • DevOps Engineers managing deployments and CI/CD pipelines
  • Security Engineers ensuring compliance with corporate security requirements
  • IT Managers overseeing the implementation of AI capabilities within their organization

Prerequisites

To successfully implement the solutions described in this guide, the following prerequisites are necessary.

Knowledge Prerequisites

  • Understanding of Angular framework and SPA development
  • Familiarity with AWS services, particularly Amplify, Cognito, API Gateway, Lambda, SageMaker, and Bedrock
  • Knowledge of authentication protocols (OAuth 2.0, OIDC) and JWT token validation
  • Experience with cloud security best practices
  • Basic understanding of ML/AI service consumption patterns

Technical Prerequisites

  • AWS account with appropriate permissions to create and manage required services
  • Entra ID (formerly Azure AD) tenant with administrative access
  • Node.js and Angular development environment
  • AWS CLI and Amplify CLI configured locally
  • Access to internal ML model endpoints and permissions to AWS Bedrock services
  • Git repository for source code management
Bedrock Model Access Requirement Amazon Bedrock foundation models are not enabled by default in any AWS account. Each model must be explicitly requested and approved before use. Navigate to Amazon Bedrock → Model Access → Manage model access in the AWS Console to request access to the models you need (e.g. Anthropic Claude, Meta Llama, Amazon Nova). Model availability also varies by AWS region. Allow up to 24 hours for approval of third-party models.
Scope Note This guide assumes that ML models are already developed and deployed to SageMaker endpoints or accessible via Bedrock APIs. The focus is on building the frontend application and the secure access architecture.

Architecture Benefits

🛡
Security
  • Multi-layer defense at each tier
  • Edge protection via WAF + CloudFront
  • Entra ID + Cognito federation
  • Lambda authorizer token validation
  • Fine-grained IAM access control
  • Encrypted data throughout
Performance
  • Global CloudFront edge delivery
  • Static asset caching
  • Optimized API routing
  • Scalable Amplify hosting
  • Async ML service consumption
  • Serverless Lambda scalability
Operational
  • Built-in CI/CD with Amplify
  • Simplified UAT/PROD deployments
  • Centralized monitoring & logging
  • Automated demand-based scaling
  • Managed service overhead reduction
📈
Business
  • Democratized ML/AI access
  • Corporate identity UX consistency
  • Serverless cost optimization
  • Accelerated AI feature delivery
  • Enterprise-grade compliance
Section 2

2Architecture Overview

High-Level Architecture Diagrams

Overall System Architecture

Phase 1 — App Delivery
① Corporate User
Web browser — requests SPA
Edge Layer
② WAF
Edge protection
③ CloudFront CDN
Cache · SSL · Global edge
④ Amplify Hosting
Origin: static SPA files
↺ Cache miss: CloudFront fetches from Amplify & caches · Cache hit: serves directly (no Amplify call)
✕ WAF block at CloudFront edge: threat matched → 403 returned before forwarding to Amplify origin
⑤ SPA loaded into browser
Phase 2 — Authentication
Authentication Round-Trip Loopback
⑥ Angular SPA
signInWithRedirect()
⑦ AWS Cognito
Hosted UI · SAML redirect
⑧ Entra ID
Validate credentials
→ forward leg (request)
⑪ Angular SPA
JWT stored & session active
⑩ AWS Cognito
Issues Access + ID + Refresh JWT
⑨ Entra ID
SAML assertion → Cognito
← return leg (loopback — JWT arrives back at SPA)
↺ Token refresh loopback: SPA silently calls Cognito with refresh token → new access token returned (every ~1 hr, no user interaction)
Phase 3 — Authorised API Request
⑫ SPA attaches JWT → API request
API Layer
⑬ WAF
Inspect & filter
⑭ Regional API Gateway
Invoke authorizer
⑮ Lambda Authorizer
Verify JWT · check groups
⑭ Regional API Gateway
Enforce decision
⑮ Lambda Authorizer
Returns IAM policy
↺ Authorizer loopback: Lambda returns Allow/Deny IAM policy back to API Gateway · Deny = 403 to SPA
✕ WAF block: 403 short-circuit directly to SPA — request never reaches API Gateway
Phase 4 — ML Processing & Response
ML Services Layer
⑯ Lambda Processor
ML Orchestration
⑰ SageMaker Endpoint
Custom model inference
inference result
⑰ Bedrock Model
Foundation model inference
inference result
↺ ML response loopback: inference result returns to Lambda Processor → API Gateway → WAF → Browser (API responses travel directly to the browser — not through the Amplify CloudFront distribution, which serves only static SPA files)
⑱ Response travels back: ML → Lambda → API Gateway → WAF → Browser
① Corporate User
Receives ML result in browser
Request / forward flow Response / loopback WAF block / short-circuit

Figure 1: Overall System Architecture — with loopbacks

Network Security Architecture

Phase 1 — Inbound Request
① Internet
User's browser — HTTPS request
Edge Protection
② WAF
Edge Protection
SQLi · XSS · Rate limits · Geo-block
③ CloudFront CDN
SSL termination
Cache · Global edge delivery
✕ WAF block at CloudFront edge: threat matched → 403 returned before forwarding to origin — Amplify never called
Phase 2 — CloudFront Routes Traffic
CloudFront dispatches to two separate destinations
Path A — SPA Delivery
④ Amplify Hosting
Origin: Angular SPA static files
↺ Cache miss: CloudFront fetches from Amplify & caches result ↺ Cache hit: CloudFront serves directly — Amplify not called
⑤ Angular SPA
Loaded into browser
Path B — API Calls
⑥ API Gateway WAF
2nd WAF layer — API-specific rules
✕ WAF block loopback: 403 returned to SPA — never reaches API Gateway
⑦ Regional API Gateway
Throttle · Route · Invoke authorizer
Phase 3 — Internal AWS Network (VPC)
Internal AWS Network (VPC)
Authorizer Round-Trip
⑦ API Gateway
Invoke authorizer
⑧ Lambda Authorizer
Verify JWT · check cognito:groups
⑦ API Gateway
Enforces decision
⑧ Lambda Authorizer
Returns Allow / Deny IAM policy
↺ Authorizer loopback: IAM policy returned to API Gateway · Deny = 403 immediately back to SPA
ML Processing & Response Loopbacks
⑨ Lambda Processor
ML Orchestration
⑩ SageMaker
Custom model inference
result
⑩ Bedrock API
Foundation model inference
result
↺ ML response loopback: result → Lambda Processor → API Gateway → WAF → Browser
⑪ Full response path: ML result → Lambda → API Gateway → WAF → Browser
① User Browser
Receives ML result — round-trip complete
Request / forward flow Response / loopback WAF block / short-circuit

Figure 2: Network Security Architecture — with loopbacks

Component Descriptions

The architecture consists of the following key components, each serving a specific purpose in the overall system.

ComponentDescriptionRole in Architecture
AWS Amplify HostingFully managed service for hosting web applicationsHosts the Angular SPA with built-in CI/CD, global CDN, and HTTPS
Angular SPASingle Page Application built with Angular frameworkProvides the user interface for accessing ML services
Amazon CloudFrontContent delivery network serviceDistributes application content globally and integrates with WAF
AWS WAFWeb Application FirewallProtects against common web exploits at both edge and API levels
Entra IDMicrosoft's cloud identity service (formerly Azure AD)Corporate identity provider for user authentication
Amazon CognitoUser authentication and authorization serviceIdentity federation with Entra ID and token issuance for AWS services
API GatewayManaged service for creating, publishing, and securing APIsEntry point for backend ML service access with WAF protection
Lambda AuthorizerCustom authorization logic for API GatewayValidates JWT tokens and assigns IAM policies for backend access
Lambda ProcessorsServerless compute for processing ML requestsCalls Bedrock Foundational Model APIs and processes responses
Amazon SageMakerFully managed ML serviceHosts custom internal ML models with endpoint access
AWS BedrockFully managed foundation model serviceProvides access to foundation models from Anthropic (Claude), Amazon (Titan, Nova), Meta (Llama), Mistral AI, Cohere, AI21 Labs, Stability AI, and Writer. Note: OpenAI models are not available on Amazon Bedrock.
IAM RolesIdentity and Access Management rolesControls permissions for accessing AWS services and resources

AWS Amplify CloudFront Architecture

AWS Amplify Hosting automatically provisions and configures a CloudFront distribution to serve your SPA, providing several integration benefits.

☁ CloudFront Integration Features
  • Global content delivery via edge locations
  • Automatic cache management for static assets
  • HTTPS enforcement with managed SSL certificates
  • Edge computing for optimized delivery
  • AWS WAF integration for edge security
  • Customizable cache behaviors and TTL settings
🔒 Security Enhancements
  • WAF Web ACL associations for threat protection
  • Geo-restriction capabilities
  • Field-level encryption for sensitive data
  • Amplify-managed origin access control (internal S3 bucket protected by CloudFront origin configuration — not user-configurable)
  • Custom HTTP security headers
  • DDoS protection via AWS Shield

CloudFront Configuration for SPA

The CloudFront distribution provisioned by Amplify Hosting is managed internally and is not directly user-configurable in the same way as a standalone CloudFront distribution. The following is a conceptual illustration of the key behaviours Amplify configures — it is not a deployable configuration block.

Amplify manages CloudFront internally The actual origin domain is an internal Amplify S3 bucket, not amplifyapp.com. The origin path format and access control are managed by Amplify and are not exposed as user-configurable CloudFront settings. SPA client-side routing (404→200 redirect) is handled automatically by Amplify — a separate CloudFront Function is not needed.
JSON — Conceptual illustration only (not a deployable config)
{
  "Origin": {
    "Domain": "[internal-amplify-managed-s3-bucket].s3.amazonaws.com",
    "ID": "amplify-hosted-app"
    // Note: actual origin domain is managed by Amplify — not user-configurable
  },
  "DefaultCacheBehavior": {
    "ViewerProtocolPolicy": "redirect-to-https",
    "AllowedMethods": ["GET", "HEAD", "OPTIONS"],
    "DefaultTTL": 86400,
    "MaxTTL": 31536000
    // Note: SPA routing (404→200) is handled automatically by Amplify
    // A separate CloudFront Function for SPA routing is not required
  },
  "CustomErrorResponses": [
    { "ErrorCode": 404, "ResponseCode": 200, "ResponsePagePath": "/index.html" }
  ],
  "WebACLId": "arn:aws:wafv2:us-east-1:[account]:global/webacl/AmplifyAppProtection/[id]",
  "Enabled": true,
  "PriceClass": "PriceClass_All"
}
  • SPA routing support via automatic 404→200 redirect to index.html — handled by Amplify, no CloudFront Function needed
  • HTTPS enforcement with redirect-to-https viewer protocol policy
  • Optimized caching strategy for static assets
  • WAF integration via Web ACL associated with the CloudFront distribution
  • Global distribution through PriceClass_All

API Gateway and WAF Architecture

While the frontend is protected by CloudFront + WAF, the backend ML services require an additional protection layer provided by a dedicated WAF-protected API Gateway with Lambda authorization.

Angular SPA
User's Browser
API Gateway Protection Layer
WAF Web ACL
Regional API Gateway
Lambda Authorizer
JWT Validation
ML Services

Figure 3: API Gateway and WAF Architecture

Regional API Gateway

For browser-based SPAs, a Regional endpoint type is required. The Regional API Gateway is configured to:

  • Accept HTTPS requests directly from the user's browser
  • Use resource policies to restrict access by IP or VPC as needed
  • Integrate with Lambda Authorizers for JWT token validation
  • Route requests to appropriate ML services via Lambda
  • Enable structured access logging and CloudWatch monitoring
  • Implement request throttling and burst limits to prevent abuse

WAF Configuration

The WAF Web ACL for the API Gateway includes:

  • Rate-based rules to prevent DDoS attacks
  • IP-based access control rules
  • SQL injection protection
  • Cross-site scripting (XSS) protection
  • Geo-matching rules to restrict access by location
  • Custom rules for specific security requirements
Why Regional, not Private? A Private API Gateway endpoint ("types": ["PRIVATE"]) is accessible only from within a VPC — not from a public browser. An Amplify-hosted Angular SPA runs in the user's browser outside any VPC and cannot call a Private API Gateway directly. A Regional endpoint type is the correct choice for this architecture. It can still be locked down using WAF rules, Cognito authorizers, and resource policies. If true network-level isolation is required, see the appendix for VPC-peered proxy and PrivateLink patterns.

API Gateway Endpoint Configuration

JSON — Regional endpoint (correct for browser SPA)
{
  "apiId": "abc123def456",
  "endpointConfiguration": {
    "types": ["REGIONAL"]
  },
  "policy": {
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:region:account-id:api-id/*"
    }]
  }
  // Access control is enforced by WAF Web ACL + Lambda Authorizer JWT validation
  // not by VPC endpoint restriction (which requires Private endpoint type)
}

Architecture Layers

The solution is organized into four distinct layers, promoting separation of concerns, better security, and easier maintenance.

⬡ Frontend Layer
  • AWS Amplify Hosting — Managed hosting with built-in CI/CD capabilities
  • Angular SPA — User interface for accessing ML services
  • Amplify Libraries — Client-side auth, API, and AWS service integrations
  • CloudFront Distribution — Automatically configured for content delivery and edge security
  • SSL/TLS Termination — HTTPS enforcement with managed certificates
Key Responsibilities
  • User interface presentation and interaction
  • Client-side authentication flow handling
  • Token management and secure storage
  • API requests and response rendering
🔑 Authentication Layer
  • Azure Entra ID — Enterprise identity provider, handles user authentication and directory
  • AWS Cognito User Pool — Identity federation with Entra ID and JWT token issuance
  • Identity Federation — OIDC-based integration between Entra ID and Cognito
  • IAM Roles — Authorization and access control for AWS resources
Key Responsibilities
  • User authentication through corporate credentials
  • Identity federation between Entra ID and AWS
  • JWT token issuance, management, and refresh
  • Role-based access control
🌐 API Layer
  • AWS WAF — Web Application Firewall at CloudFront and API Gateway levels
  • API Gateway — Managed API service configured for private access
  • Lambda Authorizers — Custom JWT validation and IAM permission assignment
  • API Routes — Endpoint definitions for different ML services
Key Responsibilities
  • Request routing to appropriate backend services
  • Token validation and authorization
  • Rate limiting, throttling, and threat protection
  • Request/response transformation and logging
🤖 ML Services Layer
  • SageMaker Endpoints — Managed hosting for custom internal ML models
  • Bedrock Foundation Models — Access to models from Anthropic, Amazon, Meta, Mistral, Cohere, and others
  • Lambda Processors — Serverless functions for Bedrock API requests and business logic
  • Model Monitoring — Performance and usage tracking for ML models
Key Responsibilities
  • ML model inference and processing
  • Request transformation for model compatibility
  • Response processing and formatting
  • Usage tracking, quota management, and error handling
Section 3

3Authentication and Authorization Flows

Authentication Flow Diagrams

User Authentication Flow

Phase 1 — Initial Access
① User
Opens browser → navigates to SPA
② Angular SPA
No active session detected
③ Amplify Auth Library
signInWithRedirect() — redirects browser to Cognito Hosted UI
Phase 2 — SAML Authentication Round-Trip
SAML Round-Trip — Cognito ↔ Entra ID ↔ User
④ AWS Cognito
Hosted UI · SAML redirect
⑤ Entra ID
Login page presented
① User
Enters credentials
→ forward leg
✕ Failed login loopback: invalid credentials → Entra ID shows error → user retries on same login page (no redirect back to Cognito yet)
④ AWS Cognito
Receives SAML assertion
⑥ Entra ID
Validates · issues SAML assertion
← return leg loopback (SAML assertion back to Cognito)
Phase 3 — Token Issuance & Callback Loopback
Cognito → SPA Callback Loopback
⑦ Cognito Token Issuance
Access token (1 hr) · ID token · Refresh token (30 days)
⑧ JWT → SPA
Browser redirected to /auth/callback
② Angular SPA
Back where it started
↺ Callback loopback: browser redirected back to the same SPA that initiated login · Amplify Auth Library intercepts /auth/callback · tokens stored in memory
Phase 4 — Active Session & Refresh Loopbacks
Session Management Loopbacks
⑨ Angular SPA — Session Active
Amplify manages token cache · user interacts with app
⑨ Angular SPA
Access token nearing expiry
④ Cognito
Silent token refresh request
⑨ Angular SPA
New access + ID tokens stored
④ Cognito
Issues new access + ID tokens
↺ Token refresh loopback: SPA silently exchanges refresh token with Cognito every ~1 hr · no user interaction · session stays active ↺ Full re-auth loopback: if refresh token expires (30 days) → entire flow restarts from Phase 1 · user must log in again
Request / forward flow Response / loopback Block / retry / re-auth

Figure 4: User Authentication Flow — with loopbacks

API Authorization Flow

Phase 1 — Request Dispatch
① Angular SPA
Authenticated user · attaches JWT to Authorization header
② API Request
Authorization: Bearer <access_token>
WAF Inspection
③ WAF Rules Check
SQLi · XSS · Rate limits · IP rules
④ Regional API Gateway
Receives request if WAF passes
✕ WAF block loopback: threat matched → 403 returned immediately to SPA — API Gateway never invoked
✕ Expired token loopback: API Gateway receives request with expired JWT → Lambda Authorizer returns Deny → 401 to SPA → SPA triggers silent token refresh with Cognito → retries request with new token
Phase 2 — Authorizer Round-Trip Loopback
API Gateway ↔ Lambda Authorizer
④ API Gateway
Invokes authorizer
⑤ Lambda Authorizer
Verify RS256 · validate client_id · check cognito:groups
→ forward invocation
④ API Gateway
Enforces policy decision
⑤ Lambda Authorizer
Returns Allow / Deny IAM policy
← return loopback (policy decision back to API Gateway)
✓ Allow: user in Administrators or MLUsers → API Gateway proceeds to Lambda Processor ✕ Deny loopback: user not in required group → API Gateway returns 403 directly back to SPA — no ML call made
Phase 3 — ML Processing & Response Loopback
ML Services Round-Trip
⑥ Lambda Processor
ML Orchestration
⑦ SageMaker Endpoint
Custom model inference
result
⑦ Bedrock Model
Foundation model inference
result
↺ ML response loopback: inference result → Lambda Processor → API Gateway → WAF → SPA → rendered to user
⑧ Full return path: ML result → Lambda → API Gateway → WAF → SPA → rendered for user
① Angular SPA
Displays ML result — round-trip complete
Request / forward flow Response / loopback Block / Deny / short-circuit

Figure 5: API Authorization Flow — with loopbacks

JWT Token Validation Process

The Lambda Authorizer performs a multi-step validation process on every incoming JWT access token before granting access to ML services.

  1. Extract Token — Remove the Bearer prefix from the Authorization header
  2. Decode Header — Decode the JWT header without verification to extract the kid (key ID)
  3. Fetch Public Key — Retrieve the matching RSA public key from Cognito's JWKS endpoint
  4. Verify Signature — Verify the token's RS256 signature using the public key
  5. Validate Claims — Check expiration (exp), issuer (iss). For Cognito access tokens, validate client_id (not aud) and confirm token_use === "access". Note: iat is informational only and not enforced by the authorizer
  6. Check Group Membership — Read group membership from the cognito:groups claim and verify required groups
  7. Generate IAM Policy — Return an Allow or Deny policy to API Gateway with enriched user context
Cognito Access Token Claims Cognito access tokens use client_id rather than the standard aud claim for audience identification. Group membership is stored under cognito:groups (not groups). Configuring JWT libraries with audience: CLIENT_ID will cause InvalidAudienceError on access tokens — disable audience verification in the library and validate client_id manually.

Sample JWT Access Token Payload

JSON — Cognito Access Token Payload
{
  "sub": "user123",
  "iss": "https://cognito-idp.region.amazonaws.com/us-east-1_example",
  "client_id": "clientidexample",
  "token_use": "access",
  "scope": "aws.cognito.signin.user.admin",
  "auth_time": 1684858239,
  "exp": 1684861839,
  "iat": 1684858239,
  "username": "[email protected]",
  "cognito:groups": ["ML_Users", "Data_Scientists"]
  // Note: scope for SAML-federated users reflects Cognito internal scopes,
  // not openid/profile/email — those appear in the ID token, not the access token
}

Lambda Authorizer Implementation

JavaScript — Lambda Authorizer
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const client = jwksClient({
  jwksUri: `https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`,
  cache: true,
  cacheMaxAge: 3600000  // 1 hour — safe given Cognito pre-publishes new keys before rotation
});

const getSigningKey = (kid) => new Promise((resolve, reject) => {
  client.getSigningKey(kid, (err, key) => {
    if (err) return reject(err);
    resolve(key.publicKey || key.rsaPublicKey);
  });
});

// Generate an IAM policy document for API Gateway
const generatePolicy = (principalId, effect, resource, context = {}) => ({
  principalId,
  policyDocument: {
    Version: '2012-10-17',
    Statement: [{ Action: 'execute-api:Invoke', Effect: effect, Resource: resource }]
  },
  context
});

// Check group membership AND optionally scope by method ARN
// methodArn format: arn:aws:execute-api:region:account:apiId/stage/METHOD/resource
const determineAccess = (groups, methodArn) => {
  const allowedGroups = ['Administrators', 'MLUsers'];
  const hasGroup = groups.some(g => allowedGroups.includes(g));
  if (!hasGroup) return false;

  // Optional: scope Administrators to all methods, MLUsers to POST /ml/* only
  const [, , , , , , method, ...resourceParts] = methodArn.split(/[:/]/);
  const isMLRoute = resourceParts.join('/').startsWith('ml');
  if (groups.includes('Administrators')) return true;
  if (groups.includes('MLUsers') && method === 'POST' && isMLRoute) return true;
  return false;
};

exports.handler = async (event) => {
  try {
    const token = event.authorizationToken.replace('Bearer ', '');
    const decoded = jwt.decode(token, { complete: true });
    if (!decoded) throw new Error('Invalid token format');

    const signingKey = await getSigningKey(decoded.header.kid);

    // Cognito ACCESS tokens use client_id, not aud — omit audience option
    const verifiedToken = jwt.verify(token, signingKey, {
      issuer: `https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}`,
      // audience omitted intentionally — Cognito access tokens use client_id
    });

    // Manually validate client_id (replaces standard audience check)
    if (verifiedToken.client_id !== process.env.CLIENT_ID) {
      throw new Error('Invalid client_id');
    }

    // Confirm this is an access token, not an ID token
    if (verifiedToken.token_use !== 'access') {
      throw new Error('Invalid token_use: expected access token');
    }

    // Group membership is in cognito:groups — NOT groups
    const userGroups = verifiedToken['cognito:groups'] || [];
    const allowed = determineAccess(userGroups, event.methodArn);

    return generatePolicy(
      verifiedToken.sub,
      allowed ? 'Allow' : 'Deny',
      event.methodArn,
      { username: verifiedToken.username || verifiedToken.sub, groups: userGroups.join(',') }
    );
  } catch (error) {
    console.error('Authorization error:', error);
    throw new Error('Unauthorized');
  }
};
Section 4

4Data Flow Architecture

End-to-End Data Flow

The end-to-end data flow describes how data travels through the system, from the initial user request to the final ML response. The following steps walk through the complete journey including all loopbacks and decision points.

  1. Initial RequestUser navigates to the SPA URL in a web browser. The request hits the internet-facing entry point.
  2. WAF Edge InspectionThe request arrives at the CloudFront edge location, where the associated WAF web ACL is evaluated. WAF is not a separate network tier — it is a web ACL associated with the CloudFront distribution and evaluated at the edge before the request is forwarded to the origin. Requests matching threat rules (SQLi, XSS, rate limits, geo-blocks) receive a 403 at the edge before being forwarded to Amplify Hosting. Clean requests pass through to CloudFront's cache and origin logic.
  3. Content DeliveryCloudFront delivers the Angular SPA static files from the nearest edge location. On a cache hit, Amplify Hosting is not called. On a cache miss, CloudFront fetches the files from Amplify Hosting as the origin, caches them, and serves them to the browser.
  4. Session Check & AuthenticationThe SPA loads in the browser and checks for an existing valid session via the Amplify Auth Library. If no session exists, signInWithRedirect() redirects the browser to the Cognito Hosted UI, which redirects again to Entra ID. The user enters corporate credentials. On success, Entra ID sends a SAML assertion back to Cognito, which issues three JWT tokens (Access, ID, Refresh) and redirects the browser back to the SPA via the /auth/callback URL. If a valid session already exists, this step is skipped entirely.
  5. Local InteractionThe authenticated user interacts with the Angular SPA interface to compose and submit an ML request. The SPA formats the request payload client-side.
  6. API Request PreparationThe SPA retrieves the current access token via fetchAuthSession(). If the access token has expired, Amplify silently exchanges the refresh token with Cognito to obtain a new one before proceeding. The JWT is attached to the Authorization: Bearer header of the outgoing API request.
  7. WAF API InspectionA second WAF layer dedicated to the API Gateway evaluates the request with API-specific rules. Blocked requests receive a 403 directly — the API Gateway is never invoked.
  8. Lambda Authorizer Round-TripAPI Gateway invokes the Lambda Authorizer, which verifies the RS256 signature, validates client_id and token_use, and checks cognito:groups membership. The Authorizer returns an Allow or Deny IAM policy back to API Gateway. On Deny, API Gateway returns 403 to the SPA immediately — no ML service is called.
  9. Lambda Processor InvocationOn Allow, API Gateway invokes the Lambda Processor. For Bedrock, Lambda is required — API Gateway has no native Bedrock integration. For SageMaker, API Gateway can technically invoke SageMaker endpoints directly via an AWS Service integration, but the Lambda Processor pattern is strongly recommended as it provides request/response transformation, error handling, timeout management, and a consistent interface for both ML service types. The Lambda Processor calls either the SageMaker real-time endpoint or the Bedrock Converse API depending on the request.
  10. ML Inference & Response LoopbackThe ML service performs inference and returns the result to the Lambda Processor. The Lambda Processor formats the response and returns it to API Gateway, which passes it back through WAF directly to the browser. Note: the Amplify CloudFront distribution serves only static SPA files — API responses travel directly from API Gateway to the browser.
  11. Result DeliveryThe Angular SPA receives the ML response and renders the result for the user, completing the full round-trip.

Data Transformation Points

Transformation PointTransformation TypePurpose
Client-side (Angular SPA)User input formattingPrepare data in format suitable for API requests
API GatewayRequest/response mappingTransform between frontend and backend data formats
Lambda Processor (pre-call)Model-specific request formattingTransform generic API request into Converse API format for Bedrock, or into SageMaker endpoint payload format. This transformation happens in Lambda before calling either ML service — API Gateway does not call ML services directly.
SageMakerModel inference onlyExecutes the custom model and returns raw model output as-is. No response transformation occurs inside SageMaker — that is handled by Lambda Processor after receiving the output.
Lambda Processor (post-call)Response normalisationProcesses and formats raw ML output (from SageMaker or Bedrock) into a structured, consistent JSON response before returning to API Gateway.
BedrockFoundation model responseStandardised response via Converse API across all model families — Anthropic, Amazon Nova, Meta Llama, Mistral, etc. Returned to Lambda Processor for normalisation.

SageMaker Integration Flow

For custom internal ML models, the Lambda Processor acts as the orchestration layer between API Gateway and the SageMaker real-time inference endpoint. The inference result returns through the same path back to the SPA.

Request Path →
① API Gateway
Authorised request — invokes Lambda
② Lambda Processor
Formats request payload for SageMaker endpoint
③ SageMaker Real-Time Endpoint
Executes custom model inference
← response loopback begins here
← Response Loopback Path
④ Raw Inference Result
Returned to Lambda Processor
⑤ Lambda Processor
Normalises & formats response as structured JSON
⑥ API Gateway → WAF → Browser
Response travels directly back to browser — not through Amplify CloudFront
Request / forward Response / loopback

Figure 6: SageMaker Integration Flow — with response loopback

API Gateway Integration Timeout — the binding constraint When Lambda sits behind API Gateway, the binding constraint is the API Gateway integration timeout of 29 seconds — not the Lambda maximum execution time. Even though Lambda can execute for up to 15 minutes in isolation, any Lambda invoked synchronously via API Gateway will be cut off at 29 seconds with a 504 error. For ML inference tasks likely to exceed 28 seconds, implement an asynchronous pattern: API Gateway returns a Job ID immediately, the Lambda Processor invokes the ML service asynchronously (via SQS or EventBridge), and the SPA polls a separate status endpoint for the result. The 15-minute Lambda limit only applies when Lambda is invoked outside of API Gateway (e.g. directly from SQS, S3, or EventBridge).

Bedrock Integration Flow

For foundation model access, the Lambda Processor invokes the Bedrock Converse API, which provides a unified interface across all supported text-based conversational model families. Note that the Converse API does not cover embedding models, image generation, or other non-conversational model types. The model response returns through Lambda back to API Gateway and then directly to the SPA.

Request Path →
① API Gateway
Authorised request — invokes Lambda
② Lambda Processor
Formats Converse API request — selects model ID
③ Bedrock Converse API
Unified interface — routes to selected model family
④ Anthropic Claude
Foundation model inference
result
④ Amazon Nova
Foundation model inference
result
④ Meta Llama 3
Foundation model inference
result
← Response Loopback Path
⑤ Bedrock Converse API
Returns standardised response to Lambda Processor
⑥ Lambda Processor
Extracts completion · formats structured JSON response
⑦ API Gateway → WAF → Browser
Response travels directly back to browser — not through Amplify CloudFront
Request / forward Response / loopback

Figure 7: Bedrock Integration Flow — with response loopback

Bedrock Lambda Processor

JavaScript — Bedrock Converse API
const { BedrockRuntimeClient, ConverseCommand } = require("@aws-sdk/client-bedrock-runtime");

const bedrockClient = new BedrockRuntimeClient({ region: process.env.AWS_REGION || "us-east-1" });

// Map Bedrock error codes to appropriate HTTP status codes
const mapBedrockError = (error) => {
  const errorMap = {
    ThrottlingException:        { statusCode: 429, message: 'Model throttled — retry after backoff' },
    AccessDeniedException:      { statusCode: 403, message: 'Model access denied — check Bedrock model access permissions' },
    ModelTimeoutException:      { statusCode: 504, message: 'Model inference timed out' },
    ValidationException:        { statusCode: 400, message: `Invalid request: ${error.message}` },
    ServiceUnavailableException:{ statusCode: 503, message: 'Bedrock service temporarily unavailable' },
    ModelNotReadyException:     { statusCode: 503, message: 'Model not ready — access may not be enabled' },
  };
  return errorMap[error.name] || { statusCode: 500, message: `Bedrock error: ${error.message}` };
};

exports.handler = async (event) => {
  try {
    const { prompt, model = "anthropic.claude-3-5-sonnet-20241022-v2:0", maxTokens = 1000, temperature = 0.7, systemPrompt } = JSON.parse(event.body);

    const params = {
      modelId: model,
      messages: [{ role: "user", content: [{ text: prompt }] }],
      inferenceConfig: { maxTokens, temperature, topP: 0.9 }
    };
    if (systemPrompt) params.system = [{ text: systemPrompt }];

    const response = await bedrockClient.send(new ConverseCommand(params));
    const completion = response.output?.message?.content?.[0]?.text || "";

    return {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        model,
        completion,
        stopReason: response.stopReason,
        usage: {
          input_tokens:  response.usage?.inputTokens  || 0,
          output_tokens: response.usage?.outputTokens || 0,
          total_tokens: (response.usage?.inputTokens  || 0) + (response.usage?.outputTokens || 0)
        }
      })
    };

  } catch (error) {
    console.error('Bedrock invocation error:', { name: error.name, message: error.message });
    const { statusCode, message } = mapBedrockError(error);
    return {
      statusCode,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ error: message, errorType: error.name })
    };
  }
};

Supported Bedrock Model Families

Converse API scope and model availability The Converse API supports text-based conversational models only — it does not cover embedding models, image generation, or other non-conversational model types. Model IDs and availability vary by AWS region. Always verify current model IDs and regional availability in the Bedrock documentation before deploying. Model IDs in this table were last verified July 2026.
Model FamilyConverse API (text)Recommended Model IDNotes
Anthropic Claude 3/3.5Supportedanthropic.claude-3-5-sonnet-20241022-v2:0Use Converse API. Legacy Human/Assistant format deprecated.
Amazon Nova / TitanSupportedamazon.nova-pro-v1:0Nova is current-gen; Titan Text is legacy but available.
Meta Llama 3Supportedmeta.llama3-70b-instruct-v1:0Open-weight; Llama 3 replaces Llama 2 on Bedrock.
Mistral AISupportedmistral.mistral-large-2402-v1:0Strong multilingual performance.
AI21 JambaSupportedai21.jamba-1-5-large-v1:0Replaces the Jurassic series.
Cohere CommandSupportedcohere.command-r-plus-v1:0Strong at RAG and tool use. Cohere Embed models are not accessible via Converse API.
Model Access Must Be Explicitly Enabled Foundation model access is disabled by default. Go to Amazon Bedrock → Model Access → Manage model access to enable each model you intend to use. Availability varies by region.
Section 5

5Security Architecture

Multi-Layer Security Model

The architecture implements defense-in-depth with security controls at every layer. No single point of failure can expose the ML services backend.

LayerControlsThreats Mitigated
Edge (CloudFront + WAF)WAF Web ACL, geo-restrictions, rate limiting, DDoS protectionBots, DDoS, SQL injection, XSS, geo-based attacks
Identity (Cognito + Entra ID)OIDC federation, MFA, short-lived JWT tokens, session managementCredential theft, unauthorized access, account takeover
API (API Gateway + Lambda Authorizer)JWT validation, IAM policy generation, request throttling, input validationToken replay, privilege escalation, API abuse
Compute (Lambda)Least-privilege IAM roles, VPC isolation, encrypted environment variablesLateral movement, data exfiltration, over-privileged access
ML Services (SageMaker + Bedrock)VPC endpoints, IAM resource policies, CloudWatch monitoringUnauthorized model access, data leakage, cost abuse
🛡 WAF Rule Sets
  • AWS Managed Rules — Core Rule Set (CRS)
  • AWS Managed Rules — Known Bad Inputs
  • Rate-based rules (per IP threshold)
  • Geo-match rules for regional restrictions
  • Custom rules for application-specific patterns
  • IP set allow/deny lists
🔐 IAM Least Privilege
  • Lambda Authorizer: read-only Cognito JWKS access
  • Lambda Processors: specific SageMaker and Bedrock ARN permissions only
  • No wildcard * resource permissions in production
  • Separate execution roles per Lambda function
  • Regular IAM access reviews via Access Analyzer

JWT Token Security Implementation

Token Validation in Python (Lambda Authorizer)

Python — JWT Validation
import jwt, requests, os
from functools import lru_cache

REGION     = os.environ['COGNITO_REGION']
POOL_ID    = os.environ['USER_POOL_ID']
CLIENT_ID  = os.environ['CLIENT_ID']
JWKS_URL   = f'https://cognito-idp.{REGION}.amazonaws.com/{POOL_ID}/.well-known/jwks.json'

@lru_cache(maxsize=1)
def get_jwks():
    return requests.get(JWKS_URL, timeout=10).json()

def get_public_key(kid):
    for key in get_jwks()['keys']:
        if key['kid'] == kid:
            return jwt.algorithms.RSAAlgorithm.from_jwk(key)
    raise ValueError(f'Key not found: {kid}')

def validate_token(token):
    header = jwt.get_unverified_header(token)
    public_key = get_public_key(header['kid'])

    # Cognito access tokens use client_id not aud — disable audience verification
    claims = jwt.decode(
        token, public_key, algorithms=['RS256'],
        options={'verify_aud': False},
        issuer=f'https://cognito-idp.{REGION}.amazonaws.com/{POOL_ID}'
    )
    if claims.get('client_id') != CLIENT_ID:
        raise ValueError('Invalid client_id')
    if claims.get('token_use') != 'access':
        raise ValueError('Invalid token_use')
    return claims

def lambda_handler(event, context):
    token = event.get('authorizationToken', '').replace('Bearer ', '')
    claims = validate_token(token)

    username = claims.get('username', claims.get('sub'))
    # Group membership is in cognito:groups (not groups)
    groups   = claims.get('cognito:groups', [])

    if not any(g in ['Administrators', 'MLUsers'] for g in groups):
        return generate_policy(username, 'Deny', event['methodArn'])

    return generate_policy(username, 'Allow', event['methodArn'], {
        'username': username,
        'email':    claims.get('email', ''),
        'groups':   ','.join(groups),
        'sub':      claims.get('sub', '')
    })

Data Protection and Compliance

🔒 Encryption in Transit
  • TLS 1.2 minimum for all HTTPS connections (CloudFront Security Policy: TLSv1.2_2021)
  • TLS 1.3 negotiated where clients support it
  • Certificate management via AWS Certificate Manager
  • API Gateway SSL termination
  • VPC endpoint encryption for internal traffic
🗄 Encryption at Rest
  • S3 buckets encrypted with AES-256 or SSE-KMS
  • CloudWatch Logs encrypted with KMS CMK
  • Lambda environment variables encrypted at rest
  • AWS KMS with customer-managed keys
  • Automated key rotation policies
🚫
Production API Gateway — Data Tracing Never enable dataTraceEnabled: true on API Gateway stages in production. This setting logs the full request and response payloads (including ML inputs/outputs) to CloudWatch, creating a significant security and compliance risk. Use structured access logging with accessLogDestination instead.

CORS Configuration

In production, always specify explicit allowed origins rather than using the wildcard *. The origins should correspond to your actual Amplify hosting domain and any custom domains in use.

TypeScript — CDK API Gateway CORS
defaultCorsPreflightOptions: {
  // Explicitly list allowed origins — never use Cors.ALL_ORIGINS in production
  allowOrigins: [
    'https://your-app.amplifyapp.com',  // Amplify hosted domain
    'https://your-custom-domain.com'    // Custom domain (if applicable)
  ],
  allowMethods: ['GET', 'POST', 'OPTIONS'],
  allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'],
  allowCredentials: true
}
Section 6

6Implementation Guide

Setting Up AWS Amplify

AWS Amplify Hosting can be connected to your Git repository (GitHub, GitLab, Bitbucket, or AWS CodeCommit) for automated CI/CD deployments on every push to your configured branch.

Amplify CLI Initialization

Bash
# Install and configure Amplify CLI
npm install -g @aws-amplify/cli
amplify configure

# Initialize Amplify in your Angular project
cd your-angular-project
amplify init

# Add hosting (choose Amplify Console for CI/CD)
amplify add hosting
amplify publish

Amplify Build Specification

The amplify.yml build spec is for frontend only. Infrastructure (CDK/CloudFormation) deployments must run in a separate CI/CD pipeline — not inside Amplify Hosting, which lacks the broad IAM permissions CDK requires.

YAML — amplify.yml
version: 1
frontend:
  phases:
    preBuild:
      commands:
        - cd frontend
        - npm ci
    build:
      commands:
        - npm run build:prod
  artifacts:
    baseDirectory: frontend/dist/ml-app
    files:
      - '**/*'
  cache:
    paths:
      - frontend/node_modules/**/*

Configuring Cognito User Pools

TypeScript — CDK Cognito Stack
import * as cognito from 'aws-cdk-lib/aws-cognito';

this.userPool = new cognito.UserPool(this, 'MLAppUserPool', {
  selfSignUpEnabled: false,
  signInAliases: { email: true },
  passwordPolicy: {
    minLength: 12,
    requireLowercase: true, requireUppercase: true,
    requireDigits: true,    requireSymbols: true
  },
  mfa: cognito.Mfa.REQUIRED,
  mfaSecondFactor: { sms: true, otp: true },
  accountRecovery: cognito.AccountRecovery.EMAIL_ONLY
});

this.userPoolClient = new cognito.UserPoolClient(this, 'MLAppClient', {
  userPool: this.userPool,
  generateSecret: false,
  authFlows: { userSrp: true },
  oAuth: {
    flows: { authorizationCodeGrant: true },
    scopes: [cognito.OAuthScope.OPENID, cognito.OAuthScope.EMAIL, cognito.OAuthScope.PROFILE],
    callbackUrls:  ['https://your-app.amplifyapp.com/auth/callback'],
    logoutUrls:    ['https://your-app.amplifyapp.com/auth/logout']
  },
  accessTokenValidity: cdk.Duration.hours(1),
  idTokenValidity:     cdk.Duration.hours(1),
  refreshTokenValidity: cdk.Duration.days(30)
});

Integrating with Entra ID

TypeScript — CDK SAML Identity Provider
const samlProvider = new cognito.CfnUserPoolIdentityProvider(this, 'EntraIDProvider', {
  userPoolId: this.userPool.userPoolId,
  providerName: 'EntraID',
  providerType: 'SAML',
  providerDetails: {
    MetadataURL: 'https://login.microsoftonline.com/[tenant-id]/federationmetadata/2007-06/federationmetadata.xml',
    IDPSignout: 'true'
  },
  attributeMapping: {
    email:              'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
    given_name:         'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
    family_name:        'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
    'custom:department': 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department'
  }
});

Angular SPA Development

Environment Configuration

TypeScript — environment.ts
export const environment = {
  production: false,
  cognito: {
    userPoolId:         'us-east-1_XXXXXXXXX',
    userPoolWebClientId: 'XXXXXXXXXXXXXXXXXXXXXXXXXX',
    // domainPrefix is the Cognito hosted UI prefix — NOT the userPoolId
    domainPrefix: 'my-company-ml-app-dev',
    region:       'us-east-1',
    identityPoolId: 'us-east-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
  },
  api: {
    baseUrl: 'https://api.example.com/dev',
    region:  'us-east-1'
  }
};

Implementing Authentication

Use AWS Amplify v6 modular imports for authentication. The legacy import { Auth } from 'aws-amplify' pattern from Amplify v5 is no longer supported in v6 — use named function imports from aws-amplify/auth instead.

TypeScript — Amplify v6 Configuration (app.module.ts)
import { Amplify } from 'aws-amplify';
import { environment } from '../environments/environment';

// Amplify v6 configuration structure
Amplify.configure({
  Auth: {
    Cognito: {
      userPoolId:       environment.cognito.userPoolId,
      userPoolClientId: environment.cognito.userPoolWebClientId,
      identityPoolId:   environment.cognito.identityPoolId,
      loginWith: {
        oauth: {
          // Use domainPrefix — NOT userPoolId — for the OAuth domain
          domain: `${environment.cognito.domainPrefix}.auth.${environment.cognito.region}.amazoncognito.com`,
          scopes: ['email', 'openid', 'profile'],
          redirectSignIn:  [window.location.origin + '/auth/callback'],
          redirectSignOut: [window.location.origin + '/auth/logout'],
          responseType: 'code'
        }
      }
    }
  }
});
TypeScript — Auth Service (Amplify v6)
// Amplify v6: modular imports from 'aws-amplify/auth'
import { signIn, signOut, getCurrentUser, fetchAuthSession, signInWithRedirect } from 'aws-amplify/auth';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private currentUserSubject = new BehaviorSubject<User | null>(null);

  async initializeAuth(): Promise<void> {
    try {
      const { username, userId } = await getCurrentUser();
      const session = await fetchAuthSession();
      const payload = session.tokens?.idToken?.payload;
      this.currentUserSubject.next({
        username, userId,
        email:  payload?.['email'] as string || '',
        groups: (payload?.['cognito:groups'] as string[]) || []
      });
    } catch { /* no active session */ }
  }

  async signInWithSAML(): Promise<void> {
    await signInWithRedirect({ provider: { custom: 'EntraID' } });
  }

  async getAccessToken(): Promise<string> {
    const session = await fetchAuthSession();
    return session.tokens?.accessToken?.toString() ?? '';
  }

  async signOut(): Promise<void> {
    await signOut();
    this.currentUserSubject.next(null);
  }
}

API Gateway Configuration

TypeScript — CDK API Gateway Stack
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as logs from 'aws-cdk-lib/aws-logs';

this.api = new apigateway.RestApi(this, 'MLAppAPI', {
  restApiName: 'ML App API',
  endpointConfiguration: { types: [apigateway.EndpointType.REGIONAL] },
  defaultCorsPreflightOptions: {
    allowOrigins: ['https://your-app.amplifyapp.com'],
    allowMethods: ['GET', 'POST', 'OPTIONS'],
    allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date'],
    allowCredentials: true
  },
  deployOptions: {
    stageName: 'prod',
    throttlingRateLimit:  1000,
    throttlingBurstLimit: 2000,
    loggingLevel: apigateway.MethodLoggingLevel.ERROR,
    dataTraceEnabled: false,  // Never enable in production
    metricsEnabled: true,
    accessLogDestination: new apigateway.LogGroupLogDestination(
      new logs.LogGroup(this, 'ApiAccessLogs', { retention: logs.RetentionDays.THIRTY_DAYS })
    ),
    accessLogFormat: apigateway.AccessLogFormat.jsonWithStandardFields()
  }
});

Lambda Authorizer Implementation

Lambda Memory Configuration Lambda supports memory from 128 MB up to 10,240 MB (10 GB). For ML workloads, 1,024–4,096 MB is typical. Memory allocation also scales CPU proportionally.

See the complete Python Lambda Authorizer implementation in Section 5 — JWT Token Security Implementation.

ML Services Integration

See the complete Bedrock Converse API implementation in Section 4 — Bedrock Integration Flow. For SageMaker integration, invoke real-time endpoints via the AWS SDK using the endpoint name from your deployed SageMaker model.

Python — SageMaker Endpoint Invocation
import boto3, json

sagemaker_runtime = boto3.client('sagemaker-runtime', region_name=os.environ['AWS_REGION'])

def invoke_sagemaker_endpoint(endpoint_name, payload):
    response = sagemaker_runtime.invoke_endpoint(
        EndpointName=endpoint_name,
        ContentType='application/json',
        Accept='application/json',
        Body=json.dumps(payload)
    )
    result = json.loads(response['Body'].read().decode())
    return result
Back Knowledge Base stephen-ade.github.io Part 2 AWS Amplify ML Services Integration Guide CDK stacks · Lambda code · CI/CD · Deployment