When working with user authentication in ASP.NET Core, you may encounter situations where you need to pass additional information through the authentication process. For example, you might want to track specific user actions, ensure they are redirected to a particular page after logging in, or pass custom parameters to an identity provider.
The AuthenticationProperties class provides an elegant solution for these scenarios. It allows you to carry state
through call sites within a specific request, maintain state throughout the authentication process, or pass additional
parameters to the identity provider you are using.
In this post, we'll see how to use the AuthenticationProperties class effectively in your ASP.NET Core applications
and explore some OpenID Connect-specific options you can use in your apps.
What is AuthenticationProperties?
The
AuthenticationProperties class in ASP.NET Core
is a simple yet powerful way to pass additional state or metadata during the authentication process.
While it has several properties like AllowRefresh, ExpiresUtc, IssuedUtc, IsPersistent, and RedirectUri that
reflect various characteristics of an authentication session, these values are backed by two dictionaries:
Items- Contains state values about the authentication session and is carried throughout the authentication process.Parameters- Contains parameters passed to the authentication handler and used for flowing data between call sites in the same request.
Let's look at some examples of where and how they can be used!
Using AuthenticationProperties.Items in your application
The Items dictionary in AuthenticationProperties is designed to store state values that persist throughout the
authentication process. This can be useful for scenarios where you need to track custom data related to the
authentication session, such as user-specific metadata or temporary flags.
For example, suppose you are building a weather app, and the user selects a weather location before signing in. You can
store this information in the Items dictionary and retrieve it later after the user logs in:
var items = new Dictionary<string, string?>
{
["weather_location"] = "Antwerp, Belgium"
};
return Challenge(
properties: new AuthenticationProperties(items)
{
RedirectUri = "/home"
},
authenticationSchemes: "oidc");
In this example, the Challenge method initiates the authentication process using the "oidc" scheme. A custom key/value
pair ("weather_location": "Antwerp, Belgium") is added to the Items dictionary, which will be carried along through
the authentication flow.
Later, after the user logs in, you can access this data by inspecting the AuthenticationProperties associated with the
current user, for example, in Razor Pages. Note that the method name AuthenticateAsync may be misleading: it is used
to retrieve information for the current user and not to trigger authentication (which would be done using the
ChallengeAsync method).
public class WeatherModel : PageModel
{
public string WeatherLocation { get; private set; }
public async Task<IActionResult> OnGet()
{
var authResult = await HttpContext.AuthenticateAsync();
var authProperties = authResult?.Properties;
if (authProperties?.Items.TryGetValue("weather_location", out var weatherLocation) == true)
{
Weather = RetrieveWeatherForLocationAsync(weatherLocation);
}
return Page();
}
}
Nice! This approach allows you to transfer the user's selected weather location through the authentication process and use it in your Razor Pages to enhance the user experience in your weather app.
Note that while the Items dictionary is a convenient way to pass small amounts of data through the authentication
process, it is not intended to serve as a session storage mechanism. The data stored in the Items dictionary is often
roundtripped to the identity provider, which in may have a limit on the size of this parameter. If you need to store and
carry more than one or a few key-value pairs through the authentication flow, consider using other storage mechanisms to
keep the state, such as sessions or TempData.
External identity providers will not be able to see the contents of the Items dictionary. While data is serialized and
typically added to the state query string parameter, it is encrypted
using ASP.NET Core Data Protection and
only readable by your own application. We recommend always configuring Data Protection in your client applications!
The state parameter may be part of the query string sent to the authorization endpoint, but if you are
using Pushed Authorization Requests (PAR) it will be
sent to the identity provider using a back channel to prevent authorization parameters being seen or tampered with -
even if they are encrypted by default.
Using AuthenticationProperties.Parameters in your application
The Parameters dictionary in AuthenticationProperties is used to pass additional data to the authentication handler
during the authentication process. This is particularly useful when you need to send custom parameters to an external
identity provider or modify the behavior of the authentication flow.
For example, suppose you are integrating with an OpenID Connect provider and want to pre-fill the username field on the
login page by passing a login_hint parameter. You can achieve this by adding the parameter to the Parameters
dictionary:
var parameters = new Dictionary<string, object?>
{
["login_hint"] = "user@example.com"
};
return Challenge(
properties: new AuthenticationProperties(items: null, parameters: parameters)
{
RedirectUri = "/profile",
},
authenticationSchemes: "oidc");
In this example, the Challenge method initiates the authentication process using the "oidc" scheme. The login_hint
parameter is added to the Parameters dictionary, which is then passed along to any authentication events or handlers
in your ASP.NET Core pipeline. For example, you could consume the login_hint parameter in the
OnRedirectToIdentityProvider of your OpenIdConnectOptions:
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.Name = "AcmeCorp.WeatherApp";
})
.AddOpenIdConnect(options =>
{
// ...
options.Events.OnRedirectToIdentityProvider = async context =>
{
if (context.Properties.Parameters.TryGetValue("login_hint", out var loginHint)
{
Logger.LogInformation("Setting login hint to {LoginHint}", loginHint);
}
await Task.FromResult(0);
};
// ...
});
Note that the Parameters dictionary is available in the event callback, but changes you make to this collection will
not be reflected in the redirect to the identity provider. As we'll see later in this post, you'll need to use the
ProtocolMessage property on the context object here to update any query parameters sent to the identity provider.
The identity provider can use the login_hint parameter to pre-fill the username field on the login page, improving the
user experience by reducing the information the user needs to enter manually. If you're using Duende IdentityServer,
you'll find the UI templates come
with a login page in which the login hint is consumed from the authorization request parameters:
private async Task BuildModelAsync(string? returnUrl)
{
// ...
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
{
// ...
Input.Username = context.LoginHint;
// ...
}
// ...
}
The authorization context here has convenience properties for many known parameters like tenant (in the acr
parameter), login_hint, prompt modes, and more. Custom parameters can be accessed using the Parameters property:
var customParameter = context.Parameters["custom-parameter"];
Keep in mind that the Parameters dictionary is not encrypted or protected like the Items dictionary. It is intended
to pass data between your application and the authentication handler, and its contents may be visible to external
systems. Use it only for non-sensitive data or data required by the identity provider.
Note that while the Parameters dictionary is sent to the identity provider for the OpenIdConnectHandler, this may
not be true for all authentication handlers available in ASP.NET Core. An example is the GoogleHandler,
which only sends specific well-known parameters
such as login_hint, prompt, and approval_prompt.
Providing additional information to an OpenID Connect identity provider
In the previous section, we saw how the AuthenticationProperties.Parameters dictionary can flow data between call
sites in the ASP.NET Core authentication pipeline. There are two more ways to do this, at least in the case of the
OpenIdConnectHandler:
- Using the
ProtocolMessage.Parametersdictionary in an event callback likeOnRedirectToIdentityProvider - Using the
AdditionalAuthorizationParametersdictionary inOpenIdConnectOptions(in .NET 9+)
As a general rule of thumb, I'd recommend using AdditionalAuthorizationParameters when a static parameter value has to
be added when redirecting to the identity provider, and ProtocolMessage.Parameters when the parameter's value must
differ based on custom logic.
Returning to the previous section example, you could consume the login_hint parameter in the
OnRedirectToIdentityProvider of your OpenIdConnectOptions, and update its value in ProtocolMessage.Parameters:
.AddOpenIdConnect(options =>
{
// ...
// Add application_type query string parameter when redirecting to the identity provider
options.AdditionalAuthorizationParameters.Add("application_type", "demo");
// ...
options.Events.OnRedirectToIdentityProvider = async context =>
{
// Append #external if login hint domain is not example.com
if (context.Properties.Parameters.TryGetValue("login_hint", out var loginHint)
&& loginHint != null
&& !loginHint.ToString()!.EndsWith("@example.com"))
{
context.ProtocolMessage.Parameters["login_hint"] = loginHint + "#external";
}
await Task.FromResult(0);
};
// ...
});
Conclusion
In this post, we explored the AuthenticationProperties class in ASP.NET Core and its two key dictionaries: Items and
Parameters. We saw how Items can carry state throughout the authentication process while Parameters allow passing
additional data to authentication handlers or identity providers. We also discussed OpenID Connect-specific options like
ProtocolMessage.Parameters and AdditionalAuthorizationParameters for customizing authentication flows.
Using these features, you can build more flexible authentication flows that support your application's needs.
What are your thoughts on using AuthenticationProperties in your applications? Let us know in the comments below!