29 March 2025
by

Tran Manh Hung
Understanding OAuth/OpenID Response Types in .NET Web APIs:
.NET
Cyber security

Have you ever wondered how applications let you "Log in with Google" or "Sign in with Microsoft" without asking you to create yet another username and password? In most cases, they use protocols called OAuth and OpenID.
Maybe you even heard about SAML or Kerberos, but that is another time...
One of the most important things to understand about OAuth is the response type, which is how your authentication information is delivered back to the application after you log in. Think of it as choosing between receiving a package by mail, pickup, or delivery—each method has its own benefits and security considerations.
Recently, I had to explain this concept to a few of my colleagues, and it seems most other articles were too many words :(
In this beginner-friendly article, we'll explore the three principal response types - query, fragment, and post - and how to implement them in .NET Web API controllers. We'll use simple examples, highlight the security aspects you need to know, and share best practices to help you build secure applications that your users can trust.
Understanding Response Types: The Basics
Before we dive into code, let's break down what these response types actually mean in simple terms:
1. Query Response Type (response_type=code
)
Think of this like a ticket exchange system. Instead of giving you the actual backstage pass (token) right away, the authorization server gives you a ticket stub (authorization code) that you can exchange for the actual pass later.
The code comes back in the URL as a query parameter (the part after the
?
symbol):Example URL:
1https://your-app.com/callback?code=ABC123XYZ
Why it matters: This is generally more secure because the actual tokens aren't exposed in the URL—instead, your server exchanges this temporary code for tokens in a separate, secure server-to-server communication.
Security note: While it's safer to pass a code rather than tokens in the URL, you should never use query parameters to return actual tokens directly. Why? Because:
- URLs can be stored in browser history
- web servers might log URLs
- URLs can be visible in referrer headers when linking to other sites
2. Fragment Response Type (response_type=token
or response_type=id_token token
)
This approach returns tokens directly in the URL fragment (the part after the
#
symbol).Example URL:
1https://your-app.com/callback#access_token=eyJhbGciOi...&token_type=Bearer&expires_in=3600
Why it works this way: The fragment part of a URL (after the #) is special - it never gets sent to the server! When a browser navigates to a URL with a fragment, everything after the # stays in the browser. This means your client-side JavaScript code must read and process these tokens.
This is a key security aspect: the tokens in the fragment aren't visible to the server, which prevents them from being logged in server logs. However, they are still visible in the browser and could be compromised if someone gains access to the user's browser history.
3. Post Response Type (response_mode=form_post
)
This is like getting your authentication information in a sealed envelope rather than written on a postcard. The authorization server sends tokens via an HTML form POST request directly to your callback URL.
Why it's better: Unlike query or fragment approaches that put sensitive information in the URL (where it might be seen or logged), the POST method puts tokens in the request body where they're more protected. They won't appear in:
- Browser history
- Server logs
- Referrer headers
Example:
The identity provider automatically generates and submits an HTML form with hidden fields containing your tokens, so the user doesn't even see this happening.
Implementing Response Types in .NET Web APIs
Let's see how to implement each of these response types in a .NET Web API controller. For these examples, we'll use the Microsoft.AspNetCore.Authentication.OpenIdConnect package.
Setting Up Dependencies
First, add the necessary packages to your .NET project:
1// .NET CLI
2dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
3dotnet add package Microsoft.Identity.Web
1. Query Response Type (Authorization Code Flow)
This is the most common and secure flow, ideal for server-side web applications.
1// Program.cs
2using Microsoft.AspNetCore.Authentication.OpenIdConnect;
3using Microsoft.Identity.Web;
4
5var builder = WebApplication.CreateBuilder(args);
6
7// Add authentication services
8builder.Services.AddAuthentication(options =>
9{
10 options.DefaultScheme = "Cookies";
11 options.DefaultChallengeScheme = "OpenIdConnect";
12})
13.AddCookie("Cookies", options =>
14{
15 options.Cookie.HttpOnly = true;
16 options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
17 options.Cookie.SameSite = SameSiteMode.Strict;
18})
19.AddOpenIdConnect("OpenIdConnect", options =>
20{
21 options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
22 options.ClientId = "your-client-id";
23 options.ClientSecret = "your-client-secret";
24 options.ResponseType = OpenIdConnectResponseType.Code;
25 options.ResponseMode = OpenIdConnectResponseMode.Query;
26 options.SaveTokens = true;
27 options.GetClaimsFromUserInfoEndpoint = true;
28 options.Scope.Add("openid");
29 options.Scope.Add("profile");
30 options.Scope.Add("api://your-api-scope/access");
31});
32
33// Add controller services
34builder.Services.AddControllers();
35
36var app = builder.Build();
37
38// Configure the app
39app.UseAuthentication();
40app.UseAuthorization();
41app.MapControllers();
42
43app.Run();
Let's create a controller to handle the authentication flow:
1// AuthController.cs
2using Microsoft.AspNetCore.Authentication;
3using Microsoft.AspNetCore.Authentication.Cookies;
4using Microsoft.AspNetCore.Authentication.OpenIdConnect;
5using Microsoft.AspNetCore.Mvc;
6using System.Threading.Tasks;
7
8namespace YourApp.Controllers;
9
10[ApiController]
11[Route("[controller]")]
12public class AuthController : ControllerBase
13{
14 [HttpGet("signin")]
15 public IActionResult SignIn()
16 {
17 return Challenge(new AuthenticationProperties
18 {
19 RedirectUri = "/"
20 }, OpenIdConnectDefaults.AuthenticationScheme);
21 }
22
23 [HttpGet("signout")]
24 public IActionResult SignOut()
25 {
26 return SignOut(
27 new AuthenticationProperties { RedirectUri = "/" },
28 CookieAuthenticationDefaults.AuthenticationScheme,
29 OpenIdConnectDefaults.AuthenticationScheme);
30 }
31
32 [HttpGet("callback")]
33 public async Task<IActionResult> Callback()
34 {
35 // The authorization code is automatically processed by the middleware
36 // If you're here, authentication was successful
37
38 // You can access tokens from the AuthenticationProperties
39 var authenticateResult = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
40 var accessToken = authenticateResult.Properties.GetTokenValue("access_token");
41
42 return Redirect("/");
43 }
44}
2. Fragment Response Type (Implicit Flow)
The fragment response type is typically used in single-page applications (SPAs) where the tokens are returned directly to the browser. However, this flow is now considered less secure and has been largely replaced by the authorization code flow with PKCE for SPAs.
1// Program.cs
2builder.Services.AddAuthentication(options =>
3{
4 options.DefaultScheme = "Cookies";
5 options.DefaultChallengeScheme = "OpenIdConnect";
6})
7.AddCookie("Cookies")
8.AddOpenIdConnect("OpenIdConnect", options =>
9{
10 options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
11 options.ClientId = "your-client-id";
12 options.ResponseType = OpenIdConnectResponseType.IdTokenToken;
13 options.ResponseMode = OpenIdConnectResponseMode.Fragment;
14 options.SaveTokens = true;
15 options.Scope.Add("openid");
16 options.Scope.Add("profile");
17 options.Scope.Add("api://your-api-scope/access");
18
19 // Note: No client secret is used in implicit flow
20});
For fragment response type, most of the token handling happens client-side in JavaScript. Here's a simplified example of how you might handle the callback in a SPA:
1// JavaScript in your SPA
2function handleCallback() {
3 if (window.location.hash) {
4 // Parse the fragment
5 const fragmentParams = new URLSearchParams(
6 window.location.hash.substring(1)
7 );
8
9 const accessToken = fragmentParams.get("access_token");
10 const idToken = fragmentParams.get("id_token");
11
12 if (accessToken) {
13 // Store the token (preferably in a secure way)
14 sessionStorage.setItem("access_token", accessToken);
15
16 // Redirect to your application's main page
17 window.location.href = "/";
18 }
19 }
20}
21
22// Call the function when the page loads
23window.onload = handleCallback;

Do you need a reliable partner in tech for your next project?
3. Post Response Type (Form Post Response Mode)
The post response type is a more secure alternative to the fragment response type for browser-based applications, as it doesn't expose tokens in the URL.
1// Program.cs
2builder.Services.AddAuthentication(options =>
3{
4 options.DefaultScheme = "Cookies";
5 options.DefaultChallengeScheme = "OpenIdConnect";
6})
7.AddCookie("Cookies")
8.AddOpenIdConnect("OpenIdConnect", options =>
9{
10 options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
11 options.ClientId = "your-client-id";
12 options.ClientSecret = "your-client-secret";
13 options.ResponseType = OpenIdConnectResponseType.Code;
14 options.ResponseMode = OpenIdConnectResponseMode.FormPost;
15 options.SaveTokens = true;
16 options.GetClaimsFromUserInfoEndpoint = true;
17 options.Scope.Add("openid");
18 options.Scope.Add("profile");
19 options.Scope.Add("api://your-api-scope/access");
20});
With form post, you need to define an endpoint that can accept POST requests:
1// AuthController.cs
2[HttpPost("callback")]
3public async Task<IActionResult> CallbackPost()
4{
5 // The form post is automatically processed by the middleware
6 // Similar to the query response type handling
7
8 var authenticateResult = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
9 var accessToken = authenticateResult.Properties.GetTokenValue("access_token");
10
11 return Redirect("/");
12}
When to Use Each Response Type
Choosing the right response type depends on your application architecture and security requirements:
1. Query Response Type (Authorization Code Flow)
- Best for: Server-side web applications
- Why: It's the most secure flow as it keeps tokens server-side
- Enhanced security: Often used with PKCE (Proof Key for Code Exchange) for added security
- Newer enhancements in .NET: In .NET 9/10, the middleware has improved PKCE support
2. Fragment Response Type (Implicit Flow)
- Best for: Legacy single-page applications (SPAs)
- Why: Historically used when SPAs couldn't securely store client secrets
- Important note: This flow is now discouraged by security experts and the OAuth working group
- Modern alternative: Authorization code flow with PKCE is now recommended even for SPAs
3. Post Response Type (Form Post Response Mode)
- Best for: Browser-based applications where you want to avoid exposing tokens in URLs
- Why: More secure than fragment as tokens aren't visible in browser history or logs
- Hybrid scenarios: Often used in hybrid flows combining authorization code and implicit flows
Security Concerns and Mitigations
Each response type comes with its own security considerations:
Query Response Type
- Concern: Authorization codes in URL can be logged in server logs
- Mitigation: Short-lived codes + PKCE extension
- In .NET: Enable PKCE by setting
options.UsePkce = true;
1// Enabling PKCE in .NET
2options.UsePkce = true;
Fragment Response Type
- Concern: Tokens directly exposed in the browser
- Concern: Vulnerable to XSS attacks
- Mitigation: Consider switching to authorization code flow with PKCE
- If you must use it: Implement strong Content Security Policy (CSP) headers
1// Adding CSP headers in .NET
2app.Use(async (context, next) =>
3{
4 context.Response.Headers.Add(
5 "Content-Security-Policy",
6 "default-src 'self'; script-src 'self'; object-src 'none'");
7 await next();
8});
Post Response Type
- Concern: CSRF attacks if not properly protected
- Mitigation: Anti-forgery tokens
- In .NET: Use the built-in anti-forgery features
1// Add anti-forgery token validation
2builder.Services.AddAntiforgery(options =>
3{
4 options.HeaderName = "X-XSRF-TOKEN";
5 options.Cookie.Name = "XSRF-TOKEN";
6 options.Cookie.SameSite = SameSiteMode.Strict;
7 options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
8});
9
10// In your controller
11[ValidateAntiForgeryToken]
12[HttpPost("callback")]
13public async Task<IActionResult> CallbackPost()
14{
15 // ...
16}
Best Practices
Regardless of which response type you choose, here are some best practices to enhance the security of your OAuth/OpenID implementation:
1. Secure Cookie Handling
Always use secure, HttpOnly, and SameSite cookies to store authentication state:
1// Configure secure cookies
2services.Configure<CookiePolicyOptions>(options =>
3{
4 options.MinimumSameSitePolicy = SameSiteMode.Strict;
5 options.HttpOnly = HttpOnlyPolicy.Always;
6 options.Secure = CookieSecurePolicy.Always;
7});
8
9// Configure authentication cookies
10services.AddAuthentication(options =>
11{
12 // ...
13})
14.AddCookie(options =>
15{
16 options.Cookie.HttpOnly = true;
17 options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
18 options.Cookie.SameSite = SameSiteMode.Strict;
19});
2. Implement Proper Token Validation
Always validate tokens on your server before trusting them:
1// Add JWT Bearer token validation
2builder.Services.AddAuthentication()
3 .AddJwtBearer(options =>
4 {
5 options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
6 options.Audience = "your-client-id";
7 options.TokenValidationParameters = new TokenValidationParameters
8 {
9 ValidateIssuer = true,
10 ValidateAudience = true,
11 ValidateLifetime = true,
12 ValidateIssuerSigningKey = true,
13 ClockSkew = TimeSpan.FromMinutes(5)
14 };
15 });
3. Use Authorization Code Flow with PKCE for All Clients
Even for SPAs, the authorization code flow with PKCE is now recommended:
1// For SPAs using auth code flow with PKCE
2builder.Services.AddAuthentication()
3 .AddOpenIdConnect(options =>
4 {
5 options.ResponseType = OpenIdConnectResponseType.Code;
6 options.UsePkce = true;
7 // Other options...
8 });
4. Implement Proper Error Handling
Handle authentication errors gracefully to avoid revealing sensitive information:
1// Configure error handling
2options.Events = new OpenIdConnectEvents
3{
4 OnAuthenticationFailed = context =>
5 {
6 context.HandleResponse();
7 context.Response.Redirect("/error?message=Authentication_failed");
8 return Task.CompletedTask;
9 },
10 OnRemoteFailure = context =>
11 {
12 context.HandleResponse();
13 context.Response.Redirect("/error?message=Remote_authentication_failed");
14 return Task.CompletedTask;
15 }
16};
5. Implement Rate Limiting
Protect your authentication endpoints from brute force attacks:
1// Add rate limiting in .NET 9/10
2builder.Services.AddRateLimiter(options =>
3{
4 options.AddPolicy("auth", httpContext =>
5 RateLimitPartition.GetFixedWindowLimiter(
6 partitionKey: httpContext.Connection.RemoteIpAddress?.ToString(),
7 factory: _ => new FixedWindowRateLimiterOptions
8 {
9 PermitLimit = 10,
10 Window = TimeSpan.FromMinutes(1)
11 }));
12});
13
14// Apply rate limiting to authentication endpoints
15app.UseRateLimiter();
16
17// In your controller
18[EnableRateLimiting("auth")]
19[HttpGet("signin")]
20public IActionResult SignIn()
21{
22 // ...
23}
Conclusion: Choosing Your Authentication Path
Authentication might seem like a complex maze of options and security concerns, but it doesn't have to be overwhelming. Think of response types as different paths to the same destination - a secure, authenticated user experience.
The query response type (authorization code flow) with PKCE is like taking the highway - it's well-traveled, widely supported, and has good security checkpoints along the way. For most applications, including modern SPAs, this is your best route.
The fragment response type (implicit flow) is more like an old country road. It served its purpose in the past, but now there are better alternatives with fewer security potholes. Consider this path only if you're maintaining legacy applications that require it.
The post-response type is the express lane that balances security and convenience - keeping your sensitive data protected in the request body rather than exposed in URLs.
Remember: authentication is not just about getting users into your application but building trust. Each time a user clicks "Login," they put their digital identity in your hands. By choosing the right response type and following security best practices, you're not just implementing a feature - you're making a promise to protect your users.
The world of authentication is constantly evolving, and staying informed about the latest security practices is one of the most valuable investments you can make as a developer. Your users might never notice all the careful decisions you've made to protect them - and that's exactly how it should be.
Additional Resources
Let’s stay connected
Do you want the latest and greatest from our blog straight to your inbox? Chuck us your email address and get informed.
You can unsubscribe any time. For more details, review our privacy policy