Phase 1: Building a Secure Identity Provider with ASP.NET Core, OAuth 2.1, PKCE, JWT & JWK

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.json

The 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 JWT

On the other hand, retrieving public signing keys is a query:

GetJwksQuery
     │
     ▼
Query Handler
     │
     ▼
Public JWK Response

This 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 Token

The 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_challenge

The authorization request contains the challenge:

code_challenge
code_challenge_method=S256

Later, 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         Request

Only 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
jti

The 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             JWK

This 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.json

The 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-configuration

This provides discovery metadata for the fictional Identity Provider, including information such as:

issuer
authorization_endpoint
token_endpoint
jwks_uri

This 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

  • kid key identification

  • Issuer 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 endpoint

Integration 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

https://localhost:7001/swagger

Swagger UI — explore the API endpoints and try them live

https://localhost:7001/swagger/v1/swagger.json

Raw OpenAPI JSON

https://localhost:7001/.well-known/jwks.json

Public JWK — exposes the public RSA signing key

https://localhost:7001/.well-known/openid-configuration

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.json

Only 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-configuration

This 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 localhost URLs 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 Verification

Building 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 Deployment

The 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.

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