Modern applications increasingly depend on external identity and trusted-data services. These integrations require more than simply implementing a login form — they involve authorization flows, token security, cryptographic signatures, key management, and protected APIs.
For this Proof of Concept, I wanted to build those concepts from the ground up using C# and ASP.NET Core, without relying on an external identity provider.
The result is Secure Identity & Trusted Data API, a self-contained identity platform POC designed to demonstrate the core security mechanisms behind modern identity integrations.
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 1 focuses on building the foundation of a fictional Identity Provider.
The Identity Provider supports:
OAuth Authorization Code flow
PKCE using S256
JWT access tokens
RSA asymmetric signing
JWK public-key discovery
OpenID-style discovery metadata
Authorization-code validation
Redirect URI validation
Single-use authorization codes
Short-lived authorization codes and access tokens
CQRS-based application architecture
Automated unit and integration tests
The objective was not to create a complete production identity platform, but to build a realistic and security-focused foundation that can be extended in later phases.
Architecture
The Phase 1 architecture is intentionally focused.
┌───────────────────────────┐
│ Demo Client │
│ │
│ Authorization Request │
│ PKCE Code Challenge │
└─────────────┬─────────────┘
│
│ OAuth Authorization Code
▼
┌───────────────────────────┐
│ Identity Provider API │
│ ASP.NET Core │
│ │
│ /oauth/authorize │
│ /oauth/token │
│ │
│ JWT Token Service │
│ PKCE Service │
│ Authorization Code Store │
└─────────────┬─────────────┘
│
├──────────────────┐
│ │
▼ ▼
RSA Signing Key JWK Endpoint
│
▼
/.well-known/jwks.jsonThe implementation uses CQRS to separate application commands from queries.
Why CQRS?
I chose CQRS because the identity provider has a natural separation between operations that change state and operations that retrieve information.
For example, exchanging an authorization code for a token is a command because it changes the authorization-code state.
ExchangeAuthorizationCodeCommand
│
▼
Command Handler
│
├── Validate code
├── Validate PKCE
├── Validate client
├── Validate redirect URI
├── Mark code as used
└── Generate JWTOn the other hand, retrieving public signing keys is a query:
GetJwksQuery
│
▼
Query Handler
│
▼
Public JWK ResponseThis keeps the application logic organized around business use cases instead of putting all the logic inside controllers.
OAuth Authorization Code Flow
The main authentication flow follows the Authorization Code pattern.
The simplified flow is:
Client
│
│ 1. Authorization Request
▼
Identity Provider
│
│ 2. Authenticate Demo User
▼
Consent
│
│ 3. Authorization Code
▼
Client
│
│ 4. Code + PKCE Verifier
▼
Token Endpoint
│
│ 5. Validate
│
│ 6. Issue JWT
▼
Access TokenThe authorization code itself does not contain sensitive user information.
Instead, the server maintains the authorization transaction and binds the code to the expected client, redirect URI, user, scope, and PKCE challenge.
PKCE
One of the important security mechanisms implemented in Phase 1 is Proof Key for Code Exchange (PKCE).
The client creates a random code_verifier and derives a code_challenge.
The simplified relationship is:
code_verifier
│
│ SHA-256
▼
Base64URL
│
▼
code_challengeThe authorization request contains the challenge:
code_challenge
code_challenge_method=S256Later, when the client exchanges the authorization code, it provides the original verifier.
The server calculates the challenge again and compares the values.
Client
│
├── code_challenge ───────► Authorization
│
│
└── code_verifier ────────► Token Endpoint
│
▼
Calculate S256
│
▼
Compare challenge
│
┌──────┴──────┐
│ │
Match Mismatch
│ │
▼ ▼
Issue Reject
Token RequestOnly S256 is supported in this POC.
JWT Access Tokens
After successful authorization-code and PKCE validation, the Identity Provider creates a signed JWT access token.
The token contains claims such as:
iss
sub
aud
scope
iat
exp
jtiThe JWT is signed using an RSA private key.
The private key remains inside the Identity Provider.
The corresponding public key is exposed through the JWK endpoint.
This creates an asymmetric signing model:
RSA Key Pair
│
┌────────┴────────┐
│ │
Private Key Public Key
│ │
▼ ▼
Sign JWT Verify JWT
│ │
▼ ▼
Token JWKThis is preferable to sharing a symmetric secret between multiple services when the architecture requires independent token verification.
JWK
The Identity Provider exposes its public signing key through:
GET /.well-known/jwks.jsonThe response contains the public portion of the RSA key.
A kid is included so consumers can identify which signing key was used for a particular JWT.
The important security principle here is that the public key can be distributed, while the private signing key must remain protected.
The API never exposes private RSA parameters.
Discovery Metadata
Phase 1 also implements:
GET /.well-known/openid-configurationThis provides discovery metadata for the fictional Identity Provider, including information such as:
issuer
authorization_endpoint
token_endpoint
jwks_uriThis makes the POC closer to how discoverable identity services are commonly structured while deliberately avoiding a claim of full OpenID Connect compliance.
Security Considerations
Security was treated as a core requirement rather than something added at the end.
The Phase 1 implementation includes:
Authorization Code Security
Short expiration
Single-use enforcement
Client binding
Redirect URI binding
PKCE binding
Cryptographically secure random generation
JWT Security
RSA asymmetric signing
Short token lifetime
kidkey identificationIssuer and audience claims
No unnecessary personal information in the token
Key Security
Private signing key is never exposed
Public key is exposed through JWK
No private keys committed to source control
Production key-management considerations documented
API Security
Input validation
Strict redirect URI validation
Consistent OAuth-style errors
No access-token logging
No authorization-code logging
No private-key logging
No stack traces returned to clients
Testing
Security-related code needs more than happy-path tests.
The project includes tests for scenarios such as:
✓ Valid authorization request
✓ Invalid client
✓ Invalid redirect URI
✓ Missing PKCE challenge
✓ Unsupported PKCE method
✓ Valid token exchange
✓ Invalid authorization code
✓ Expired authorization code
✓ Reused authorization code
✓ Incorrect client
✓ Incorrect redirect URI
✓ Invalid code verifier
✓ Missing code verifier
✓ JWT generation
✓ JWT signature
✓ JWK endpoint
✓ Discovery endpointIntegration tests also verify the complete authorization-code flow.
Technology Stack
Backend
C#
.NET 10
ASP.NET Core Web API
CQRS
MediatR
Security
OAuth 2.1 concepts
PKCE
JWT
JWK
RSA
Cryptographic signing
Testing
Unit testing
Integration testing
Development
Git
GitHub
Swagger / OpenAPI
Mermaid architecture diagrams
A database and cloud infrastructure are intentionally not part of Phase 1.
Running the POC Locally
After starting the ASP.NET Core application, the browser should open automatically.
If it doesn't, navigate to the following URLs:
URL | Purpose |
|---|---|
| Swagger UI — explore the API endpoints and try them live |
| Raw OpenAPI JSON |
| Public JWK — exposes the public RSA signing key |
| Discovery document — exposes the Identity Provider's supported endpoints and metadata |
Swagger UI
The Swagger interface provides an interactive way to explore the Identity Provider API during development.
From Swagger, the implemented endpoints can be inspected and tested without requiring an additional API client.
JWK Endpoint
The JWK endpoint exposes the public RSA key used by clients and APIs to verify JWT signatures.
https://localhost:7001/.well-known/jwks.jsonOnly the public portion of the signing key is exposed. The private signing key remains inside the Identity Provider.
Discovery Endpoint
The discovery endpoint provides metadata about the fictional Identity Provider:
https://localhost:7001/.well-known/openid-configurationThis allows clients to discover important endpoints such as the authorization endpoint, token endpoint, and JWK endpoint.
Note: These endpoints are running locally over HTTPS. The
localhostURLs are intended for development and demonstration purposes only.
Why I Built This
The purpose of this POC is to go beyond implementing a traditional username/password authentication system.
I wanted to understand what happens between:
Authorization
↓
Authorization Code
↓
PKCE Verification
↓
Token Exchange
↓
JWT Signing
↓
Public Key Discovery
↓
Token VerificationBuilding the Identity Provider myself makes each part of the security flow visible and testable.
It also provides a foundation for the next phases of the project.
What's Next?
Phase 1 establishes the Identity Provider.
The planned roadmap is:
Phase 1
OAuth + PKCE + JWT + JWK
│
▼
Phase 2
Protected Identity / Data API
│
▼
Phase 3
DPoP + Token Binding
│
▼
Phase 4
Next.js / React Client
│
▼
Phase 5
AWS + PostgreSQL + Cloud DeploymentThe next phase will introduce a protected data API that validates the access tokens issued by the Identity Provider.
Later phases will introduce DPoP, allowing the project to demonstrate sender-constrained access tokens and stronger protection for API requests.
Source Code
The complete Phase 1 source code is available on GitHub:
[GitHub Repository — View Phase 1 Source Code]
Disclaimer
This project is an independent educational Proof of Concept created to demonstrate general software engineering and security 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.
