Phase 2: Building a Protected Identity & Trusted Data API
After completing Phase 1, where I built a self-contained Identity Provider capable of issuing OAuth authorization codes and signed JWT access tokens, the next step was to build the service that actually consumes those tokens.
Phase 2 introduces the Protected Identity & Trusted Data API.
This component acts as an OAuth Resource Server. It accepts access tokens issued by the Phase 1 Identity Provider, validates those tokens using the Identity Provider's public JWK, checks authorization scopes, and retrieves fictional identity information from PostgreSQL.
The goal is to demonstrate the complete relationship between an authorization server and a protected resource server using C# and ASP.NET Core.
Important: This is an independent educational Proof of Concept. It does not connect to, replicate, or represent Singpass, MyInfo, or any government identity service.
What I Built
Phase 2 introduces a new ASP.NET Core application:
IdentityData.Api
The API is responsible for protecting and serving fictional identity information.
The main technologies used in this phase are:
C#
.NET 10
ASP.NET Core Web API
CQRS
MediatR
Entity Framework Core
PostgreSQL
Supabase PostgreSQL
JWT Bearer Authentication
JWK
OAuth scopes
Swagger / OpenAPI
xUnit
Integration testing
Docker
The Next.js client and DPoP are intentionally deferred to later phases.
Phase 1 → Phase 2
The architecture now extends from:
Phase 1
Client
│
▼
Identity Provider
│
▼
JWT
to:
Phase 1 Phase 2
Identity Provider
│
│ JWT Access Token
▼
Identity Data API
│
│ Validate JWT
│ Validate Scope
▼
PostgreSQL
│
▼
Identity Data
This separation is important.
The Identity Provider is responsible for issuing tokens.
The Identity Data API is responsible for protecting resources and deciding whether an incoming request is authorized to access them.
Authorization Server vs Resource Server
One of the main concepts demonstrated in Phase 2 is the separation between authentication/authorization infrastructure and protected resources.
IdentityProvider.Api
Acts as the fictional Authorization Server.
Its responsibilities include:
Authorization
PKCE
Authorization Code
JWT issuance
RSA signing
JWK publication
IdentityData.Api
Acts as the Resource Server.
Its responsibilities include:
JWT validation
Scope validation
Authenticated-user identification
Protected resources
Database access
The relationship looks like this:
┌────────────────────────────┐
│ IdentityProvider.Api │
│ │
│ Authorization Server │
│ │
│ OAuth + PKCE │
│ JWT Issuance │
└─────────────┬──────────────┘
│
│ Access Token
▼
┌────────────────────────────┐
│ IdentityData.Api │
│ │
│ Resource Server │
│ │
│ JWT Validation │
│ Scope Authorization │
│ Protected Resources │
└─────────────┬──────────────┘
│
▼
PostgreSQL
This gives each service a clearly defined responsibility.
JWT Validation
The Identity Data API does not simply trust an incoming JWT.
Every protected request goes through token validation.
For example:
GET /api/profile
Authorization: Bearer eyJ...
The API validates the token before allowing access to the resource.
The validation includes checks such as:
Signature
Issuer
Audience
Expiration
Signing Key
Algorithm
The public signing key comes from the Identity Provider's JWK endpoint.
IdentityProvider.Api
│
│ /.well-known/jwks.json
▼
IdentityData.Api
│
│ Public RSA Key
▼
JWT Signature Validation
The Resource Server never receives or stores the private RSA signing key.
JWK-Based Key Discovery
Phase 1 exposed the Identity Provider's public RSA key through:
/.well-known/jwks.json
Phase 2 consumes that information.
This creates an important separation:
Private Key
│
▼
IdentityProvider.Api
│
│ Signs JWT
▼
Access Token
│
▼
IdentityData.Api
│
│ Retrieves public key
▼
JWK
│
▼
Verify JWT
The private signing key stays with the service that issues the token.
The Resource Server only needs the corresponding public key to verify the signature.
Authentication vs Authorization
Phase 2 also demonstrates the difference between authentication and authorization.
Authentication
Answers:
Who is making this request?
The Resource Server obtains the subject from the validated JWT.
For example:
sub = user-001
Authorization
Answers:
Is this user/application allowed to access this resource?
That is where OAuth scopes become important.
For example:
identity.read
can be required to access identity information.
This produces the following flow:
Request
│
▼
Is JWT valid?
│
├── No → 401 Unauthorized
│
▼
Who is the caller?
│
▼
Does caller have required scope?
│
├── No → 403 Forbidden
│
▼
Return protected resource
Keeping authentication and authorization separate makes the API security model easier to reason about.
OAuth Scopes
Phase 2 introduces scope-based authorization.
The fictional Identity Provider can issue scopes such as:
openid
profile
identity.read
The protected identity endpoints require:
identity.read
For example:
GET /api/profile
Authorization: Bearer <access_token>
If the token is valid and contains:
identity.read
the request is allowed.
If the token is valid but does not contain the required scope:
403 Forbidden
This demonstrates that having a valid access token does not automatically mean the caller has permission to access every resource.
Protected Profile Endpoint
The first protected endpoint is:
GET /api/profile
The endpoint requires:
identity.read
The API obtains the user's identity from the validated token:
JWT
│
└── sub = user-001
│
▼
PostgreSQL lookup
│
▼
User Profile
The client does not provide an arbitrary user ID.
Instead of allowing:
GET /api/profile/user-001
the API determines the current user from the validated authentication context.
This helps prevent a client from simply changing an ID in the URL to access another user's information.
Identity Endpoint
Phase 2 also introduces:
GET /api/identity
The endpoint returns fictional identity information.
Example:
{
"subject": "user-001",
"name": "Demo User",
"email": "demo@example.test",
"dateOfBirth": "1995-04-12"
}
All information used by this POC is fictional.
The API does not expose database implementation details to the client.
PostgreSQL
Phase 2 introduces PostgreSQL storage.
For development and deployment planning, the database is hosted using Supabase PostgreSQL.
The initial model contains concepts such as:
users
identity_attributes
consents
audit_logs
The basic relationship is:
users
│
├── identity_attributes
│
└── consents
audit_logs
This provides a foundation for later phases where consent and audit capabilities can be expanded.
Entity Framework Core
Entity Framework Core is used as the data-access layer.
The application uses:
CQRS Query
│
▼
Query Handler
│
▼
EF Core
│
▼
PostgreSQL
For example:
GetProfileQuery
│
▼
GetProfileQueryHandler
│
▼
Users table
│
▼
Profile DTO
The database layer is intentionally separated from the API endpoints.
CQRS
CQRS continues to be used in Phase 2.
Because Phase 2 primarily exposes read-only identity resources, the main application operations are queries.
Examples:
GetProfileQuery
GetIdentityAttributesQuery
A request follows this pattern:
GET /api/profile
│
▼
GetProfileQuery
│
▼
GetProfileQueryHandler
│
▼
Current User
│
▼
EF Core
│
▼
PostgreSQL
│
▼
Profile DTO
The controllers remain thin while application behavior lives inside the query handlers.
Current User Context
A dedicated ICurrentUser abstraction was introduced to avoid accessing HttpContext.User throughout the application.
The abstraction exposes information such as:
Subject
Scopes
ClientId
This keeps the application code focused on the authenticated identity rather than the underlying ASP.NET Core HTTP implementation.
The basic flow is:
JWT
│
▼
ASP.NET Core Authentication
│
▼
ClaimsPrincipal
│
▼
ICurrentUser
│
▼
Application Query
Audit Logging
Phase 2 also introduces a lightweight audit mechanism.
Examples of recorded events include:
ProfileAccessed
IdentityAccessed
UnauthorizedRequest
ForbiddenRequest
The audit records contain useful information such as:
Event Type
User Subject
Resource
Timestamp
Sensitive values such as access tokens and authorization codes are not stored in the audit log.
Error Handling
The API distinguishes between authentication and authorization failures.
Missing or invalid token
401 Unauthorized
Valid token without required scope
403 Forbidden
This distinction is important when designing protected APIs.
The API also avoids returning internal exception details or database errors to clients.
Security Considerations
Security remains a major focus of this phase.
The implementation includes:
JWT signature validation
Issuer validation
Audience validation
Expiration validation
Signing-key validation
OAuth scope authorization
Strict authentication handling
Secure error responses
Restricted CORS configuration
No access-token logging
No private-key logging
No authorization-code logging
Parameterized database access through EF Core
Configuration-based environment settings
The Resource Server does not require the Identity Provider's private signing key.
It only requires access to the public signing key.
Testing
Phase 2 includes both unit and integration testing.
Security-related negative scenarios are particularly important.
Tests cover cases such as:
✓ Valid JWT
✓ Invalid JWT signature
✓ Expired JWT
✓ Invalid issuer
✓ Invalid audience
✓ Missing JWT
✓ Missing identity.read scope
✓ Valid identity.read scope
✓ Current user extraction
✓ Protected profile endpoint
✓ Protected identity endpoint
Integration tests verify the complete interaction between the authentication middleware, authorization policies, application layer, and database.
The goal is to verify not only that valid requests work, but also that invalid requests are rejected correctly.
Docker
The Identity Data API is also prepared as a Docker container.
The application receives environment-specific configuration through environment variables rather than embedding secrets inside the image.
The Dockerized API provides a foundation for the eventual cloud deployment.
The planned architecture is:
Docker
│
▼
AWS ECS Fargate
│
▼
IdentityData.Api
│
▼
Supabase PostgreSQL
Actual AWS deployment is intentionally reserved for Phase 5.
Running Phase 2 Locally
After starting the ASP.NET Core applications, the APIs can be explored through Swagger.
Identity Provider
https://localhost:7001/swagger
Identity Data API
The exact local port is configured by the ASP.NET Core launch settings.
Open the Swagger URL shown when the application starts.
The protected endpoints include:
GET /api/profile
GET /api/identity
These endpoints require a valid JWT access token with the appropriate scope.
Phase 2 End-to-End Flow
The complete flow now looks like:
┌──────────────────────┐
│ Demo Client │
└──────────┬───────────┘
│
│ Authorization Code + PKCE
▼
┌──────────────────────┐
│ Identity Provider │
│ ASP.NET Core │
└──────────┬───────────┘
│
│ Signed JWT
▼
┌──────────────────────┐
│ Identity Data API │
│ ASP.NET Core │
│ │
│ JWT Validation │
│ Scope Validation │
└──────────┬───────────┘
│
│ EF Core
▼
┌──────────────────────┐
│ PostgreSQL │
│ │
│ Users │
│ Identity Attributes │
│ Consents │
│ Audit Logs │
└──────────────────────┘
This creates the foundation for a more advanced security model in the next phase.
Technology Stack
Backend
C#
.NET 10
ASP.NET Core Web API
Architecture
CQRS
MediatR
Clean Architecture principles
Security
OAuth
JWT
JWK
OAuth scopes
RSA signatures
JWT Bearer Authentication
Database
PostgreSQL
Supabase
Entity Framework Core
Testing
xUnit
Unit testing
Integration testing
Development
Swagger / OpenAPI
Docker
Git
GitHub
What I Learned
Phase 2 helped demonstrate the complete relationship between an OAuth Authorization Server and a protected Resource Server.
The most important concepts were:
JWT issuance
↓
JWK publication
↓
JWT validation
↓
Authentication
↓
Scope authorization
↓
Protected resource
↓
Database access
Rather than treating a JWT as simply a string containing claims, the API treats the token as a security credential that must be validated before any protected resource is accessed.
What's Next?
Phase 1 established the Identity Provider.
Phase 2 established the protected Resource Server.
The next phase will strengthen the token security model by introducing DPoP (Demonstrating Proof of Possession).
The planned architecture will evolve from:
Bearer Token
↓
Resource Server
to:
Client Key Pair
↓
DPoP Proof
↓
Bound Access Token
↓
Protected Resource
This will allow the POC to demonstrate sender-constrained access tokens and cryptographic proof of possession.
The later phases will also introduce the Next.js client and cloud deployment.
Project Roadmap
✓ Phase 1
OAuth + PKCE + JWT + JWK
│
▼
✓ Phase 2
Protected Identity/Data API
JWT Validation + Scopes + PostgreSQL
│
▼
○ Phase 3
DPoP + Token Binding
│
▼
○ Phase 4
Next.js / React Client
│
▼
○ Phase 5
AWS + Cloud Deployment
Source Code
The Phase 2 implementation is part of the same GitHub repository as Phase 1:
Secure Identity & Trusted Data API
[View Phase2 Source Code]
The repository contains the complete multi-phase POC and its source code.
Disclaimer
This project is an independent educational Proof of Concept created to demonstrate software engineering, API security, OAuth, cryptography, and identity integration concepts.
It is not affiliated with, connected to, endorsed by, or an implementation of Singpass, MyInfo, or any government identity service.
All identities and data used by the project are fictional.
