Demystifying OpenID Connect’s State and Nonce Parameters in ASP.NET Core

Demystifying OpenID Connect’s State and Nonce Parameters in ASP.NET Core

In the world of web application security, OpenID Connect plays a key role in streamlining authentication processes. But what makes it really tick? In this blog post, we dive deep into two critical security features of OpenID Connect – the state and nonce parameters – and how they are used in ASP.NET Core.

This simplified diagram tries to show how the state and nonce are used when a user authenticates using OpenID Connect:

Overview of how the state and nonce parameter is used durign the authentication flow.

In the image above, the client should verify that the returned state value matches the expected value and that the nonce inside the ID-token also matches the expected value.

Challenging the user in ASP.NET Core

In ASP.NET Core, when a user attempts to access a secured part of your application, the OpenID Connect handler initiates a challenge request. This can happen automatically via the authorization middleware or manually through the ChallengeAsync method:

				
					[HttpGet]
public async Task Login()
{
    if (User.Identity.IsAuthenticated == false)
    {
        await HttpContext.ChallengeAsync();
    }
}

				
			

This challenge triggers a redirect to the authorization server, such as Duende IdentityServer, with a URL crafted with various parameters, including the client_id, redirect_uri, response_type, and, notably, the state and nonce parameters.

The redirect URL can look something like this:

				
					https://identityservice.secure.nu/connect/authorize?
client_id=localhost-addoidc-client
&redirect_uri=https%3A%2F%2Flocalhost%3A5001%2Fsignin-oidc
&response_type=code
&prompt=consent
&scope=openid%20profile%20email%20employee_info%20offline_access%20api
&code_challenge=3LEckeTVCxl4TD12nxptlbiIgTzEEMfUqBa-sZo8foY
&code_challenge_method=S256
&response_mode=form_post
&nonce=638357382195073041.NzViMTM2MzktNWFhMi00MDhjLWIzODYtMDBkYzAxMDE2MmR
kYjY3NzdhYTEtZGQ0Mi00MTM4LWIwOTgtZjMyNmFlOTFjYzZk
&state=AQAAAAQAAAAJLnJlZGlyZWN0DC9Vc2VyL0xvZ2luPw1jb2RlX3ZlcmlmaWVyK3pPNzREaFJI
NndPbTdOSWZHZDZ3bkc0MDB0MEdNR3dMRGtmblF6NnVOMzgFLnhzcmYra2Q3OXk2N2E3Nk5DZ
3ZuY1RfNGdMQVgyeHVhMW12YjFrTkRwdnJBUXVpbx5PcGVuSWRDb25uZWN0LkNvZGUuUmVkaX
JlY3RVcmkiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMS9zaWduaW4tb2lkYw
&x-client-SKU=ID_NET8_0
&x-client-ver=7.0.0.0

				
			
The exact content depends on your OpenID-Connect middleware configuration.

The state parameter in OpenID Connect

The state parameter is crucial to an OpenID Connect authorization request. But what is its main purpose?

In OpenID Connect, the state parameter serves as an opaque value created by the client. Its primary function is maintaining the state between the initial request and the callback, acting as a shield against cross-site request forgery attacks during the authentication process.

While some implementations keep the state as a random value, that is verified during the callback. Other implementations injecting “data” into the state parameter. By doing this, the client doesn’t have to remember this information elsewhere, making the client stateless.

Looking inside the state parameter 

The OIDC handler in ASP.NET Core stores data in the state parameter, which is encrypted and secured using the Data Protection API.

However, we can “bypass” this encryption and issue an unencrypted state parameter. To do this, we can add our own transparent data protector and implement it like this:

				
					public class MyDataProtector : IDataProtector
{
    public IDataProtector CreateProtector(string purpose)
    {
        return new MyDataProtector();
    }

    public byte[] Protect(byte[] plaintext)
    {
        return plaintext;
    }

    public byte[] Unprotect(byte[] protectedData)
    {
        return protectedData;
    }
}

				
			
Then, we can tell the OpenIDConnect handler to replace the default encryption protector with this one instead:
				
					}).AddOpenIdConnect("oidc", o =>
{
    //...
    o.StateDataFormat = new PropertiesDataFormat(new MyDataProtector());
});

				
			
Adding this allows us to peek inside the state parameter:
				
					GET https://identityservice.secure.nu/connect/authorize?
client_id=localhost-addoidc-client
...
&state=AQAAAAQAAAAJLnJlZGlyZWN0DC9Vc2VyL0xvZ2luPw1jb2RlX3ZlcmlmaWVyK0dISVBiMmJG
SHNfWDdGRjRmSm1aZDBKR1BXbm5BeXllVWlsUXJnMjE2NE0FLnhzcmYrLTMxWktMb0N3TlVUcnd
EQTVfSHRkZExPbUtVbTVkM0piMC14X3ZmSm1RWR5PcGVuSWRDb25uZWN0LkNvZGUuUmVkaXJl
Y3RVcmkiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMS9zaWduaW4tb2lkYw

				
			
If we decode the state parameter using a tool like base64 decode https://www.base64decode.org , then we get a state that looks like this:
				
					.................redirect./User/Login?
code_verifier+GHIPb2bFHs_X7FF4fJmZd0JGPWnnAyyeUilQrg2164M..xsrf+-
31ZKLoCwNUTrwDA5_HtddLOmKUm5d3Jb0-
x_vfJmQY.OpenIdConnect.Code.RedirectUri"https://localhost:5001/signin-oidc

				
			
(The non-printable characters have been replaced with a dot).
The above can be a bit hard to read, a cleaned-up version looks like this:
				
					[0]: {[.redirect, /User/Login?]}
    [1]: {[code_verifier, GHIPb2bFHs_X7FF4fJmZd0JGPWnnAyyeUilQrg2164M]}
    [2]: {[.xsrf, -31ZKLoCwNUTrwDA5_HtddLOmKUm5d3Jb0-x_vfJmQY]}
    [3]: {[OpenIdConnect.Code.RedirectUri, https://localhost:5001/signin-oidc]}


				
			

Can I add custom properties here?

Yes, if you provide an instance of the AuthenticationProperties with the challenge, like this:

				
					[HttpGet]
public async Task Login()
{
    if (User.Identity.IsAuthenticated == false)
    {
        var properties = new AuthenticationProperties()
        {
            RedirectUri = "/",
            Items =
            {
                { "IpAddress", "192.168.0.3" },
                { "ComputerName", "MyComputer" },
                { "ApiKey", "Summer2023" },
                { "Language", "English" }
            }
        };

        await HttpContext.ChallengeAsync(properties);
    }
}

				
			
Then you might end up with a state parameter that looks like this:
				
					…
&state=AQAAAAgAAAAJLnJlZGlyZWN0AS8JSXBBZGRyZXNzCzE5Mi4xNjguMC4zDENvbXB1dGVyTm
FtZQpNeUNvbXB1dGVyBkFwaUtleQpTdW1tZXIyMDIzCExhbmd1YWdlB0VuZ2xpc2gNY29kZV92ZXJ
pZmllcitpVGw1UnczYno1SnYxNm90ektIbHpHT0RGSjczblJ6dzRSbDQ4Zkh4RGFFBS54c3JmK1hOd3R
fd3otMjlpVGtGYklXN2ZsWGNwdmplenF1bElWYkt5alQzWWIzZ00eT3BlbklkQ29ubmVjdC5Db2RlLlJl
ZGlyZWN0VXJpImh0dHBzOi8vbG9jYWxob3N0OjUwMDEvc2lnbmluLW9pZGM
…

				
			
If you base64 decode it and then clean it up, you will get the following information:
				
					[0]: {[.redirect, /]}
    [1]: {[IpAddress, 192.168.0.3]}
    [2]: {[ComputerName, MyComputer]}
    [3]: {[ApiKey, Summer2023]}
    [4]: {[Language, English]}
    [5]: {[code_verifier, iTl5Rw3bz5Jv16otzKHlzGODFJ73nRzw4Rl48fHxDaE]}
    [6]: {[.xsrf, XNwt_wz-29iTkFbIW7flXcpvjezqulIVbKyjT3Yb3gM]}
    [7]: {[OpenIdConnect.Code.RedirectUri, https://localhost:5001/signin-oidc]}

				
			
The AuthenticationProperties that you passed to the initial challenge, will later be available in the ClaimsPrincipal user object after the user is signed in.

The nonce parameter in OpenID Connect

The nonce parameter in OpenID Connect is crucial for associating a client session with the ID token and is used to mitigate replay attacks.

In ASP.NET Core, it’s generated by the GenerateNonce method, as shown below:

				
					public virtual string GenerateNonce()
{
   LogHelper.LogVerbose(LogMessages.IDX21328);
   string nonce = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString() + 
                                                               Guid.NewGuid().ToString()));
    if (RequireTimeStampInNonce)
    {
        return DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture) + "." + nonce;
    }

    return nonce;
}

				
			

The method is found in the OpenIdConnectProtocolValidator class.

The nonce generated by this method is a string that consists of a timestamp and a random value; it can look like this:

				
					638357413236076453.NjY2MmZlZTctOTU2OC00ZmNlLTk5NWUtMWRmODMxZDY4NTFhYmRmYjI3ODgtN
TU3ZS00NjZiLWJjMGMtNWFlOWNkOGQ3M2Vi
				
			

How is the nonce parameter handled in ASP.NET Core?

Upon challenging the user, a nonce is generated and stored as a cookie, as shown below:

				
					Set-Cookie: 
.AspNetCore.OpenIdConnect.Nonce
.NjM4MzU3NDE2MTcwODk2NjQ2LllUWmtOak5qWm1RdE5UTmpaUzAwTkRNeExUZzBNRFV0Wm1aalpHVmxOekk
1WlRsak5XWmhaR000TmpndFpEY3pPQzAwTXpRd0xXRTFNR010TTJRd01qQm1aV0kzT1ROag=N;
expires=Thu, 16 Nov 2023 14:42:08 GMT; path=/signin-oidc; secure; samesite=none; httponly
				
			

The cookie name is “.AspNetCore.OpenIdConnect.[Nonce]” and the interesting thing here is that the cookie name contains the nonce value. The nonce here is also protected using the Data Protection API.

Similar to what we did before, we can introduce the transparent protector by setting the StringDataFormat property.

				
					}).AddOpenIdConnect("oidc", o =>
{
	//...
    o.StateDataFormat = new PropertiesDataFormat(new MyDataProtector());
    o.StringDataFormat = new SecureDataFormat<string>(new StringSerializer(),
    new MyDataProtector());
});

				
			

Adding this will make the handler store the nonce in the cookie in unprotected form. However, this is not useful, as there’s little interesting information to see in the nonce cookie.

Besides storing the nonce in the cookie, it is also included in its raw form in the authentication request to the authorization server:

				
					GET https://identityservice.secure.nu/connect/authorize?client_id=localhost-addoidc-client
...
&nonce=638357416170896646.YTZkNjNjZmQtNTNjZS00NDMxLTg0MDUtZmZjZGVlNzI5ZTljNWZhZGM4NjgtZD
czOC00MzQwLWE1MGMtM2QwMjBmZWI3OTNj
...

				
			

Conclusions

In this blog post, we have explored what is inside the encrypted state parameter that is passed to the authorization server when the user is challenged. We also saw that we can pass custom AuthenticationProperties to the challenge operation, and these properties will later end up in the ClaimsPrincipal user object.

We also explored the nonce, a random string that we store securely in a cookie and pass in its raw form to the authorization server. The nonce ties the ID-token to the initial request made to the authorization server. With the nonce, the client knows the token is generated for itself, and it won’t consume a token injected by a malicious party.

Feedback, comments, found any bugs?

Let me know if you have any feedback, anything I missed, or any bugs/typos. You can find my contact details here.

About the author

Hi, I’m Tore! I have been fascinated by computers since I unpacked my first Commodore VIC-20. Today, I enjoy providing freelance development and developer training services, focusing on ASP.NET Core, IdentityServer, OpenID Connect, Architecture, and Web Security. You can connect with me on LinkedIn and Twitter, and you can find out more about me and my services here, as well as my courses for developers, including my course, Introduction to IdentityServer and OpenID-Connect.

Share This Story

Related Posts

About me

My name is Tore Nestenius and I’m a trainer and senior software developer focusing on Architecture, Security and Identity, .NET, C#, Backend, and Cloud, among other things.

Do You Want Tore To Be Your Mentor?

Categories