Phase 2: Building a Protected Identity & Trusted Data API with ASP.NET Core

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.

Related Posts

FlappyLove — Real-Time Multiplayer Browser Game ❤️

A playful real-time multiplayer browser game built with Next.js and TypeScript, featuring multiplayer rooms, synchronized gameplay, live scoring, and a Railway-hosted game server. ❤️

September 12, 2026·Marvin D. Verdera·Next.js, React

Phase 4: Building a Secure Next.js Identity Client

A Next.js and React client demonstrating OAuth, PKCE, JWT, and DPoP integration with the Secure Identity & Trusted Data API.

September 11, 2026·Marvin D. Verdera·Next.js, React, PKCE, JWK, JWT, API Security

Phase 3: Securing OAuth Access Tokens with DPoP in ASP.NET Core

Phase 3 introduces DPoP sender-constrained access tokens to the Secure Identity & Trusted Data API, demonstrating ES256 proof-of-possession, JWT key binding, replay protection, OAuth scopes, and ASP.NET Core security architecture.

September 2, 2026·Marvin D. Verdera·API Integration, API Security, JWK, JWT, LLM