惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
爱范儿
爱范儿
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CERT Recently Published Vulnerability Notes
P
Palo Alto Networks Blog
博客园 - 司徒正美
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
罗磊的独立博客
阮一峰的网络日志
阮一峰的网络日志
The Register - Security
The Register - Security
D
DataBreaches.Net
A
Arctic Wolf
C
Cyber Attacks, Cyber Crime and Cyber Security
P
Privacy & Cybersecurity Law Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
B
Blog
V
Vulnerabilities – Threatpost
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
Google Developers Blog
aimingoo的专栏
aimingoo的专栏
T
Tor Project blog
GbyAI
GbyAI
Recent Announcements
Recent Announcements
T
The Blog of Author Tim Ferriss
Simon Willison's Weblog
Simon Willison's Weblog
Cyberwarzone
Cyberwarzone
C
Cisco Blogs
G
GRAHAM CLULEY
宝玉的分享
宝玉的分享
T
Threat Research - Cisco Blogs
C
Check Point Blog
W
WeLiveSecurity
F
Fortinet All Blogs
P
Proofpoint News Feed
Security Archives - TechRepublic
Security Archives - TechRepublic
月光博客
月光博客
Project Zero
Project Zero
Know Your Adversary
Know Your Adversary
V
Visual Studio Blog
H
Help Net Security
H
Hacker News: Front Page
Webroot Blog
Webroot Blog
S
Securelist
酷 壳 – CoolShell
酷 壳 – CoolShell
O
OpenAI News
The Cloudflare Blog
Attack and Defense Labs
Attack and Defense Labs

Duende Software Official Site

Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Stop AI Bots from Wasting Your Server How Duende IdentityServer Filters Claims (And Why It Matters) Core vs Extended Protocols in Duende IdentityServer v8: What You Get and When You Need More Your IdentityServer v8 Upgrade Checklist: A Quick Pre-Flight Guide Setting Up SAML Single Sign-On in ASP.NET with Duende IdentityServer Your Identity, Your Terms: Duende's Modular Identity Infrastructure and v8.x Release Duende Spring Launch '26: Identity Infrastructure That Expands With You SAML and OpenID Connect (OIDC): Coexistence, Not Competition The 9 Components of SAML You Need to Know, Ranked by Importance The Cost of NOT Implementing Financial-Grade Security Token Issuer Isolation: Why It Matters for Security and Compliance The Composable Identity Pattern: Build What You Need, Skip What You Don't Multi-Brand Identity: When Your Company Needs More Than One Face Post-Quantum Cryptography in .NET 10: A Practical Guide The field Keyword in C# 14: Write Less, Validate More The Real Cost of Build vs. Buy for Identity Beyond localhost: Multi-Instance ASP.NET Core Deployment with .NET 10 Harden Your .NET JSON Deserialization with System.Text.Json and JsonSerializerOptions.Strict ASP.NET Core Cookie Size Limits in Production: Causes and Fixes The Emergency Stop Button - Implementing Immediate Token Revocation in .NET 10 The 2025 OWASP Top 10 and IdentityServer Update Guidance for CVE-2026-40372 - ASP.NET Data Protection Why a Standard JWT Access Token Matters The Identity Governance Checklist You Wish You Had Six Months Ago The History and Future of SAML: Why a 20-Year-Old Protocol Still Matters The Cookie Apocalypse Already Happened Verify - Open Source Sponsorship Why Identity Is Infrastructure, Not a Feature Extending Duende IdentityServer Server-Side Sessions with Dynamic User Metadata Give Your AI Coding Assistant Duende Expertise with Agent Skills and MCP Server Triggering User Registration via OpenID Connect with Duende IdentityServer Improving .NET Security Code with C# 14 Property Extensions Developing Audit Logs with Duende IdentityServer Events Patch Releases: Addressing CVE-2026-26127 in Microsoft.BCL.Memory Client-Initiated Backchannel Authentication (CIBA) in ASP.NET Core 10 with Duende Identity Server Rate Limiting IdentityServer Endpoints It's Probably DNS - Can You Dig It? Security Lingo Explained: Encode vs Encrypt vs Hash Implementing Zero Trust with Resource Isolation Security Lingo Explained: JWT DPoP Security for .NET APIs with JwtBearer Extensions v1.0.0 Announcing the Duende IdentityServer4 Migration Analysis Tool BenchmarkDotNet - Open Source Sponsorship Security Lingo Explained: PAR Why Signing Key Rotation Matters in OpenID Connect and Duende IdentityServer Security Lingo Explained: OP Duende Year-End Review 2025 Security Lingo Explained: BCP Security Lingo Explained: DPoP Security Lingo Explained: Auth Secure frontend apps with the BFF Pattern Scaling with Duende IdentityServer, MCP, and AI Duende IdentityServer v7.4 is now available Duende BFFv4 is now available Securing OpenAPI and Swagger UI with OAuth in .NET 10 Building a Federation Gateway with Duende IdentityServer: Strategies and Considerations for Identity Orchestration
OAuth 2.1 Made Simple: The Only Flows You Need
Khalid Abuhakmeh · 2026-05-06 · via Duende Software Official Site

Back in 2019, Dominick Baier, Duende Cofounder and Security subject-matter expert, wrote a prophetic post called "Two is the Magic Number", a riff on De La Soul's "Three is the Magic Number", arguing that you only needed two OAuth flows to cover every real-world scenario. At the time, it was a bold simplification. OAuth 2.0 had shipped with a sprawl of grant types: Implicit, Resource Owner Password Credentials, Authorization Code without PKCE, Client Credentials, and the ecosystem dutifully built tutorials for all of them. The result was confusion. Developers picked the wrong flow, shipped insecure apps, and blamed OAuth itself for being "too complicated."

Dominick was right, and the standards body agreed. OAuth 2.1 formally removed the footguns, mandated PKCE on every authorization code grant, and left us with a protocol that is dramatically simpler to learn and harder to misuse. If you're building on .NET 10 in 2026, this is the only article you need. Two flows cover almost everything. A third handles the edge case. Let's go.

Flow 1: Client Credentials

For: Server-to-server communication (no user involved)

Sometimes software talks to other software, and no human is anywhere near the conversation. A nightly batch job pulls records from an API. A shipping microservice notifies an inventory microservice that stock levels have changed. A background worker processes a queue. In all of these cases, the service itself is the identity — it acts on its own behalf, not on behalf of a user. That's where Client Credentials flow comes in.

The flow is dead simple. Your service presents its credentials to the token service and gets back an access token. No browser redirects, no user interaction, no PKCE. Just a direct, back-channel HTTP POST. The token service verifies the client's identity and returns a token scoped to whatever permissions that client has been granted.

How the client proves its identity is worth a quick mention. The most common method is a shared secret: a client_id and client_secret sent in the request body or as a Basic auth header. For higher-security environments, you can use private_key_jwt, where the client signs a JWT assertion with its private key and the token service validates it against the registered public key. The strongest option is mutual TLS (mTLS), where the client authenticates at the transport layer using an X.509 certificate. All three work with Client Credentials; choose the one most applicable to your threat model.

sequenceDiagram
    participant Service A
    participant Token Service
    Service A->>Token Service: client_id + client_secret
    Token Service->>Service A: access_token

Csharp

// .NET 10 — requesting a client credentials token
var client = new HttpClient();

var response = await client.RequestClientCredentialsTokenAsync(
    new ClientCredentialsTokenRequest
    {
        Address = "https://identity.example.com/connect/token",
        ClientId = "service-a",
        ClientSecret = "secret",
        Scope = "api1.read api1.write"
    });

var accessToken = response.AccessToken;

When to use: background services calling APIs, microservice-to-microservice communication, daemon processes. Any scenario where no human user is involved.

Flow 2: Authorization Code + PKCE

For: Everything involving a user (web apps, native apps, SPAs via BFF)

If a human is logging in, this is your flow. Full stop. It doesn't matter whether you're building a server-rendered Razor Pages app, a native iOS client, a desktop application, or a single-page app behind a backend-for-frontend. Authorization Code with PKCE is the universal answer for interactive authentication in OAuth 2.1. The old world gave you choices: Implicit for SPAs, Resource Owner Password Credentials (ROPC), and if you "trusted" the client, plain Authorization Code for server apps. OAuth 2.1 collapsed all of that into a single flow and made PKCE mandatory. Less choice, more security.

Here's how it works. Your application redirects the user to the identity provider's authorization endpoint. The user authenticates by entering credentials, completing MFA, and fulfilling any required policy, and the identity provider redirects back to your application with a short-lived authorization code. Your application then exchanges that code for tokens via a direct back-channel call to the token endpoint. The user's credentials never touch your application. The tokens never pass through the browser's address bar. That's the whole point.

Proof Key for Code Exchange (PKCE, pronounced "pixie," RFC 7636) adds a critical layer of protection to this exchange. Before starting the flow, your application generates a random string called a code_verifier and derives a code_challenge from it using SHA-256. The challenge is sent along with the initial authorization request. When your app later exchanges the authorization code for tokens, it sends the original verifier. The token service hashes the verifier, compares it to the stored challenge, and only issues tokens if they match. This prevents an attacker who intercepts the authorization code — through a malicious app registered on the same custom URI scheme, a compromised redirect, or any other vector — from using it. Without the original verifier, the code is worthless. OAuth 2.1 requires PKCE on every authorization code grant, and .NET enables it by default.

sequenceDiagram
    participant User/Browser
    participant App
    participant Identity Provider
    User/Browser->>App: Initiate login
    App->>Identity Provider: Authorization request + code_challenge
    Identity Provider->>User/Browser: Login page
    User/Browser->>Identity Provider: Authenticate
    Identity Provider->>App: Authorization code
    App->>Identity Provider: Exchange code + code_verifier (back-channel)
    Identity Provider->>App: Tokens

Csharp

// .NET 10 — configuring OIDC with Code + PKCE
builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = "cookie";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("cookie")
    .AddOpenIdConnect("oidc", options =>
    {
        options.Authority = "https://identity.example.com";
        options.ClientId = "web-app";
        options.ClientSecret = "secret";
        options.ResponseType = "code";        // Authorization Code Flow
        options.UsePkce = true;               // PKCE (default in .NET)
        options.SaveTokens = true;
        
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("api1.read");
    });

When to use: server-side web applications (MVC, Razor Pages, Blazor Server), native mobile apps (iOS, Android), desktop applications, SPAs via the BFF pattern. Basically, any app that lets a human log in.

Note: It is currently the industry BCP to secure all client-side web applications built with JavaScript using the Backend-For-Frontend pattern. If your client-side JavaScript apps store tokens in the browser, you are putting both tokens and users at risk.

Flow 3: Device Authorization (Bonus)

For: Devices without browsers or with limited input (TVs, IoT, CLI tools)

Some devices don't have a browser or a decent keyboard. Think of a smart TV, a gaming console, a CLI tool running in a headless environment, or an IoT sensor with a tiny screen. You can't redirect a user to a login page that doesn't exist. Device Authorization (RFC 8628) solves this with a decoupled pattern. The device requests a user code and a verification URL from the token service, then displays both to the user. For example, like "Go to https://login.example.com/device and enter code ABCD-1234." The user picks up their phone or laptop, navigates to that URL, enters the code, and authenticates normally. Meanwhile, the device polls the token endpoint at a specified interval, receiving authorization_pending responses until the user completes the flow, at which point it gets back an access token. It's simple, secure, and doesn't require the constrained device to render a login UI.

sequenceDiagram
    participant TV/IoT Device
    participant Token Service
    participant User's Phone
    TV/IoT Device->>Token Service: Request device code
    Token Service->>TV/IoT Device: device_code + user_code + verification_url
    TV/IoT Device->>User's Phone: Display: "Go to https://login.example.com/device<br/>Enter code: ABCD-1234"
    User's Phone->>Token Service: Navigate to URL and enter code
    User's Phone->>Token Service: Authenticate and approve
    loop Polling
        TV/IoT Device->>Token Service: Poll with device_code
        Token Service-->>TV/IoT Device: authorization_pending
    end
    Token Service->>TV/IoT Device: access_token (when user completes)

When to use: smart TVs, gaming consoles, CLI tools, IoT devices. Anything without a browser or a usable keyboard.

The OAuth 2.1 Decision Tree

Picking the right flow takes about five seconds. Start with one question: Is a human user involved? If not, use Client Credentials. If yes, ask whether the device has a browser. If it does, use Authorization Code with PKCE, regardless of whether it's a server-side web app, a native mobile app, or an SPA behind a BFF. If there's no browser, use Device Authorization. That's the entire decision tree.

flowchart TD
    A[Is a human user involved?] -->|NO| B[Client Credentials Flow]
    A -->|YES| C[Does the device have a browser?]
    C -->|YES| D[Authorization Code + PKCE]
    C -->|NO| E[Device Authorization Flow]
    D --> F[Server-side app?]
    D --> G[Native app?]
    D --> H[SPA?]
    F --> I[Direct OIDC integration]
    G --> J[System browser + PKCE AppAuth]
    H --> K[BFF pattern - server does the OIDC]

What OAuth 2.1 Removed (and Why?)

You don't need to learn these concepts or ideas unless you're in the process of migrating away from them. If you see them in old tutorials, ignore them. But if you're curious about why they were cut, here's the short version.

Implicit Flow was designed for SPAs in an era when browsers couldn't make cross-origin POST requests. It returned access tokens directly in the URL fragment. That meant tokens showed up in browser history, got leaked through referrer headers, and landed in server logs. There was no mechanism for refresh tokens, no way to bind tokens to specific clients, and an enormous attack surface. The moment CORS became universally supported, the rationale for Implicit evaporated. Authorization Code with PKCE does everything Implicit did, without exposing tokens in URLs.

Resource Owner Password Credentials (ROPC) asked users to type their username and password directly into the client application. This fundamentally defeats the purpose of OAuth, which exists to keep user credentials away from third-party apps. ROPC couldn't support multi-factor authentication or federated login, and it trained users to hand their credentials to apps that shouldn't have them. It was a migration crutch for legacy systems, and it overstayed its welcome.

Authorization Code without PKCE was the original server-side flow, and it “worked” fine until it didn't. On mobile platforms, multiple apps can register the same custom URI scheme, which means a malicious app could intercept the authorization code during the redirect. Without PKCE, that intercepted code could be exchanged for real tokens. PKCE closes this gap entirely by binding the code to a cryptographic proof that only the original client possesses. OAuth 2.1 no longer offers a "without PKCE" option. PKCE is just how authorization code grants work now.

Beyond choosing the right flow, OAuth 2.1 also benefits from complementary hardening mechanisms. Refresh tokens require careful lifecycle management, including rotation, replay detection, and sender-constraining. And for high-security environments, proof-of-possession tokens (DPoP and mTLS) bind access tokens to the client that requested them, ensuring stolen tokens are useless without the corresponding private key.

Bonus: Token Exchange (RFC 8693)

Token Exchange isn't a user-facing flow. It's plumbing for microservice architectures. Picture this: a user makes a request to your API gateway, which holds an access token issued to the user. The gateway needs to call a downstream billing service, but the billing service requires a token with a narrower scope and a different audience. Instead of passing the original token along (over-privileged and insecure) or falling back to Client Credentials (which loses the user context entirely), the gateway uses Token Exchange (RFC 8693) to swap the user's token for a new one scoped specifically to the billing service. The token service mints a constrained token that carries the user's identity but is only valid for the downstream resource. Delegation without credential sharing.

Bonus: Client-Initiated Backchannel Authentication (CIBA)

CIBA flips the authentication model on its head. Instead of the user initiating a login, the application asks the identity provider to authenticate the user on a separate device. Think of a bank teller processing a large withdrawal: the teller's application sends a CIBA request to the identity provider, which pushes a notification to the customer's phone stating, "Do you approve a $5,000 withdrawal at the downtown branch?" The customer taps approve, the identity provider issues tokens back to the teller's application, and the transaction proceeds. The same pattern works for helpdesk scenarios, step-up authentication, and any flow where the person authorizing the action isn't sitting at the device that initiated it. See the CIBA specification for the full details.

Setting Up Your Token Service

Here's a Duende IdentityServer v8 configuration that supports all three core flows. Note that RequirePkce is set to true on every interactive client. OAuth 2.1 mandates it, and your token service should enforce it.

Csharp

// Program.cs — Duende IdentityServer v8 on .NET 9
builder.Services.AddIdentityServer()
    .AddInMemoryApiScopes(new[]
    {
        new ApiScope("api1.read", "Read access to API 1"),
        new ApiScope("api1.write", "Write access to API 1"),
    })
    .AddInMemoryApiResources(new[]
    {
        new ApiResource("api1", "API 1")
        {
            Scopes = { "api1.read", "api1.write" }
        }
    })
    .AddInMemoryIdentityResources(new[]
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
    })
    .AddInMemoryClients(new[]
    {
        // 1. Confidential web app — Authorization Code + PKCE
        new Client
        {
            ClientId = "web-app",
            ClientName = "Server-Side Web Application",
            AllowedGrantTypes = GrantTypes.Code,
            RequirePkce = true,
            ClientSecrets = { new Secret("web-app-secret".Sha256()) },
            RedirectUris = { "https://webapp.example.com/signin-oidc" },
            PostLogoutRedirectUris = { "https://webapp.example.com/signout-callback-oidc" },
            AllowedScopes = { "openid", "profile", "api1.read" },
            AllowOfflineAccess = true   // enables refresh tokens
        },

        // 2. Native public client — Authorization Code + PKCE (no secret)
        new Client
        {
            ClientId = "native-app",
            ClientName = "Native Mobile / Desktop App",
            AllowedGrantTypes = GrantTypes.Code,
            RequirePkce = true,
            RequireClientSecret = false, // public client
            RedirectUris = { "com.example.nativeapp://callback" },
            PostLogoutRedirectUris = { "com.example.nativeapp://signout" },
            AllowedScopes = { "openid", "profile", "api1.read", "api1.write" },
            AllowOfflineAccess = true
        },

        // 3. Device client — Device Authorization Flow
        new Client
        {
            ClientId = "device-app",
            ClientName = "TV / IoT Device",
            AllowedGrantTypes = GrantTypes.DeviceFlow,
            RequireClientSecret = false, // device clients are typically public
            AllowedScopes = { "openid", "profile", "api1.read" },
            AllowOfflineAccess = true
        }
    });

Three client definitions, three scenarios, one token service. The confidential web app uses a shared secret and obtains refresh tokens to maintain long-lived sessions. The native app is a public client — no secret, because mobile and desktop binaries can't keep secrets — and relies entirely on PKCE for security. The device client uses Device Authorization Flow and is also public. Each client gets only the scopes it needs.

Conclusion

OAuth 2.1 did the hard work of saying "no" so you don't have to. Client Credentials for machines. Authorization Code with PKCE for humans. Device Authorization when there's no browser. Token Exchange and CIBA are there when you need delegation or decoupled authentication. That's the whole protocol, and it fits on an index card. Stop reading OAuth tutorials. Go build something.