OpenID Connect Client with .NET

OpenID Connect Client with .NET

On this page

Overview

This tutorial provides a basic demo web application, created using cross platform .NET. It shows how to use Microsoft's web security framework to implement an OpenID Connect flow, then retrieve OAuth tokens, in order to call APIs.

Note

Curity Identity Server is used in this example, but other OAuth servers can also be used.

Get the Code Sample

Clone the code repository from the link at the top of this page, then view the configuration in the appsettings.json file:

json
123456789101112131415
{
"OpenIDConnect" : {
"ClientId": "dotnet-client",
"ClientSecret": "U2U9EnSKx31fUnvgGR3coOUszko5MiuCSI2Z_4ogjIiO5-UbBzIBWU6JQQaljEis",
"Issuer": "https://login.example.com:8443/oauth/v2/oauth-anonymous",
"Scope": "openid profile"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

In order to run the example using real-world URLs for the website and OpenID provider, add the following two DNS names to your computer's hosts file:

text
1
127.0.0.1 www.example.com login.example.com

Run the website with the following standard .NET commands, then browse to http://www.example.com:5000:

csharp
12
dotnet build
dotnet run

To reduce infrastructure for developers, the example website uses plain HTTP. For deployed systems, follow Microsoft guides for configuring endpoints to update to an HTTPS setup.

Identity Server Configuration

The OpenID Connect settings from the appsettings.json file must also be registered with the OpenID provider, so that the app is trusted. The following XML provides the client configuration for the Curity Identity Server. It can be saved as XML and then imported via the Changes / Upload menu option of the Admin UI:

xml
1234567891011121314151617181920212223242526272829303132
<config xmlns="http://tail-f.com/ns/config/1.0">
<profiles xmlns="https://curity.se/ns/conf/base">
<profile>
<id>token-service</id>
<type xmlns:as="https://curity.se/ns/conf/profile/oauth">as:oauth-service</type>
<expose-detailed-error-messages />
<settings>
<authorization-server xmlns="https://curity.se/ns/conf/profile/oauth">
<client-store>
<config-backed>
<client>
<id>dotnet-client</id>
<secret>U2U9EnSKx31fUnvgGR3coOUszko5MiuCSI2Z_4ogjIiO5-UbBzIBWU6JQQaljEis</secret>
<redirect-uris>http://www.example.com:5000/signin-oidc</redirect-uris>
<scope>openid</scope>
<scope>profile</scope>
<user-authentication>
<allowed-post-logout-redirect-uris>http://www.example.com:5000</allowed-post-logout-redirect-uris>
</user-authentication>
<capabilities>
<code>
</code>
</capabilities>
<validate-port-on-loopback-interfaces>true</validate-port-on-loopback-interfaces>
</client>
</config-backed>
</client-store>
</authorization-server>
</settings>
</profile>
</profiles>
</config>

Run the Website

The website is intentionally minimal, since its only purpose is to demonstrate security. The initial page is a simple unauthenticated view:

Unauthenticated View

After you have authenticated, a protected view is rendered. The page shows how to get the current access token, which will be needed if the website calls APIs. Options are also provided for refreshing tokens, and logging out.

Authenticated View

Integrating .NET Security

Security is implemented in the Startup.cs file. First the Configure method enables authentication and authorization:

charp
12345678910
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapRazorPages();
});
}

Next, the ConfigureServices method indicates how authentication is managed, and the most important OpenID Connect and cookie settings are shown here:

charp
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => {
options.Cookie.SameSite = SameSiteMode.Strict;
})
.AddOpenIdConnect(options => {
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = Configuration.GetValue<string>("OpenIdConnect:Issuer");
options.ClientId = Configuration.GetValue<string>("OpenIdConnect:ClientId");
options.ClientSecret = Configuration.GetValue<string>("OpenIdConnect:ClientSecret");
options.ResponseType = OpenIdConnectResponseType.Code;
options.ResponseMode = OpenIdConnectResponseMode.Query;
options.GetClaimsFromUserInfoEndpoint = true;
string scopeString = Configuration.GetValue<string>("OpenIDConnect:Scope");
scopeString.Split(" ", StringSplitOptions.TrimEntries).ToList().ForEach(scope => {
options.Scope.Add(scope);
});
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = options.Authority,
ValidAudience = options.ClientId
};
options.Events.OnRedirectToIdentityProviderForSignOut = (context) =>
{
context.ProtocolMessage.PostLogoutRedirectUri = Configuration.GetValue<string>("OpenIdConnect:PostLogoutRedirectUri");
return Task.CompletedTask;
};
options.SaveTokens = true;
});
services.AddAuthorization();
services.AddRazorPages();
}

Security Flow

Razor pages are used to represent views, and those to which an Authorize attribute is applied require authorization. If a valid secure cookie is not provided when a page is invoked, an OpenID Connect redirect is triggered:

csharp
12345
[Authorize]
public class ProtectedModel : PageModel
{
...
}

Authentication Flow

Microsoft security libraries first use the issuer URL to locate the OpenID Connect metadata endpoint. This is done by appending a standard subpath, and is called via a GET request. The response contains public endpoints that will be called during user authentication, and to receive tokens.

text
1
GET http://login.example.com:8443/oauth/v2/oauth-anonymous/.well-known/openid-configuration

When OpenID Connect authentication begins, the Microsoft framework runs a code flow, expressed via response_type=code. It then generates state and PKCE values. These are stored in a temporary cookie, along with the URL of the protected page. A GET request is then issued to the authorize endpoint:

text
123456789
GET http://login.example.com:8443/oauth/v2/oauth-authorize
?client_id=dotnet-client
&redirect_uri=http://www.example.com:5000/signin-oidc
&response_type=code
&scope=openid profile
&code_challenge=WhmRaP18B9z2zkYcIlb4uVcZzjLqcZsaBQJf5akUxsA
&code_challenge_method=S256
&state=CfDJ8Nxa-YhPzjpBilDQz2C...
&nonce=638088910888703720.YzY3...

The user then authenticates, after which a response is returned to the browser, containing code and state parameters. No tokens are returned in the browser response:

text
123
http://www.example.com:5000/signin-oidc
?code=I9xL9DY9jAYHPuHSiW2OpWUaNRW4otei
&state=CfDJ8Nxa-YhPzjpBilDQz2CBMWR7SYDKEzCoODTBw5oO...

The Microsoft libraries validate the state, after which an authorization code grant request is posted to the token endpoint, to redeem the code for tokens:

text
123456789
POST http://login.example.com:8443/oauth/v2/oauth-token
Content-Type: application/x-www-form-urlencoded
client_id=dotnet-client
&client_secret=U2U9EnSKx31fUnvgGR3coOUszko5MiuCSI2Z_4ogjIiO5-UbBzIBWU6JQQaljEis
&code=I9xL9DY9jAYHPuHSiW2OpWUaNRW4otei
&grant_type=authorization_code
&redirect_uri=http://www.example.com:5000/signin-oidc
&code_verifier=HlfffYlGy7SIX3pYHOMJfhnO5AhUW1eOIKfjR42ue28

A response containing a set of tokens is then returned, after which the web app is returned to the protected page originally requested.

..",
12345678
{
"id_token":"eyJraWQiOiIzMjgwMTUwMzgiLCJ4NXQiOiJMd ...",
"token_type":"bearer",
"access_token":"_0XBPWQQ_c0f0677f-5aa9-4c4e-a3d4-c0f53db4037a",
"refresh_token":"_1XBPWQQ_197003c2-704f-4475-923c-2b40e5f5d696",
"scope":"openid profile",
"expires_in":299
}

Cookies Issued

The Microsoft framework writes the tokens received into encrypted HTTP-only session cookies. The code example uses only the most secure cookies, with SameSite=strict. This helps to prevent Cross Site Request Forgery (CSRF) vulnerabilities. Storing tokens in cookies means the security is stateless and easy to manage. The Curity Identity Server uses small opaque tokens by default, which also reduces the size of cookies.

aspnetcore.cookiesc1
12
Set-Cookie: .AspNetCore.CookiesC1=CfDJ8Nxa-YhP... path=/; samesite=strict; httponly secure
Set-Cookie: .AspNetCore.CookiesC2=mgOEjpmn6gcD... path=/; samesite=strict; httponly secure

Using Tokens

The ID token will contain values similar to the following, to represent proof of the authentication event. The Microsoft libraries verify the ID token before issuing session cookies, and the TokenValidationParameters can be used to customize this when required. The aud claim should be the client_id of the web app, and the iss claim should have the expected OpenID provider's value.

json
12345678910111213141516171819
{
"exp": 1673298677,
"nbf": 1673295077,
"jti": "3af7521b-5b24-475e-b368-c859c812ff19",
"iss": "http://login.example.com:8443/oauth/v2/oauth-anonymous",
"aud": "dotnet-client",
"sub": "642a797c311f0b7aef3db4e0a292bc69b924e6496d1e87aa3b28672c01611da7",
"auth_time": 1673295077,
"iat": 1673295077,
"purpose": "id",
"at_hash": "XW3GHVL_-VKsLzft-8PMyg",
"acr": "urn:se:curity:authentication:html-form:htmlform",
"delegation_id": "a7be55d5-598b-4d4f-9bef-be7cbdb5b14c",
"s_hash": "kQsASAVXIrk43CDx8O1jTw",
"azp": "dotnet-client",
"amr": "urn:se:curity:authentication:html-form:htmlform",
"nonce": "638088910888703720.YzY3...",
"sid": "eP33QugPjfhjexas"
}

After tokens are validated, a ClaimsPrincipal is built from the ID token. The GetClaimsFromUserInfoEndpoint property is used in the code example, to get name details for display from the OAuth user info endpoint. These values are also included in the .NET ClaimsPrincipal. In the Curity Identity Server, the token designer can be used to control where claims are issued.

The code example also shows how to get access tokens from within the web backend, which can be used to call APIs. When the access token expires, the code example shows how to refresh access tokens, which results in the following refresh token grant message being posted. A new set of tokens are then returned, and secure cookies are rewritten:

text
1234567
POST http://login.example.com:8443/oauth/v2/oauth-token
Content-Type: application/x-www-form-urlencoded
client_id=dotnet-client
&client_secret=U2U9EnSKx31fUnvgGR3coOUszko5MiuCSI2Z_4ogjIiO5-UbBzIBWU6JQQaljEis
&grant_type=refresh_token
&refresh_token=_1XBPWQQ_197003c2-704f-4475-923c-2b40e5f5d696

Finally, the logout operation results in a request to the end-session endpoint, and the web app also expires its session cookies at this point:

text
123
http://login.example.com:8443/oauth/v2/oauth-session/logout
?post_logout_redirect_uri=http://www.example.com:5000
&id_token_hint=eyJraWQiOiIzMjgwMTUwMzgiLCJ...

An ID token is simply base64 encoded JSON, so it is recommended to ensure that no personally identifiable information (PII) is revealed in the browser history or server logs. In this tutorial, the web app received PII from the user info endpoint, so that the logout request does not reveal any sensitive information.

Finishing Touches

A real .NET application would need to extend the code example, to implement logic for handling error and expiry conditions. The OpenID provider can return various error responses in the above messages, in error and error_description fields. For further details on handling these, see the OAuth troubleshooting for developers tutorial.

In most real-world deployments, multiple instances of the .NET secured website would be hosted in a cluster, then accessed using a load balancer. You then need to use a shared cookie decryption key, and ensure that the web application returns external URLs to the browser during OpenID Connect redirects. See the Microsoft guides on Data Protection and Load Balancing Configuration for further details on these topics.

Conclusion

This tutorial showed how to quickly implement an end-to-end OpenID Connect flow in .NET. Only simple code is needed, after which protected views are secured. The code example implementation follows OAuth best practices for browser based apps, since only the most secure HTTP only cookies are issued to the browser.