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
The example uses the Curity Identity Server, but you can run the code against any standards-based authorization server.
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:
{"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:
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
:
dotnet builddotnet 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:
<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:
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.
Integrating .NET Security
Security is implemented in the Startup.cs
file. First the Configure
method enables authentication and authorization:
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:
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:
[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.
GET /oauth/v2/oauth-anonymous/.well-known/openid-configuration HTTP/1.1Host: login.example.com:8443
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:
GET /oauth/v2/oauth-authorize HTTP/1.1Host: login.example.com:8443client_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
, state
and iss
parameters. No tokens are returned with the browser response:
GET /signin-oidc HTTP/1.1Host: www.example.com:5000code=I9xL9DY9jAYHPuHSiW2OpWUaNRW4otei&state=CfDJ8Nxa-YhPzjpBilDQz2CBMWR7SYDKEzCoODTBw5oO...&iss=https%3A%2F%2Flogin.example.com%3A8443%2Foauth%2Fv2%2Foauth-anonymous
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:
POST /oauth/v2/oauth-token HTTP/1.1Host: login.example.com:8443Content-Type: application/x-www-form-urlencodedclient_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.
{"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.
Set-Cookie: .AspNetCore.CookiesC1=CfDJ8Nxa-YhP... path=/; samesite=strict; httponly secureSet-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.
{"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:
POST /oauth/v2/oauth-token HTTP/1.1Host: login.example.com:8443Content-Type: application/x-www-form-urlencodedclient_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:
GET /oauth/v2/oauth-session/logout HTTP/1.1Host: login.example.com:8443post_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.
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial