<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dor Danai</title>
    <description>The latest articles on DEV Community by Dor Danai (@karnafun).</description>
    <link>https://dev.to/karnafun</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3597343%2Feba2c378-2a91-44d6-815a-76caa4e16755.png</url>
      <title>DEV Community: Dor Danai</title>
      <link>https://dev.to/karnafun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/karnafun"/>
    <language>en</language>
    <item>
      <title>Building a Secure Stripe Checkout Integration with ASP.NET Core and Webhook Handling</title>
      <dc:creator>Dor Danai</dc:creator>
      <pubDate>Sun, 09 Nov 2025 12:04:19 +0000</pubDate>
      <link>https://dev.to/karnafun/building-a-secure-stripe-checkout-integration-with-aspnet-core-and-webhook-handling-ga3</link>
      <guid>https://dev.to/karnafun/building-a-secure-stripe-checkout-integration-with-aspnet-core-and-webhook-handling-ga3</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;This demo shows how to integrate Stripe Checkout with ASP.NET Core. We handle the complete payment flow including session creation, webhook processing, and race condition management between client callbacks and server notifications.&lt;/p&gt;

&lt;p&gt;Why this matters: Stripe webhooks and client callbacks can arrive in any order. Your system needs to handle this race condition safely. This demo shows atomic state transitions and proper webhook verification for production-grade payment processing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(This demo simplifies some production implementations, explained in detail at the end.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Project / Setup Overview
&lt;/h2&gt;

&lt;p&gt;The project contains a single ASP.NET Core application running on &lt;code&gt;https://localhost:5001&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Folder Structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stripe-checkout-aspnet-demo/
├── Controllers/
│   ├── CheckoutController.cs    # Creates Stripe sessions
│   ├── CallbackController.cs    # Handles success redirect
│   └── WebhookController.cs     # Processes Stripe webhooks
├── Services/
│   ├── ProductService.cs        # Maps products to Stripe prices
│   └── OrderService.cs          # Business logic for orders
├── Repositories/
│   └── OrderRepository.cs       # Order state management
├── Contracts/
│   ├── Enums.cs                 # OrderStatus enum
│   └── CreateCheckoutSessionRequest.cs
└── wwwroot/
    └── checkout.html            # Test checkout form
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Endpoints:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /api/checkout/create-session&lt;/code&gt; – Creates Stripe Checkout session, registers order as Pending&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /success&lt;/code&gt; – Client redirect handler, updates order to Confirmed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /stripe-webhook&lt;/code&gt; – Stripe server notification, finalizes order as Fulfilled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Order States:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pending&lt;/strong&gt; – Initial state after session creation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirmed&lt;/strong&gt; – Set when user returns from Stripe&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fulfilled&lt;/strong&gt; – Set by webhook, may skip Confirmed if webhook arrives first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed&lt;/strong&gt; – Reserved for unexpected database errors (should not occur in this demo)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Session Creation
&lt;/h3&gt;

&lt;p&gt;The frontend calls &lt;code&gt;/api/checkout/create-session&lt;/code&gt; with &lt;code&gt;ItemId&lt;/code&gt; and &lt;code&gt;Quantity&lt;/code&gt;. The backend maps the item to a Stripe Price ID on the server side. This prevents price tampering since clients never see the actual price.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateCheckoutSessionRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErrorMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Item ID must be provided."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ItemId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrorMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Quantity must be between 1 and 100."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server registers the order as Pending and creates a Stripe session. The session URL redirects users to Stripe's hosted checkout page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payment Process
&lt;/h3&gt;

&lt;p&gt;After payment, Stripe performs two actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redirects the browser to &lt;code&gt;/success&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sends a webhook to &lt;code&gt;/stripe-webhook&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These can arrive in any order. The webhook might arrive before the redirect, or vice versa.&lt;/p&gt;

&lt;h3&gt;
  
  
  Race Condition Handling
&lt;/h3&gt;

&lt;p&gt;Both endpoints attempt to update order state. The &lt;code&gt;OrderRepository&lt;/code&gt; uses &lt;code&gt;ConcurrentDictionary.TryUpdate&lt;/code&gt; for atomic transitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;TryUpdateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt; &lt;span class="n"&gt;currentStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt; &lt;span class="n"&gt;newStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_orderStatuses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/success&lt;/code&gt; endpoint tries: Pending → Confirmed&lt;/p&gt;

&lt;p&gt;The webhook tries two transitions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pending → Fulfilled (if webhook arrives first)&lt;/li&gt;
&lt;li&gt;Confirmed → Fulfilled (if redirect arrived first)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;TryFulfillOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Try to transition from PENDING -&amp;gt; FULFILLED&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryUpdateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fulfilled&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// If that failed, try to transition from CONFIRMED -&amp;gt; FULFILLED&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryUpdateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Confirmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fulfilled&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whichever arrives first locks in the state. The second arrival safely fails the transition and logs appropriately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Webhook Verification
&lt;/h3&gt;

&lt;p&gt;The webhook controller verifies Stripe's signature before processing events. This prevents unauthorized webhook calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StreamReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ReadToEndAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stripeEvent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EventUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConstructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Stripe-Signature"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;_webhookSecret&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stripeEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"checkout.session.completed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stripeEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"internal_order_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Unknown"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;fulfilled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryFulfillOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fulfilled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order {OrderId} fulfilled successfully."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order {OrderId} fulfillment skipped (already fulfilled)."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dependency Injection
&lt;/h3&gt;

&lt;p&gt;Services and repositories are registered in &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Register services and repositories&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;OrderRepository&lt;/code&gt; uses Singleton lifetime because the in-memory dictionary must be shared across requests. In production with a real database, use Scoped lifetime to match DbContext lifecycle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo / Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Configure Stripe Keys
&lt;/h3&gt;

&lt;p&gt;Add your Stripe keys to &lt;code&gt;appsettings.json&lt;/code&gt; or use .NET Secret Manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"Stripe"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SecretKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk_test_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"WebhookSecret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"whsec_..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get your webhook signing secret using Stripe CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe listen &lt;span class="nt"&gt;--forward-to&lt;/span&gt; https://localhost:5001/stripe-webhook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows your webhook secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Ready! You are using Stripe API Version [2025-10-29.clover]. 
Your webhook signing secret is whsec_... (^C to quit)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the &lt;code&gt;whsec_&lt;/code&gt; value to your configuration.&lt;/p&gt;

&lt;p&gt;Learn more: &lt;a href="https://stripe.com/docs/stripe-cli/webhooks" rel="noopener noreferrer"&gt;Listening to webhooks with Stripe CLI&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Define Product Mapping
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;ProductService.cs&lt;/code&gt;, map your internal product IDs to Stripe Price IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SecurePriceMap&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"premium_product_demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"price_1ABC..."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Stripe Price IDs are account-specific. Create your own products and prices in the Stripe Dashboard, then use those Price IDs here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Run the Project
&lt;/h3&gt;

&lt;p&gt;Start the application and navigate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://localhost:5001/checkout.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Proceed to Checkout&lt;/strong&gt;. You'll be redirected to Stripe's hosted checkout page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faq591u4wu0rwctgy548u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faq591u4wu0rwctgy548u.png" alt="Checkout Button" width="456" height="61"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Complete Payment
&lt;/h3&gt;

&lt;p&gt;Use Stripe's test card number: &lt;code&gt;4242 4242 4242 4242&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any future expiry date&lt;/li&gt;
&lt;li&gt;Any 3-digit CVC&lt;/li&gt;
&lt;li&gt;Any ZIP code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After payment, observe the console output showing state transitions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxk8rn2c9qqezip251ut.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxk8rn2c9qqezip251ut.png" alt="Console Log" width="800" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Stripe CLI also shows webhook delivery:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5a4c562eptq2d056wpb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5a4c562eptq2d056wpb.png" alt="Stripe Webhook" width="800" height="219"&gt;&lt;/a&gt;&lt;br&gt;
You'll be redirected to &lt;code&gt;/success&lt;/code&gt; with a JSON response showing the final order state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Steps / Extensions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;OrderRepository&lt;/code&gt; in-memory storage with Entity Framework Core or Dapper backed by SQL Server or PostgreSQL.&lt;/li&gt;
&lt;li&gt;Implement retry logic for webhook processing using Polly or similar libraries.&lt;/li&gt;
&lt;li&gt;Track processed Stripe Event IDs in the database to ensure idempotency.&lt;/li&gt;
&lt;li&gt;Replace console logging with Serilog or another production logger.&lt;/li&gt;
&lt;li&gt;Add customer email notifications using SendGrid or similar services.&lt;/li&gt;
&lt;li&gt;Implement comprehensive error handling and monitoring with Application Insights or Sentry.&lt;/li&gt;
&lt;li&gt;Add unit tests for order state transitions and webhook handling.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Production Notes / Limitations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In-Memory Storage:&lt;/strong&gt; &lt;code&gt;OrderRepository&lt;/code&gt; uses &lt;code&gt;ConcurrentDictionary&lt;/code&gt; to simulate database persistence. This data disappears when the application restarts. Production systems need Entity Framework Core, Dapper, or another data access layer backed by a real database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Singleton Lifetime:&lt;/strong&gt; &lt;code&gt;OrderRepository&lt;/code&gt; is registered as Singleton because the in-memory dictionary must be shared across all requests. With a real database, use Scoped lifetime to match DbContext lifecycle and ensure proper transaction boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency:&lt;/strong&gt; Production systems must track processed Stripe Event IDs to prevent duplicate fulfillment. Stripe may send the same webhook multiple times. Store event IDs in your database and skip already-processed events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; Always verify webhook signatures using &lt;code&gt;EventUtility.ConstructEvent&lt;/code&gt;. Never process webhooks without signature verification. Use HTTPS for webhook endpoints in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error Handling:&lt;/strong&gt; Implement comprehensive logging for all state transitions. Monitor webhook processing failures and set up alerts. Implement retry logic for transient failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing:&lt;/strong&gt; Stripe provides test mode for development. Use different API keys for test and production environments. Never expose secret keys in client-side code or public repositories.&lt;/p&gt;

&lt;p&gt;This demo serves as an educational foundation rather than production-ready code. It demonstrates proper architectural patterns and race condition handling, but requires additional hardening for production use.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Stripe Checkout integration with ASP.NET Core showing proper webhook handling and race condition management. The demo uses atomic state transitions to handle asynchronous payment flows safely. Webhooks and client callbacks can arrive in any order, and the system handles both scenarios correctly.&lt;/p&gt;

&lt;p&gt;GitHub Repository: &lt;a href="https://github.com/karnafun/stripe-checkout-aspnet-demo" rel="noopener noreferrer"&gt;https://github.com/karnafun/stripe-checkout-aspnet-demo&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Looking for help integrating payment systems or building secure APIs? &lt;a href="https://www.fiverr.com/s/NNKV0VG" rel="noopener noreferrer"&gt;Hire me&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Backend engineer &amp;amp; API integrator. Building secure, scalable APIs with .NET.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>stripe</category>
      <category>webhooks</category>
    </item>
    <item>
      <title>Building an OAuth2 Protected API with C#, IdentityServer, and ASP.NET Core</title>
      <dc:creator>Dor Danai</dc:creator>
      <pubDate>Thu, 06 Nov 2025 18:45:42 +0000</pubDate>
      <link>https://dev.to/karnafun/building-an-oauth2-protected-api-with-c-identityserver-and-aspnet-core-23g2</link>
      <guid>https://dev.to/karnafun/building-an-oauth2-protected-api-with-c-identityserver-and-aspnet-core-23g2</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;This demo demonstrates protecting an ASP.NET Core API with OAuth2 and JWT tokens. We use Duende IdentityServer as the authorization server. The API validates tokens and allows only authenticated requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; OAuth2 is the standard for securing APIs. Understanding token-based authentication helps you build production-ready applications. This setup shows the complete flow from token request to protected endpoint access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project / Setup Overview
&lt;/h2&gt;

&lt;p&gt;The solution contains two projects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;IdentityServer&lt;/strong&gt; – Issues OAuth2 tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ApiDemo&lt;/strong&gt; – Protected API that validates JWT tokens&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Folder Structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CSharp-API-OAuth2-Demo/
├── IdentityServer/
│   ├── Config.cs          # Client, scope, and resource definitions
│   ├── Users.cs           # Test user credentials
│   ├── Program.cs         # IdentityServer setup
│   └── wwwroot/
│       └── test-token.html # Token request form
└── ApiDemo/
    ├── Controllers/
    │   └── UsersController.cs  # Protected endpoint
    ├── Configuration/
    │   └── IdentityServerOptions.cs  # Strongly-typed config
    └── Program.cs         # API with JWT validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Endpoints:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /connect/token&lt;/code&gt; (IdentityServer) – Request access token&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /Users&lt;/code&gt; (ApiDemo) – Protected endpoint requiring a valid token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1z886rvx7qfjtpq4jbz2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1z886rvx7qfjtpq4jbz2.png" alt="Visual Studio solution showing IdentityServer (authorization server) and ApiDemo (protected API) projects side by side" width="345" height="68"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  IdentityServer Configuration
&lt;/h3&gt;

&lt;p&gt;IdentityServer defines clients, scopes, and resources. Clients represent applications that request tokens. Scopes define what the token allows. Resources are the APIs being protected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Clients&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ClientId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"demo-client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;AllowedGrantTypes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GrantTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResourceOwnerPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ClientSecrets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo-secret"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sha256&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;AllowedScopes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"api.read"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Resource Owner Password grant is simple for demos. It allows direct username/password exchange for tokens. Production systems should use Authorization Code with PKCE for better security.&lt;/p&gt;

&lt;p&gt;The API resource defines the audience that tokens are issued for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ApiResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"API Demo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Scopes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"api.read"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  API Token Validation
&lt;/h3&gt;

&lt;p&gt;The API validates incoming JWT tokens using the &lt;code&gt;JwtBearer&lt;/code&gt; authentication scheme. It checks the token signature against IdentityServer's public key and validates the audience to ensure the token was issued for this API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddJwtBearer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// IdentityServer URL&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TokenValidationParameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TokenValidationParameters&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ValidateAudience&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ValidAudience&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"api-demo"&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuration validation:&lt;/strong&gt; The Authority URL comes from &lt;code&gt;appsettings.json&lt;/code&gt;. Validation at startup ensures required settings are present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IdentityServerOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"IdentityServer"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateDataAnnotations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authority&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
        &lt;span class="s"&gt;"IdentityServer:Authority configuration value is required."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateOnStart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Protected Endpoint
&lt;/h3&gt;

&lt;p&gt;Controllers use the &lt;code&gt;[Authorize]&lt;/code&gt; attribute to require authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Demo User"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Admin"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Without a valid token: &lt;strong&gt;401 Unauthorized&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;With a valid token: returns user data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fog1mktwusuqvudbe2ids.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fog1mktwusuqvudbe2ids.png" alt="Token request form showing username, password, client credentials, and scope fields" width="475" height="699"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo / Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Start Both Projects
&lt;/h3&gt;

&lt;p&gt;Run IdentityServer on &lt;code&gt;https://localhost:5001&lt;/code&gt; and ApiDemo on &lt;code&gt;https://localhost:5002&lt;/code&gt;. Both projects enforce HTTPS redirection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Request a Token
&lt;/h3&gt;

&lt;p&gt;Navigate to &lt;code&gt;https://localhost:5001/test-token.html&lt;/code&gt;. The form is pre-filled with demo credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Username:&lt;/strong&gt; &lt;code&gt;demo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password:&lt;/strong&gt; &lt;code&gt;password&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client ID:&lt;/strong&gt; &lt;code&gt;demo-client&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Secret:&lt;/strong&gt; &lt;code&gt;demo-secret&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; &lt;code&gt;api.read&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;strong&gt;Get Token&lt;/strong&gt;. The response contains an &lt;code&gt;access_token&lt;/code&gt; field. Copy this token.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3xko42vj4arti9gi7h81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3xko42vj4arti9gi7h81.png" alt="Token response showing JSON with  raw `access_token` endraw  field" width="800" height="136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Call the Protected API
&lt;/h3&gt;

&lt;p&gt;Use the token in the Authorization header. &lt;strong&gt;Format:&lt;/strong&gt; &lt;code&gt;Bearer &amp;lt;your-token&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using Swagger:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;https://localhost:5002/swagger&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Authorize&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;Bearer &amp;lt;your-token&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Authorize&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Call the &lt;code&gt;GET /Users&lt;/code&gt; endpoint&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Using curl:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;your-token&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     https://localhost:5002/Users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected response:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Demo User"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Admin"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Without token:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://localhost:5002/Users
&lt;span class="c"&gt;# Returns 401 Unauthorized&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faxcq5s1rxpv9q6d7kzvv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faxcq5s1rxpv9q6d7kzvv.png" alt="Swagger UI showing authorized request with successful response" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error handling:&lt;/strong&gt; The HTML form shows clear notifications for errors, including network failures and invalid credentials. Clipboard copy operations also handle browser restrictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps / Extensions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Replace Resource Owner Password grant&lt;/strong&gt; – Use Authorization Code with PKCE for web apps, or Client Credentials for service-to-service.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store users in a database&lt;/strong&gt; – Replace test users with ASP.NET Core Identity.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store clients dynamically&lt;/strong&gt; – Use &lt;code&gt;IClientStore&lt;/code&gt; for database-driven client management.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add refresh tokens&lt;/strong&gt; – Implement token refresh flow for longer sessions.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add role-based authorization&lt;/strong&gt; – Enforce roles using claims: &lt;code&gt;[Authorize(Roles = "Admin")]&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment-specific configuration&lt;/strong&gt; – Use &lt;code&gt;appsettings.Production.json&lt;/code&gt; and secure secrets in Azure Key Vault or similar.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add CORS&lt;/strong&gt; – Configure for APIs used by web apps from different origins.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Learning resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.duendesoftware.com/identityserver" rel="noopener noreferrer"&gt;Duende IdentityServer Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://oauth.net/2/" rel="noopener noreferrer"&gt;OAuth 2.0 Specification&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://jwt.io/" rel="noopener noreferrer"&gt;JWT.io&lt;/a&gt; – Decode and inspect JWT tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;IdentityServer issues JWT tokens. The API validates them to protect endpoints using &lt;code&gt;[Authorize]&lt;/code&gt;. Configuration validation ensures required settings are present. Extend this with database-backed users, different grant types, and production-ready security practices.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; [&lt;a href="https://github.com/karnafun/identityserver-oauth2-demo" rel="noopener noreferrer"&gt;https://github.com/karnafun/identityserver-oauth2-demo&lt;/a&gt;]  &lt;/p&gt;

&lt;p&gt;Looking for help building secure APIs? &lt;a href="https://www.fiverr.com/s/NNKV0VG" rel="noopener noreferrer"&gt;Hire me&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Backend engineer &amp;amp; API integrator. Building secure, scalable APIs with .NET.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>security</category>
      <category>csharp</category>
      <category>api</category>
    </item>
  </channel>
</rss>
