Pushed Authorization Requests in .NET 9: Why and How to Use Them
You may have heard that .NET 9.0 brings Pushed Authorization Requests (PAR) support in ASP.NET Core. If you are wondering what the heck Pushed Authorization Requests are, why you should use this feature, and how to use it in your .NET applications, well, this article is for you. PAR enhances the security posture of your ASP.NET Core application that uses OpenID Connect and OAuth 2.0. You should care about this security improvement, especially if you build applications for the financial, healthcare, insurance, and other data-sensitive sectors, where it is a requirement. Before you start, you need some basic knowledge about OpenID Connect and OAuth 2.0. You don’t need to be an expert; just a high-level knowledge of these protocols is enough to understand why and how PAR works. The Basic OpenID Connect Flow Let’s start by recalling how a basic OpenID Connect (OIDC) flow works. By basic flow, I mean a flow involving a classic server-rendered web application, i.e., in the context of .NET, a classic ASP.NET Core MVC application, a Razor Pages application, or a Blazor Server application. In the scenario involving this type of application, the flow we use is the Authorization Code flow, which we are going to describe using this diagram: In the scenario illustrated by the diagram, we have: A browser, which is the tool used by the user to access our web application. The web application, called client application in the diagram because it’s a client of the authorization server in the OAuth 2.0 and OIDC context. The authorization server, which is responsible for authenticating the user and authorizing the application. The API server, which is the protected server our web application wants to access using an access token. Following the numbered arrows, we can describe the flow as follows: The user uses their browser to access the web application. The application finds that the user is not authenticated and builds an authorization request for the browser; then, it redirects the browser with this authorization request to the authorization server for authentication. The browser sends the authorization request to the authorization endpoint of the authorization server, and the user authenticates. As a result of the authentication, the browser receives an authorization code from the authorization server. The browser provides the authorization code to the web application. The web application calls the token endpoint of the authorization server and provides the authorization code. As a response, the authorization server returns an ID and access tokens. The web application uses the ID token to get information about the user and the access token to call the protected API. This is a simplified description of the OIDC flow for authenticating the user and authorizing the web application to access the protected API server. Using OIDC in ASP.NET Core As you can see from the previous section, there are many moving parts behind the user authentication and application authorization with OIDC. In .NET, you don’t need to take care of all these details. What you usually need to do is to correctly configure the OpenID Connect middleware. In practice, the following code in your Program.cs file works the magic in the basic scenario: builder.services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect("Auth0", options => { options.Authority = $"https://{builder.Configuration["Auth0:Domain"]}"; options.ClientId = builder.Configuration["Auth0:ClientId"]; options.ClientSecret = builder.Configuration["Auth0:ClientSecret"]; options.ResponseType = "code"; }); The code snippet gets the configuration parameters of the authorization server — Auth0 in the example — from the appsettings.json file, or even better, from environment variables. You don’t need to do anything else to enable your web application to get the ID and access token. Everything happens behind the scenes! Some Potential Issues You probably already knew all this, but maybe you never wondered what problems can arise with this well-proven flow. There are two points in the OIDC flow that can have potential security and privacy issues, as highlighted in the following diagram: The first point is when the web application redirects the browser to the authorization server to authenticate the user. The second point is when the browser calls the authorization endpoint of the authorization server. Both points are related to the authorization request prepared by the web application and sent by the browser to the authorization server. The main issues that can affect the authorization request are: The authorization request could be altered. Usually, the web application builds the authorization request on behalf of the browser and th
You may have heard that .NET 9.0 brings Pushed Authorization Requests (PAR) support in ASP.NET Core. If you are wondering what the heck Pushed Authorization Requests are, why you should use this feature, and how to use it in your .NET applications, well, this article is for you.
PAR enhances the security posture of your ASP.NET Core application that uses OpenID Connect and OAuth 2.0. You should care about this security improvement, especially if you build applications for the financial, healthcare, insurance, and other data-sensitive sectors, where it is a requirement.
Before you start, you need some basic knowledge about OpenID Connect and OAuth 2.0. You don’t need to be an expert; just a high-level knowledge of these protocols is enough to understand why and how PAR works.
The Basic OpenID Connect Flow
Let’s start by recalling how a basic OpenID Connect (OIDC) flow works. By basic flow, I mean a flow involving a classic server-rendered web application, i.e., in the context of .NET, a classic ASP.NET Core MVC application, a Razor Pages application, or a Blazor Server application. In the scenario involving this type of application, the flow we use is the Authorization Code flow, which we are going to describe using this diagram:
In the scenario illustrated by the diagram, we have:
- A browser, which is the tool used by the user to access our web application.
- The web application, called client application in the diagram because it’s a client of the authorization server in the OAuth 2.0 and OIDC context.
- The authorization server, which is responsible for authenticating the user and authorizing the application.
- The API server, which is the protected server our web application wants to access using an access token.
Following the numbered arrows, we can describe the flow as follows:
- The user uses their browser to access the web application. The application finds that the user is not authenticated and builds an authorization request for the browser; then, it redirects the browser with this authorization request to the authorization server for authentication.
- The browser sends the authorization request to the authorization endpoint of the authorization server, and the user authenticates. As a result of the authentication, the browser receives an authorization code from the authorization server.
- The browser provides the authorization code to the web application.
- The web application calls the token endpoint of the authorization server and provides the authorization code. As a response, the authorization server returns an ID and access tokens.
- The web application uses the ID token to get information about the user and the access token to call the protected API.
This is a simplified description of the OIDC flow for authenticating the user and authorizing the web application to access the protected API server.
Using OIDC in ASP.NET Core
As you can see from the previous section, there are many moving parts behind the user authentication and application authorization with OIDC. In .NET, you don’t need to take care of all these details. What you usually need to do is to correctly configure the OpenID Connect middleware. In practice, the following code in your Program.cs
file works the magic in the basic scenario:
builder.services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("Auth0", options =>
{
options.Authority = $"https://{builder.Configuration["Auth0:Domain"]}";
options.ClientId = builder.Configuration["Auth0:ClientId"];
options.ClientSecret = builder.Configuration["Auth0:ClientSecret"];
options.ResponseType = "code";
});
The code snippet gets the configuration parameters of the authorization server — Auth0 in the example — from the appsettings.json
file, or even better, from environment variables. You don’t need to do anything else to enable your web application to get the ID and access token. Everything happens behind the scenes!
Some Potential Issues
You probably already knew all this, but maybe you never wondered what problems can arise with this well-proven flow. There are two points in the OIDC flow that can have potential security and privacy issues, as highlighted in the following diagram:
The first point is when the web application redirects the browser to the authorization server to authenticate the user. The second point is when the browser calls the authorization endpoint of the authorization server. Both points are related to the authorization request prepared by the web application and sent by the browser to the authorization server.
The main issues that can affect the authorization request are:
- The authorization request could be altered. Usually, the web application builds the authorization request on behalf of the browser and then redirects the browser to the authorization server. A malicious actor, such as a browser extension or malware, can modify, add, or remove one or more parameters of this request before sending it to the authorization server.
- No guarantee of the request's provenance. As we said, the web application builds the authorization request on behalf of the browser. However, there is no guarantee that an authorization request was built by the web application. Anyone can build it once they know some basic data, such as the client ID and the redirect URI.
- No guarantee of confidentiality. Although the browser sends the authorization request to the authorization server via HTTPS, the request parameters can be intercepted by third-party applications, such as proxies, load balancers, or even browser plugins. A malicious network component of this type can inject or change the request parameters, not to mention that the request itself can be logged.
- Browser limitations. Finally, a very complex query string in the authorization request may incur possible browser limitations on URL length.
Entering Pushed Authorization Requests (PAR)
While the issues pointed out above may appear as non-crucial issues in ordinary authentication contexts, they become significant in contexts where privacy and data protection are paramount.
Here comes PAR, an extension of OAuth 2.0 (and then of OIDC) that enhances the protocol’s security level to solve those issues and protect the authorization request’s exposure.
We will not dive deep into the details of this extension. Read this article to learn more about how Pushed Authorization Requests work. Anyway, to have a high-level idea, let’s follow this new flow in this diagram:
- As usual, the user accesses the web application with their browser.
- The web application builds an authorization request, but this time, it sends the request to the authorization server using the new PAR endpoint instead of sending it to the browser. The web application uses the PAR endpoint to register the authorization request and get an identifier for this request (the request URI).
- Instead of the typical authorization request, the browser will get a URL with the request identifier as a parameter. No risk of sensitive detail exposure or modification this time; just an identifier saying nothing to a potential attacker.
- When the authorization server receives the request with the request URI from the browser, it retrieves the previously registered authorization request and processes it as if the browser sent it.
- From now on, the usual flow continues.
PAR guarantees more security and confidentiality when managing authorization requests. It’s part of a set of specifications that fall under the umbrella of FAPI, an OpenID initiative that aims to strengthen the protocols to make them compatible with the more restrictive needs typical of financial, insurance, healthcare, and governmental contexts, among other contexts.
How to Use PAR in .NET 9
Now that we have an idea of what PAR is and how it works let's see how to use it in .NET 9.
To add PAR support to your ASP.NET Core application, your OIDC configuration in Program.cs
should look like the following:
builder.services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("Auth0", options =>
{
options.Authority = $"https://{builder.Configuration["Auth0:Domain"]}";
options.ClientId = builder.Configuration["Auth0:ClientId"];
options.ClientSecret = builder.Configuration["Auth0:ClientSecret"];
options.ResponseType = "code";
});
Can you see the difference with the code shown earlier? No? Can't you see it?
Actually there is no difference! .NET supports PAR by default, with a fallback to the traditional flow in case the authorization server does not support PAR. That’s amazing, don’t you? You get more security with no additional code.
You may say, “If I don’t need to add code, why should I care about PAR and how it works?” Actually, you should be aware of what happens under the hood. When things go wrong, having an idea of what is happening at a lower level can be helpful for debugging and detecting the cause of an issue.
Assume you are used to checking the traditional OIDC flow when a problem with authentication arises. You migrate your application to .NET 9 without any need to change code and everything works fine. One day you have a problem with user authentication and analyze the HTTP traffic to try understanding what’s going on, and you don’t find the classic authorization request. I bet you'll have a hard time figuring out what happened, since you didn't change your code.
If you were already prepared, you wouldn't have been surprised by this new flow.
Controlling PAR Configuration
We said that PAR is supported by default in .NET 9. But what if you don’t want to use it? How can you tell your application to just use the classic OIDC flow? You can do it as follows:
options.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable;
The PushedAuthorizationBehavior
option allows you to configure PAR behavior. In the previous example, you disable it, forcing your application to fall back to the classic OIDC flow. You can also force your application to use PAR with the following settings:
options.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Require;
In this case, your application will use the PAR flow. If the authorization server does not support PAR, your application will not fall back to the classic OIDC flow but will throw an exception.
Finally, you can handle a new event, onPushAuthorization
, to customize the authorization request before it is sent to the authorization server. The following example shows how you can create a handler for this event:
options.Events = new OpenIdConnectEvents
{
OnPushAuthorization = async ctx =>
{
...
}
};
Summary
We started by analyzing the basic OIDC flow and discovered a few possible security and privacy issues with authorization requests. Then, we introduced PAR and learned how it works. We also learned that .NET 9 introduces PAR support for ASP.NET Core applications by default without requiring us to write code. However, we can configure and customize PAR behavior programmatically by using a couple of options.
I hope you now have a clearer understanding of what PAR is, why using it, and how to use it in ASP.NET Core starting with .NET 9.0.