A Proof of Concept for a multi-step SPA workflow that collects PII (personal profile) and banking information. The core security principle: PII never lives in the browser state — the browser is a display/input layer only.
The Angular SPA uses @auth0/auth0-angular with PKCE and opaque access tokens (no JWTs in the browser). The C# BFF validates tokens by calling Auth0's /userinfo endpoint and encrypts all sensitive data with AES-256-GCM before storing in Redis.
Browser (Angular SPA) ASP.NET Core BFF Redis
| | |
|-- Auth0 SDK (PKCE + opaque token) ---->| |
| Authorization: Bearer <opaque> | |
| |-- GET /userinfo (Auth0) |
| | → validates token, gets sub |
| |-- AES-256-GCM encrypt -------->|
| | workflow:{userId} EX 86400 |
| | |
| |<-- encrypted blob -------------|
| |-- AES-256-GCM decrypt |
|<-- decrypted PII (display only) -------| |
sequenceDiagram
participant B as Browser (Angular)
participant A as Auth0
participant BFF as ASP.NET Core BFF
B->>A: loginWithRedirect() (PKCE + code_challenge)
A->>B: Auth0 Universal Login page
B->>A: User enters credentials
A->>B: 302 Redirect to SPA (code=xxx)
B->>A: POST /oauth/token (code + code_verifier)
A->>B: Opaque access token (not JWT)
Note over B: Token stored in localStorage (Auth0 SDK)
B->>BFF: GET /api/workflow/resume (Bearer <opaque-token>)
BFF->>A: GET /userinfo (Bearer <opaque-token>)
A->>BFF: {sub, name, email}
BFF->>B: Authenticated response
sequenceDiagram
participant B as Browser (Angular)
participant BFF as ASP.NET Core BFF
participant A as Auth0
participant R as Redis
B->>BFF: POST /api/workflow/save {PII + banking}
Note over B: Bearer token attached by auth interceptor
Note over BFF: X-CSRF header verified
BFF->>A: GET /userinfo (validate opaque token)
A->>BFF: {sub: "auth0|abc123"}
BFF->>BFF: AES-256-GCM encrypt(payload)
BFF->>R: SET workflow:{userId} <encrypted> EX 86400
R->>BFF: OK
BFF->>B: {success: true}
Note over B: PII cleared from component signals
sequenceDiagram
participant B as Browser (Angular)
participant BFF as ASP.NET Core BFF
participant A as Auth0
participant R as Redis
B->>BFF: GET /api/workflow/resume
Note over BFF: Bearer token validated
BFF->>A: GET /userinfo (validate opaque token)
A->>BFF: {sub: "auth0|abc123"}
BFF->>R: GET workflow:{userId}
R->>BFF: <encrypted blob>
BFF->>BFF: AES-256-GCM decrypt(blob)
BFF->>B: {exists: true, data: {PII + banking}}
Note over B: Display in form, never store in NgRx
graph LR
subgraph "Option 1: SPA + JWT"
A1[Angular + Auth0 SPA SDK] --> A2[JWTs in localStorage]
A2 --> A3[Direct API calls]
style A2 fill:#ff6b6b
end
subgraph "Option 2: BFF + Opaque Cookie"
B1[Angular] --> B2[BFF holds all tokens]
B2 --> B3[Opaque HttpOnly cookie]
style B2 fill:#ffd93d
end
subgraph "Option 3: SPA + Opaque Token ✓"
C1[Angular + Auth0 SDK + PKCE] --> C2[Opaque token — not readable]
C2 --> C3[BFF validates via /userinfo]
style C2 fill:#6bff6b
end
| Technology | Role |
|---|---|
| Angular 20 | SPA frontend — signals, OnPush, standalone components, PrimeNG |
| ASP.NET Core 10 | BFF — validates opaque tokens via Auth0 /userinfo, encryption |
| Redis 7 | Temporary encrypted PII storage — in-memory only, 24h TTL |
| Auth0 | Identity provider — OIDC, Universal Login, PKCE |
| AES-256-GCM | Authenticated encryption at rest in Redis |
| NgRx Signal Store | UI state only (step tracking) — PII never enters the store |
/
├── docker-compose.yml # Full stack: Redis + BFF + SPA
├── redis/
│ └── redis.conf # No persistence, password auth, hardened
├── docs/
│ ├── poc-presentation.pptx # 15-slide presentation (pre-generated)
│ ├── generate_presentation.py # Python script to regenerate the PPTX
│ └── diagrams/ # Mermaid sequence diagram sources
│ ├── auth-login.md
│ ├── workflow-save.md
│ ├── workflow-resume.md
│ └── tradeoffs.md
├── src/
│ ├── backend/ # ASP.NET Core BFF
│ │ ├── Dockerfile # Multi-stage .NET build
│ │ ├── Program.cs # Auth middleware, CORS, Redis DI, pipeline
│ │ ├── Controllers/
│ │ │ └── WorkflowController.cs # POST save, GET resume, DELETE clear
│ │ ├── Middleware/
│ │ │ └── CsrfHeaderMiddleware.cs # X-CSRF header validation on mutations
│ │ ├── Services/
│ │ │ ├── IEncryptionService.cs
│ │ │ ├── AesGcmEncryptionService.cs # AES-256-GCM encrypt/decrypt
│ │ │ ├── IWorkflowService.cs
│ │ │ └── RedisWorkflowService.cs # Redis get/set with encryption
│ │ └── Models/
│ │ └── WorkflowPayload.cs # PersonalInfo, BankingInfo DTOs
│ └── frontend/ # Angular SPA
│ ├── Dockerfile # Multi-stage Node + Nginx build
│ ├── nginx.conf # Reverse proxy /api/* → BFF
│ ├── proxy.conf.json # Dev proxy /api/* → BFF
│ └── src/app/
│ ├── app.config.ts # HttpClient, interceptors, PrimeNG Aura theme
│ ├── app.routes.ts # Lazy-loaded routes with auth guard
│ ├── app.ts # Root component — checks auth on init
│ ├── auth/
│ │ ├── auth.service.ts # Wraps @auth0/auth0-angular, signal-based
│ │ ├── auth.guard.ts # Functional CanActivateFn
│ │ ├── csrf.interceptor.ts # Adds X-CSRF header on POST/PUT/DELETE
│ │ ├── auth.interceptor.ts # Attaches Bearer token via Auth0 SDK
│ │ └── login.component.ts # Login page
│ └── workflow/
│ ├── models/workflow.models.ts # TypeScript interfaces
│ ├── services/workflow-api.service.ts # HTTP calls to BFF
│ ├── store/workflow.store.ts # NgRx Signal Store (non-PII only)
│ ├── workflow-shell.component.ts # Stepper orchestrator, holds PII in signals
│ └── components/
│ ├── personal-info-step.component.ts # Step 1 — reactive form
│ ├── banking-info-step.component.ts # Step 2 — reactive form
│ └── review-step.component.ts # Step 3 — read-only review
For local development without Docker you also need:
- .NET 10 SDK
- Node.js 20+ and npm
- Angular CLI (
npm install -g @angular/cli)
- Go to auth0.com and sign up / log in
- Navigate to Applications → Create Application
- Choose Single Page Application (the Angular SDK uses PKCE with opaque tokens)
- Go to the Settings tab of your new application
- Copy the Domain and Client ID — you'll need them below
- Scroll down and set the following:
| Setting | Value |
|---|---|
| Allowed Callback URLs | http://localhost:4200 |
| Allowed Logout URLs | http://localhost:4200 |
| Allowed Web Origins | http://localhost:4200 |
- Click Save Changes
Copy your Auth0 credentials into a .env file at the project root:
# .env (create this file in the project root)
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_CLIENT_ID=your-client-id
ENCRYPTION_KEY=your-base64-keyTo generate an encryption key:
openssl rand -base64 32Copy the output into the ENCRYPTION_KEY field in .env.
You also need to set the Auth0 credentials in the Angular environment file at src/frontend/src/environments/environment.ts (domain and clientId).
Important: Replace
your-tenant.auth0.comandyour-client-idwith the real values from Auth0. The app will fail to start if these are left as placeholders.
docker compose up --buildThat's it. Docker Compose reads the .env file automatically and starts all three services:
| Service | URL | Container |
|---|---|---|
| Angular SPA | http://localhost:4200 |
poc-spa |
| BFF API | http://localhost:5100 |
poc-bff |
| Redis | localhost:6380 |
poc-redis |
Nginx inside the SPA container proxies /api/* and /callback to the BFF automatically.
- Open
http://localhost:4200— you'll be redirected to the login page - Click Sign In with Auth0 — redirects to Auth0 Universal Login
- After authentication, you're redirected back to the multi-step workflow
- Fill in Step 1 (Personal Info) → Step 2 (Banking Info) → Step 3 (Review & Confirm)
- Each step saves encrypted data to Redis via the BFF
- Refresh the page at any point — your progress is restored from Redis
- On final confirmation, the Redis key is deleted
docker compose downRequires .NET 10 SDK, Node.js 20+, and Angular CLI installed locally.
1. Start Redis only
docker compose up redis -d
docker exec poc-redis redis-cli -a P0cR3d!s2024 PING
# → PONG2. Configure and run the BFF
cd src/backend
dotnet user-secrets init
dotnet user-secrets set "Auth0:Domain" "your-actual-tenant.auth0.com"
dotnet user-secrets set "Encryption:Key" "$(openssl rand -base64 32)"
dotnet runThe BFF starts at https://localhost:5001.
3. Run the Angular SPA
cd src/frontend
npm install
ng serveThe SPA starts at http://localhost:4200. The dev proxy forwards /api/* and /callback to the BFF.
| Layer | Protection |
|---|---|
| Auth | Auth0 SPA SDK with PKCE — opaque access tokens (not JWTs) |
| CSRF | X-CSRF header required on mutations |
| Token validation | BFF validates opaque tokens via Auth0 /userinfo — extracts sub for user identity |
| Redis data | AES-256-GCM encrypted — random nonce per write, 16-byte auth tag |
| Redis config | No disk persistence (RDB/AOF disabled), password auth, dangerous commands renamed |
| TTL | 24h auto-expiry — data exists only as long as needed |
| NgRx store | Tracks step/loading state only — PII lives in component-scoped signal() |
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/api/workflow/save |
POST | Yes | Encrypts and stores workflow payload in Redis |
/api/workflow/resume |
GET | Yes | Returns decrypted workflow data from Redis |
/api/workflow/clear |
DELETE | Yes | Deletes workflow data from Redis |
pip install python-pptx
python3 docs/generate_presentation.pyOutput: docs/poc-presentation.pptx (15 slides).
Mermaid diagram source files are also available separately in docs/diagrams/:
See the full Testing & Verification Guide for step-by-step checks (Redis, auth, CSRF, encryption, workflow, CORS).
- TLS on Redis connection (stunnel or managed Redis with TLS)
- Encryption key rotation strategy (versioned keys)
- Audit logging (who accessed what data, when)
- Rate limiting on API endpoints
- Redis Sentinel / Cluster for high availability
- WAF and DDoS protection
- Penetration testing
- Data retention policy enforcement