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
Architecture Benefits
- 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
- Global CloudFront edge delivery
- Static asset caching
- Optimized API routing
- Scalable Amplify hosting
- Async ML service consumption
- Serverless Lambda scalability
- Built-in CI/CD with Amplify
- Simplified UAT/PROD deployments
- Centralized monitoring & logging
- Automated demand-based scaling
- Managed service overhead reduction
- Democratized ML/AI access
- Corporate identity UX consistency
- Serverless cost optimization
- Accelerated AI feature delivery
- Enterprise-grade compliance
2Architecture Overview
High-Level Architecture Diagrams
Overall System Architecture
Web browser — requests SPA
Edge protection
Cache · SSL · Global edge
Origin: static SPA files
signInWithRedirect()
Hosted UI · SAML redirect
Validate credentials
JWT stored & session active
Issues Access + ID + Refresh JWT
SAML assertion → Cognito
Inspect & filter
Invoke authorizer
Verify JWT · check groups
Enforce decision
Returns IAM policy
ML Orchestration
Custom model inference
Foundation model inference
Receives ML result in browser
Figure 1: Overall System Architecture — with loopbacks
Network Security Architecture
User's browser — HTTPS request
Edge Protection
SQLi · XSS · Rate limits · Geo-block
SSL termination
Cache · Global edge delivery
Origin: Angular SPA static files
Loaded into browser
2nd WAF layer — API-specific rules
Throttle · Route · Invoke authorizer
Invoke authorizer
Verify JWT · check cognito:groups
Enforces decision
Returns Allow / Deny IAM policy
ML Orchestration
Custom model inference
Foundation model inference
Receives ML result — round-trip complete
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.
| Component | Description | Role in Architecture |
|---|---|---|
| AWS Amplify Hosting | Fully managed service for hosting web applications | Hosts the Angular SPA with built-in CI/CD, global CDN, and HTTPS |
| Angular SPA | Single Page Application built with Angular framework | Provides the user interface for accessing ML services |
| Amazon CloudFront | Content delivery network service | Distributes application content globally and integrates with WAF |
| AWS WAF | Web Application Firewall | Protects against common web exploits at both edge and API levels |
| Entra ID | Microsoft's cloud identity service (formerly Azure AD) | Corporate identity provider for user authentication |
| Amazon Cognito | User authentication and authorization service | Identity federation with Entra ID and token issuance for AWS services |
| API Gateway | Managed service for creating, publishing, and securing APIs | Entry point for backend ML service access with WAF protection |
| Lambda Authorizer | Custom authorization logic for API Gateway | Validates JWT tokens and assigns IAM policies for backend access |
| Lambda Processors | Serverless compute for processing ML requests | Calls Bedrock Foundational Model APIs and processes responses |
| Amazon SageMaker | Fully managed ML service | Hosts custom internal ML models with endpoint access |
| AWS Bedrock | Fully managed foundation model service | Provides 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 Roles | Identity and Access Management roles | Controls 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.
- 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
- 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.
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.
{
"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-httpsviewer 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.
User's Browser
JWT Validation
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
"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
{
"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.
- 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
- 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
- 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
- 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
3Authentication and Authorization Flows
Authentication Flow Diagrams
User Authentication Flow
Opens browser → navigates to SPA
No active session detected
signInWithRedirect() — redirects browser to Cognito Hosted UI
Hosted UI · SAML redirect
Login page presented
Enters credentials
Receives SAML assertion
Validates · issues SAML assertion
Access token (1 hr) · ID token · Refresh token (30 days)
Browser redirected to /auth/callback
Back where it started
Amplify manages token cache · user interacts with app
Access token nearing expiry
Silent token refresh request
New access + ID tokens stored
Issues new access + ID tokens
Figure 4: User Authentication Flow — with loopbacks
API Authorization Flow
Authenticated user · attaches JWT to Authorization header
Authorization: Bearer <access_token>
SQLi · XSS · Rate limits · IP rules
Receives request if WAF passes
Invokes authorizer
Verify RS256 · validate client_id · check cognito:groups
Enforces policy decision
Returns Allow / Deny IAM policy
ML Orchestration
Custom model inference
Foundation model inference
Displays ML result — round-trip complete
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.
- Extract Token — Remove the
Bearerprefix from the Authorization header - Decode Header — Decode the JWT header without verification to extract the
kid(key ID) - Fetch Public Key — Retrieve the matching RSA public key from Cognito's JWKS endpoint
- Verify Signature — Verify the token's RS256 signature using the public key
- Validate Claims — Check expiration (
exp), issuer (iss). For Cognito access tokens, validateclient_id(notaud) and confirmtoken_use === "access". Note:iatis informational only and not enforced by the authorizer - Check Group Membership — Read group membership from the
cognito:groupsclaim and verify required groups - Generate IAM Policy — Return an Allow or Deny policy to API Gateway with enriched user context
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
{
"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
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');
}
};
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.
- Initial RequestUser navigates to the SPA URL in a web browser. The request hits the internet-facing entry point.
- 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.
- 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.
- 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/callbackURL. If a valid session already exists, this step is skipped entirely. - 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.
- 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 theAuthorization: Bearerheader of the outgoing API request. - 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.
- Lambda Authorizer Round-TripAPI Gateway invokes the Lambda Authorizer, which verifies the RS256 signature, validates
client_idandtoken_use, and checkscognito:groupsmembership. 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. - 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.
- 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.
- Result DeliveryThe Angular SPA receives the ML response and renders the result for the user, completing the full round-trip.
Data Transformation Points
| Transformation Point | Transformation Type | Purpose |
|---|---|---|
| Client-side (Angular SPA) | User input formatting | Prepare data in format suitable for API requests |
| API Gateway | Request/response mapping | Transform between frontend and backend data formats |
| Lambda Processor (pre-call) | Model-specific request formatting | Transform 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. |
| SageMaker | Model inference only | Executes 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 normalisation | Processes and formats raw ML output (from SageMaker or Bedrock) into a structured, consistent JSON response before returning to API Gateway. |
| Bedrock | Foundation model response | Standardised 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.
Authorised request — invokes Lambda
Formats request payload for SageMaker endpoint
Executes custom model inference
Returned to Lambda Processor
Normalises & formats response as structured JSON
Response travels directly back to browser — not through Amplify CloudFront
Figure 6: SageMaker Integration Flow — with response loopback
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.
Authorised request — invokes Lambda
Formats Converse API request — selects model ID
Unified interface — routes to selected model family
Foundation model inference
Foundation model inference
Foundation model inference
Returns standardised response to Lambda Processor
Extracts completion · formats structured JSON response
Response travels directly back to browser — not through Amplify CloudFront
Figure 7: Bedrock Integration Flow — with response loopback
Bedrock Lambda Processor
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
| Model Family | Converse API (text) | Recommended Model ID | Notes |
|---|---|---|---|
| Anthropic Claude 3/3.5 | Supported | anthropic.claude-3-5-sonnet-20241022-v2:0 | Use Converse API. Legacy Human/Assistant format deprecated. |
| Amazon Nova / Titan | Supported | amazon.nova-pro-v1:0 | Nova is current-gen; Titan Text is legacy but available. |
| Meta Llama 3 | Supported | meta.llama3-70b-instruct-v1:0 | Open-weight; Llama 3 replaces Llama 2 on Bedrock. |
| Mistral AI | Supported | mistral.mistral-large-2402-v1:0 | Strong multilingual performance. |
| AI21 Jamba | Supported | ai21.jamba-1-5-large-v1:0 | Replaces the Jurassic series. |
| Cohere Command | Supported | cohere.command-r-plus-v1:0 | Strong at RAG and tool use. Cohere Embed models are not accessible via Converse API. |
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.
| Layer | Controls | Threats Mitigated |
|---|---|---|
| Edge (CloudFront + WAF) | WAF Web ACL, geo-restrictions, rate limiting, DDoS protection | Bots, DDoS, SQL injection, XSS, geo-based attacks |
| Identity (Cognito + Entra ID) | OIDC federation, MFA, short-lived JWT tokens, session management | Credential theft, unauthorized access, account takeover |
| API (API Gateway + Lambda Authorizer) | JWT validation, IAM policy generation, request throttling, input validation | Token replay, privilege escalation, API abuse |
| Compute (Lambda) | Least-privilege IAM roles, VPC isolation, encrypted environment variables | Lateral movement, data exfiltration, over-privileged access |
| ML Services (SageMaker + Bedrock) | VPC endpoints, IAM resource policies, CloudWatch monitoring | Unauthorized model access, data leakage, cost abuse |
- 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
- 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)
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
- 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
- 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
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.
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
}
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
# 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.
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
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
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
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.
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'
}
}
}
}
});
// 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
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
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.
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