Welcome to Ourstudio.IdentityServer!¶

Ourstudio.IdentityServer is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core, based on the fantastic IdentityServer4.
It enables the following features in your applications:
The Big Picture¶
Most modern applications look more or less like this:

The most common interactions are:
Browsers communicate with web applications
Web applications communicate with web APIs (sometimes on their own, sometimes on behalf of a user)
Browser-based applications communicate with web APIs
Native applications communicate with web APIs
Server-based applications communicate with web APIs
Web APIs communicate with web APIs (sometimes on their own, sometimes on behalf of a user)
Typically each and every layer (front-end, middle-tier and back-end) has to protect resources and implement authentication and/or authorization – often against the same user store.
Outsourcing these fundamental security functions to a security token service prevents duplicating that functionality across those applications and endpoints.
Restructuring the application to support a security token service leads to the following architecture and protocols:

Such a design divides security concerns into two parts:
Authentication¶
Authentication is needed when an application needs to know the identity of the current user. Typically these applications manage data on behalf of that user and need to make sure that this user can only access the data for which he is allowed. The most common example for that is (classic) web applications – but native and JS-based applications also have a need for authentication.
The most common authentication protocols are SAML2p, WS-Federation and OpenID Connect – SAML2p being the most popular and the most widely deployed.
OpenID Connect is the newest of the three, but is considered to be the future because it has the most potential for modern applications. It was built for mobile application scenarios right from the start and is designed to be API friendly.
API Access¶
Applications have two fundamental ways with which they communicate with APIs – using the application identity, or delegating the user’s identity. Sometimes both methods need to be combined.
OAuth2 is a protocol that allows applications to request access tokens from a security token service and use them to communicate with APIs. This delegation reduces complexity in both the client applications as well as the APIs since authentication and authorization can be centralized.
OpenID Connect and OAuth 2.0 – better together¶
OpenID Connect and OAuth 2.0 are very similar – in fact OpenID Connect is an extension on top of OAuth 2.0. The two fundamental security concerns, authentication and API access, are combined into a single protocol - often with a single round trip to the security token service.
We believe that the combination of OpenID Connect and OAuth 2.0 is the best approach to secure modern applications for the foreseeable future. Ourstudio.IdentityServer is an implementation of these two protocols and is highly optimized to solve the typical security problems of today’s mobile, native and web applications.
How Ourstudio.IdentityServer can help¶
IdentityServer is middleware that adds the spec compliant OpenID Connect and OAuth 2.0 endpoints to an arbitrary ASP.NET Core application.
Typically, you build (or re-use) an application that contains a login and logout page (and maybe consent - depending on your needs), and the IdentityServer middleware adds the necessary protocol heads to it, so that client applications can talk to it using those standard protocols.

The hosting application can be as complex as you want, but we typically recommend to keep the attack surface as small as possible by including authentication related UI only.
Terminology¶
The specs, documentation and object model use a certain terminology that you should be aware of.

IdentityServer¶
IdentityServer is an OpenID Connect provider - it implements the OpenID Connect and OAuth 2.0 protocols.
Different literature uses different terms for the same role - you probably also find security token service, identity provider, authorization server, IP-STS and more.
But they are in a nutshell all the same: a piece of software that issues security tokens to clients.
IdentityServer has a number of jobs and features - including:
protect your resources
authenticate users using a local account store or via an external identity provider
provide session management and single sign-on
manage and authenticate clients
issue identity and access tokens to clients
validate tokens
User¶
A user is a human that is using a registered client to access resources.
Client¶
A client is a piece of software that requests tokens from IdentityServer - either for authenticating a user (requesting an identity token) or for accessing a resource (requesting an access token). A client must be first registered with IdentityServer before it can request tokens.
Examples for clients are web applications, native mobile or desktop applications, SPAs, server processes etc.
Resources¶
Resources are something you want to protect with IdentityServer - either identity data of your users, or APIs.
Every resource has a unique name - and clients use this name to specify to which resources they want to get access to.
Identity data Identity information (aka claims) about a user, e.g. name or email address.
APIs APIs resources represent functionality a client wants to invoke - typically modelled as Web APIs, but not necessarily.
Identity Token¶
An identity token represents the outcome of an authentication process. It contains at a bare minimum an identifier for the user (called the sub aka subject claim) and information about how and when the user authenticated. It can contain additional identity data.
Access Token¶
An access token allows access to an API resource. Clients request access tokens and forward them to the API. Access tokens contain information about the client and the user (if present). APIs use that information to authorize access to their data.
Supported Specifications¶
IdentityServer implements the following specifications:
OpenID Connect¶
OAuth 2.0¶
OAuth 2.0 (RFC 6749)
OAuth 2.0 Bearer Token Usage (RFC 6750)
OAuth 2.0 Multiple Response Types (spec)
OAuth 2.0 Form Post Response Mode (spec)
OAuth 2.0 Token Revocation (RFC 7009)
OAuth 2.0 Token Introspection (RFC 7662)
Proof Key for Code Exchange (RFC 7636)
JSON Web Tokens for Client Authentication (RFC 7523)
OAuth 2.0 Device Authorization Grant (RFC 8628)
OAuth 2.0 Mutual TLS Client Authentication and Certificate-Bound Access Tokens (RFC 8705)
JWT Secured Authorization Request (draft)
Packaging and Builds¶
IdentityServer consists of a number of nuget packages.
Ourstudio.IdentityServer main repo¶
Contains the core IdentityServer object model, services and middleware as well as the EntityFramework and ASP.NET Identity integration.
nugets:
Overview¶
The quickstarts provide step by step instructions for various common IdentityServer scenarios. They start with the absolute basics and become more complex - it is recommended you do them in order.
adding IdentityServer to an ASP.NET Core application
configuring IdentityServer
issuing tokens for various clients
securing web applications and APIs
adding support for EntityFramework based configuration
adding support for ASP.NET Identity
Protecting an API using Client Credentials¶
The following Identity Server quickstart provides step by step instructions for various common IdentityServer scenarios. These start with the absolute basics and become more complex as they progress. We recommend that you follow them in sequence.
Setting up the project¶
First create a base for the quickstart:
mkdir -p src dotnet new sln -n Quickstart
Add package sources for NuGet to locate Ourstudio.IdentityServer by creating a NuGet.Config
file with the following contents:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageRestore>
<add key="enabled" value="True" />
<add key="automatic" value="True" />
</packageRestore>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="ourstudio-se" value="https://nuget.pkg.github.com/ourstudio-se/index.json" />
</packageSources>
<activePackageSource>
<add key="All" value="(Aggregate source)" />
</activePackageSource>
</configuration>
Note
Run dotnet dev-certs https --trust
in order to trust the development certificate. This only needs to be done once.
Defining the server¶
Create an empty .NET8 web application, e.g.:
dotnet new web -n Server -o src/Server/
dotnet sln Quickstart.sln add src/Server/Server.csproj
This will create a directory Server
with the following files:
Server.csproj
- the project file and aProperties/launchSettings.json
fileProgram.cs
- the main application entry pointappsettings.json
- configuration file
Update the file Properties/launchSettings.json
to look like this:
{
"profiles": {
"Server": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:5001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Add Ourstudio.IdentityServer¶
dotnet add src/Server/Server.csproj package Ourstudio.IdentityServer
Defining an API Scope¶
An API is a resource in your system that you want to protect. Resource definitions can be loaded in many ways, using a “code as configuration” approach:
Create a Config.cs
file. Open it, add the following code snippet:
public static class Config
{
public static IEnumerable<ApiScope> ApiScopes =>
new[]
{
new ApiScope("api1", "My API")
};
public static IEnumerable<IdentityResource> IdentityResources =>
new[]
{
new IdentityResources.OpenId()
};
public static IEnumerable<Client> Clients =>
new[]
{ };
}
Note
If you will be using this in production it is important to give your API a logical name. Developers will be using this to connect to your api though your Identity server. It should describe your api in simple terms to both developers and users.
Defining the client¶
The next step is to define a client application that we will use to access our new API.
For this scenario, the client will not have an interactive user, and will authenticate using the so called client secret with IdentityServer.
For this, add a client definition:
public static IEnumerable<Client> Clients =>
new[]
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
};
You can think of the ClientId and the ClientSecret as the login and password for your application itself. It identifies your application to the identity server so that it knows which application is trying to connect to it.
Configuring Ourstudio.IdentityServer¶
Loading the resource and client definitions happens in Program.cs
- update the code to look like this:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddIdentityServer()
// WARNING! Do NOT use this in production mode!
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients);
var app = builder.Build();
app.UseIdentityServer();
app.Run();
That’s it - your identity server should now be configured. If you run the server (dotnet run --project src/Server/Server.csproj
) and navigate the browser to https://localhost:5001/.well-known/openid-configuration
, you should see the so-called discovery document.
The discovery document is a standard endpoint in identity servers. The discovery document will be used by your clients and APIs to download the necessary configuration data.

At first startup, Ourstudio.IdentityServer will create a developer signing key for you, it’s a file called tempkey.jwk
.
You don’t have to check that file into your source control, it will be re-created if it is not present.
Adding an API¶
Next, add an API to your solution:
dotnet new webapi -n Api -o src/Api
dotnet sln Quickstart.sln add src/Api/Api.csproj
Configure the API application to run on https://localhost:6001
only. You can do this by editing the launchSettings.json
file inside the Properties folder:
{
"profiles": {
"Api": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:6001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
You can remove the files Controllers/WeatherForecastController.cs
and WeatherForecast.cs
.
The controller¶
Add a new class called IdentityController
:
[Route("identity")]
[Authorize]
public class IdentityController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
This controller will be used later to test the authorization requirement, as well as visualize the claims identity through the eyes of the API.
Adding a Nuget Dependency¶
In order for the configuration step to work the nuget package dependency has to be added, run this command in the root directory:
dotnet add src/Api/Api.csproj package Microsoft.AspNetCore.Authentication.JwtBearer
Configuration¶
The last step is to add the authentication services to DI (dependency injection) and the authentication middleware to the pipeline. These will:
validate the incoming token to make sure it is coming from a trusted issuer
validate that the token is valid to be used with this api (aka audience)
Update Program.cs
to look like this:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
};
// WARNING! Do NOT use this in production mode!
//
// Since we're using a development certificate, we need to tell HttpClient
// to trust the server certificate
options.BackchannelHttpHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (arg1, arg2, arg3, arg4) => true,
};
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
AddAuthentication
adds the authentication services to DI and configuresBearer
as the default scheme.UseAuthentication
adds the authentication middleware to the pipeline so authentication will be performed automatically on every call into the host.UseAuthorization
adds the authorization middleware to make sure, our API endpoint cannot be accessed by anonymous clients.
Navigating to the controller https://localhost:6001/identity
on a browser should return a 401 status code.
This means your API requires a credential and is now protected by IdentityServer.
Note
If you are wondering, why the above code disables audience validation, have a look here for a more in-depth discussion.
Creating the client¶
The last step is to write a client that requests an access token, and then uses this token to access the API. For that, add a console project to your solution:
dotnet new console -n Client -o src/
dotnet sln Quickstart.sln add src/Client/Client.csproj
The token endpoint at IdentityServer implements the OAuth 2.0 protocol, and you could use raw HTTP to access it. However, we have a client library called IdentityModel, that encapsulates the protocol interaction in an easy to use API.
Add the IdentityModel
NuGet package to your client:
dotnet add src/Client/Client.csproj package IdentityModel
IdentityModel includes a client library to use with the discovery endpoint. This way you only need to know the base-address of IdentityServer - the actual endpoint addresses can be read from the metadata:
// discover endpoints from metadata
var client = new HttpClient(new HttpClientHandler
{
// WARNING! Do NOT use this in production mode!
//
// Since we're using a development certificate, we need to tell HttpClient
// to trust the server certificate
ServerCertificateCustomValidationCallback = (arg1, arg2, arg3, arg4) =>
{
return true;
}
});
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
Next you can use the information from the discovery document to request a token to IdentityServer to access api1
:
// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
Note
Copy and paste the access token from the console to jwt.ms to inspect the raw token.
Calling the API¶
To send the access token to the API you typically use the HTTP Authorization header. This is done using the SetBearerToken
extension method:
// call api
client.SetBearerToken(tokenResponse.AccessToken);
var response = await client.GetAsync("https://localhost:6001/identity");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
The output should look like this:

Note
By default an access token will contain claims about the scope, lifetime (nbf and exp), the client ID (client_id) and the issuer name (iss).
Further experiments¶
This walkthrough focused on the success path so far
client was able to request token
client could use the token to access the API
You can now try to provoke errors to learn how the system behaves, e.g.
try to connect to IdentityServer when it is not running (unavailable)
try to use an invalid client id or secret to request the token
try to ask for an invalid scope during the token request
try to call the API when it is not running (unavailable)
don’t send the token to the API
configure the API to require a different scope than the one in the token
Interactive Applications with ASP.NET Core¶
In this quickstart we want to add support for interactive user authentication via the OpenID Connect protocol to our IdentityServer we built in the previous chapter.
Once that is in place, we will create an MVC application that will use IdentityServer for authentication.
Creating an MVC client¶
Next you will create an MVC application. Use the ASP.NET Core “Web Application” (i.e. MVC) template for that:
dotnet new mvc -n MvcClient -o src/MvcClient
dotnet sln Quickstart.sln add src/MvcClient/MvcClient.csproj
Note
We recommend using the self-host option over IIS Express. The rest of the docs assume you are using self-hosting on port 5002.
To add support for OpenID Connect authentication to the MVC application, you first need to add the nuget package containing the OpenID Connect handler to your project, e.g.:
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
..then add the following to Program.cs
:
using System.IdentityModel.Tokens.Jwt;
// ...
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:5001";
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
});
AddAuthentication
adds the authentication services to DI.
We are using a cookie to locally sign-in the user (via "Cookies"
as the DefaultScheme
),
and we set the DefaultChallengeScheme
to oidc
because when we need the user to login, we will be using the OpenID Connect protocol.
We then use AddCookie
to add the handler that can process cookies.
Finally, AddOpenIdConnect
is used to configure the handler that performs the OpenID Connect protocol.
The Authority
indicates where the trusted token service is located.
We then identify this client via the ClientId
and the ClientSecret
.
SaveTokens
is used to persist the tokens from IdentityServer in the cookie (as they will be needed later).
Note
We use the so called authorization code
flow with PKCE to connect to the OpenID Connect provider. See here for more information on protocol flows.
And then to ensure the execution of the authentication services on each request, add UseAuthentication
to Program.cs
:
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app
.MapControllers()
.RequireAuthorization();
Note
The RequireAuthorization
method disables anonymous access for the entire application.
You can also use the [Authorize]
attribute, if you want to specify authorization on a per controller or action method basis.
Also modify the home view to display the claims of the user as well as the cookie properties:
@using Microsoft.AspNetCore.Authentication
<h2>Claims</h2>
<dl>
@foreach (var claim in User.Claims)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
<h2>Properties</h2>
<dl>
@foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
{
<dt>@prop.Key</dt>
<dd>@prop.Value</dd>
}
</dl>
If you now navigate to the application using the browser, a redirect attempt will be made to IdentityServer - this will result in an error because the MVC client is not registered yet.
Adding support for OpenID Connect Identity Scopes¶
Similar to OAuth 2.0, OpenID Connect also uses the scopes concept. Again, scopes represent something you want to protect and that clients want to access. In contrast to OAuth, scopes in OIDC don’t represent APIs, but identity data like user id, name or email address.
Add support for the standard openid
(subject id) and profile
(first name, last name etc..) scopes
by amending the IdentityResources
property in Config.cs
:
public static IEnumerable<IdentityResource> IdentityResources =>
new[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
Register the identity resources with IdentityServer in Program.cs
:
builder.Services
.AddIdentityServer()
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients);
Note
All standard scopes and their corresponding claims can be found in the OpenID Connect specification
Adding Test Users¶
The sample UI also comes with an in-memory “user database”. You can enable this in IdentityServer by adding the AddTestUsers
extension method:
builder.Services
.AddIdentityServer()
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddTestUsers(TestUsers.Users);
When you navigate to the TestUsers
class, you can see that two users called alice
and bob
as well as some identity claims are defined.
You can use those users to login.
Adding the MVC Client to the IdentityServer Configuration¶
The last step is to add a new configuration entry for the MVC client to the IdentityServer.
OpenID Connect-based clients are very similar to the OAuth 2.0 clients we added so far. But since the flows in OIDC are always interactive, we need to add some redirect URLs to our configuration.
The client list should look like this:
public static IEnumerable<Client> Clients =>
new[]
{
// machine to machine client (from quickstart 1)
new Client
{
ClientId = "client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
// scopes that client has access to
AllowedScopes = { "api1" }
},
// interactive ASP.NET Core MVC client
new Client
{
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
// where to redirect to after login
RedirectUris = { "https://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
}
};
Testing the client¶
Now finally everything should be in place for the new MVC client.
Trigger the authentication handshake by navigating to the protected controller action. You should see a redirect to the login page of the IdentityServer.

After that, the IdentityServer will redirect back to the MVC client, where the OpenID Connect authentication handler processes the response and signs-in the user locally by setting a cookie. Finally the MVC view will show the contents of the cookie.

As you can see, the cookie has two parts, the claims of the user, and some metadata. This metadata also contains the original token that was issued by the IdentityServer. Feel free to copy this token to jwt.ms to inspect its content.
Adding sign-out¶
The very last step is to add sign-out to the MVC client.
With an authentication service like IdentityServer, it is not enough to clear the local application cookies. In addition you also need to make a roundtrip to the IdentityServer to clear the central single sign-on session.
The exact protocol steps are implemented inside the OpenID Connect handler, simply add the following code to some controller to trigger the sign-out:
public IActionResult Logout()
{
return SignOut("Cookies", "oidc");
}
This will clear the local cookie and then redirect to the IdentityServer. The IdentityServer will clear its cookies and then give the user a link to return back to the MVC application.
Getting claims from the UserInfo endpoint¶
You might have noticed that even though we’ve configured the client to be allowed to retrieve the profile
identity scope, the claims associated with that scope (such as name
, family_name
, website
etc.) don’t appear in the returned token. We need to tell the client to pull remaining claims from the UserInfo
endpoint by specifying scopes that the client application needs to access and setting the GetClaimsFromUserInfoEndpoint
option. In the following example we’re requesting the profile
scope, but it could be any scope (or scopes) that the client is authorized to access:
.AddOpenIdConnect("oidc", options =>
{
// ...
options.Scope.Add("profile");
options.GetClaimsFromUserInfoEndpoint = true;
// ...
});
After restarting the client app, logging out, and logging back in you should see additional user claims associated with the profile
identity scope displayed on the page.

Further Experiments¶
Feel free to add more claims to the test users - and also more identity resources.
The process for defining an identity resource is as follows:
add a new identity resource to the list - give it a name and specify which claims should be returned when this resource is requested
give the client access to the resource via the
AllowedScopes
property on the client configurationrequest the resource by adding it to the
Scopes
collection on the OpenID Connect handler configuration in the client(optional) if the identity resource is associated with a non-standard claim (e.g.
myclaim1
), on the client side add the ClaimAction mapping between the claim appearing in JSON (returned from the UserInfo endpoint) and the User Claimusing Microsoft.AspNetCore.Authentication // ... .AddOpenIdConnect("oidc", options => { // ... options.ClaimActions.MapUniqueJsonKey("myclaim1", "myclaim1"); // ... });
It is also noteworthy, that the retrieval of claims for tokens is an extensibility point - IProfileService
.
Since we are using AddTestUsers
, the TestUserProfileService
is used by default.
Adding Support for External Authentication¶
Next we will add support for external authentication. This is really easy, because all you really need is an ASP.NET Core compatible authentication handler.
ASP.NET Core itself ships with support for Google, Facebook, Twitter, Microsoft Account and OpenID Connect. In addition you can find implementations for many other authentication providers here.
Adding Google support¶
To be able to use Google for authentication, you first need to register with them. This is done at their developer console. Create a new project, enable the Google+ API and configure the callback address of your local IdentityServer by adding the /signin-google path to your base-address (e.g. https://localhost:5001/signin-google).
The developer console will show you a client ID and secret issued by Google - you will need that in the next step.
Add the Google authentication handler to the DI of the IdentityServer host.
This is done by first adding the Microsoft.AspNetCore.Authentication.Google
nuget package and then adding this snippet to Program.cs
:
builder.Services
.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "<insert here>";
options.ClientSecret = "<insert here>";
});
By default, IdentityServer configures a cookie handler specifically for the results of external authentication (with the scheme based on the constant IdentityServerConstants.ExternalCookieAuthenticationScheme
).
The configuration for the Google handler is then using that cookie handler.
Now run the MVC client and try to authenticate - you will see a Google button on the login page:

After authentication with the MVC client, you can see that the claims are now being sourced from Google data.
Note
If you are interested in the magic that automatically renders the Google button on the login page, inspect the BuildLoginViewModel
method on the AccountController
.
ASP.NET Core and API access¶
In the previous quickstarts we explored both API access and user authentication. Now we want to bring the two parts together.
The beauty of the OpenID Connect & OAuth 2.0 combination is, that you can achieve both with a single protocol and a single exchange with the token service.
So far we only asked for identity resources during the token request, once we start also including API resources, IdentityServer will return two tokens: the identity token containing the information about the authentication and session, and the access token to access APIs on behalf of the logged on user.
Modifying the client configuration¶
Updating the client configuration in IdentityServer is straightforward - we simply need to add the api1
resource to the allowed scopes list.
In addition we enable support for refresh tokens via the AllowOfflineAccess
property:
new Client
{
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
// where to redirect to after login
RedirectUris = { "https://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
AllowOfflineAccess = true,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
Modifying the MVC client¶
All that’s left to do now in the client is to ask for the additional resources via the scope parameter. This is done in the OpenID Connect handler configuration:
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:5001";
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.Scope.Add("api1");
options.Scope.Add("offline_access");
});
Since SaveTokens
is enabled, ASP.NET Core will automatically store the resulting access and refresh token in the authentication session.
You should be able to inspect the data on the page that prints out the contents of the session that you created earlier.
Using the access token¶
You can access the tokens in the session using the standard ASP.NET Core extension methods
that you can find in the Microsoft.AspNetCore.Authentication
namespace:
var accessToken = await HttpContext.GetTokenAsync("access_token");
For accessing the API using the access token, all you need to do is retrieve the token, and set it on your HttpClient:
public async Task<IActionResult> CallApi()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var content = await client.GetStringAsync("https://localhost:6001/identity");
ViewBag.Json = JArray.Parse(content).ToString();
return View("json");
}
Create a view called json.cshtml that outputs the json like this:
<pre>@ViewBag.Json</pre>
Make sure the API is running, start the MVC client and call /home/CallApi
after authentication.
Managing the access token¶
By far the most complex task for a typical client is to manage the access token. You typically want to
request the access and refresh token at login time
cache those tokens
use the access token to call APIs until it expires
use the refresh token to get a new access token
start over
ASP.NET Core has many built-in facility that can help you with those tasks (like caching or sessions), but there is still quite some work left to do. Feel free to have a look at this library, which can automate many of the boilerplate tasks.
Adding a JavaScript client¶
This quickstart will show how to build a browser-based JavaScript client application (sometimes referred to as a “Single Page Application” or “SPA”).
The user will login to IdentityServer, invoke the web API with an access token issued by IdentityServer, and logout of IdentityServer. All of this will be driven from the JavaScript running in the browser.
New Project for the JavaScript client¶
Create a new project for the JavaScript application. It can simply be an empty web project, an empty ASP.NET Core application, or something else like a Node.js application. This quickstart will use an ASP.NET Core application.
Create a new “Empty” ASP.NET Core web application in the ~/src
directory.
You can use Visual Studio or do this from the command line:
dotnet new web -n JavaScriptClient -o src/JavaScriptClient
dotnet sln Quickstart.sln add src/JavaScriptClient/JavaScriptClient.csproj
Modify hosting¶
Modify the JavaScriptClient project to run on https://localhost:5003.
Add the static file middleware¶
Given that this project is designed to run client-side, all we need ASP.NET Core to do is to serve up the static HTML and JavaScript files that will make up our application. The static file middleware is designed to do this.
Register the static file middleware in Program.cs
:
app.UseDefaultFiles();
app.UseStaticFiles();
This middleware will now serve up static files from the application’s ~/wwwroot
folder.
This is where we will put our HTML and JavaScript files.
If that folder does not exist in your project, create it now.
Reference oidc-client¶
In one of the previous quickstarts in the ASP.NET Core MVC-based client project we used a library to handle the OpenID Connect protocol.
In this quickstart in the JavaScriptClient
project we need a similar library, except one that works in JavaScript and is designed to run in the browser.
The oidc-client library is one such library.
It is available via NPM, Bower, as well as a direct download from github.
NPM
If you want to use NPM to download oidc-client
, then run these commands from your JavaScriptClient
project directory:
npm i oidc-client
copy node_modules\oidc-client\dist\* wwwroot
This downloads the latest oidc-client
package locally, and then copies the relevant JavaScript files into ~/wwwroot
so they can be served up by your application.
Manual download
If you want to simply download the oidc-client
JavaScript files manually, browse to the GitHub repository and download the JavaScript files. Once downloaded, copy them into ~/wwwroot
so they can be served up by your application.
Add your HTML and JavaScript files¶
Next is to add your HTML and JavaScript files to ~/wwwroot
.
We will have two HTML files and one application-specific JavaScript file (in addition to the oidc-client.js
library).
In ~/wwwroot
, add a HTML file named index.html
and callback.html
, and add a JavaScript file called app.js
.
index.html
This will be the main page in our application.
It will simply contain the HTML for the buttons for the user to login, logout, and call the web API.
It will also contain the <script>
tags to include our two JavaScript files.
It will also contain a <pre>
used for showing messages to the user.
It should look like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<button id="login">Login</button>
<button id="api">Call API</button>
<button id="logout">Logout</button>
<pre id="results"></pre>
<script src="oidc-client.js"></script>
<script src="app.js"></script>
</body>
</html>
app.js
This will contain the main code for our application.
The first thing is to add a helper function to log messages to the <pre>
:
function log() {
document.getElementById('results').innerText = '';
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
}
else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('results').innerHTML += msg + '\r\n';
});
}
Next, add code to register click
event handlers to the three buttons:
document.getElementById("login").addEventListener("click", login, false);
document.getElementById("api").addEventListener("click", api, false);
document.getElementById("logout").addEventListener("click", logout, false);
Next, we can use the UserManager
class from the oidc-client
library to manage the OpenID Connect protocol.
It requires similar configuration that was necessary in the MVC Client (albeit with different values).
Add this code to configure and instantiate the UserManager
:
var config = {
authority: "https://localhost:5001",
client_id: "js",
redirect_uri: "https://localhost:5003/callback.html",
response_type: "code",
scope:"openid profile api1",
post_logout_redirect_uri : "https://localhost:5003/index.html",
};
var mgr = new Oidc.UserManager(config);
Next, the UserManager
provides a getUser
API to know if the user is logged into the JavaScript application.
It uses a JavaScript Promise
to return the results asynchronously.
The returned User
object has a profile
property which contains the claims for the user.
Add this code to detect if the user is logged into the JavaScript application:
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
}
else {
log("User not logged in");
}
});
Next, we want to implement the login
, api
, and logout
functions.
The UserManager
provides a signinRedirect
to log the user in, and a signoutRedirect
to log the user out.
The User
object that we obtained in the above code also has an access_token
property which can be used to authenticate to a web API.
The access_token
will be passed to the web API via the Authorization
header with the Bearer
scheme.
Add this code to implement those three functions in our application:
function login() {
mgr.signinRedirect();
}
function api() {
mgr.getUser().then(function (user) {
var url = "https://localhost:6001/identity";
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = function () {
log(xhr.status, JSON.parse(xhr.responseText));
}
xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
xhr.send();
});
}
function logout() {
mgr.signoutRedirect();
}
Note
See the client credentials quickstart for information on how to create the api used in the code above.
callback.html
This HTML file is the designated redirect_uri
page once the user has logged into IdentityServer.
It will complete the OpenID Connect protocol sign-in handshake with IdentityServer.
The code for this is all provided by the UserManager
class we used earlier.
Once the sign-in is complete, we can then redirect the user back to the main index.html
page.
Add this code to complete the signin process:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<script src="oidc-client.js"></script>
<script>
new Oidc.UserManager({response_mode:"query"}).signinRedirectCallback().then(function() {
window.location = "index.html";
}).catch(function(e) {
console.error(e);
});
</script>
</body>
</html>
Add a client registration to IdentityServer for the JavaScript client¶
Now that the client application is ready to go, we need to define a configuration entry in IdentityServer for this new JavaScript client.
In the IdentityServer project locate the client configuration (in Config.cs
).
Add a new Client
to the list for our new JavaScript application.
It should have the configuration listed below:
// JavaScript Client
new Client
{
ClientId = "js",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RedirectUris = { "https://localhost:5003/callback.html" },
PostLogoutRedirectUris = { "https://localhost:5003/index.html" },
AllowedCorsOrigins = { "https://localhost:5003" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
Allowing Ajax calls to the Web API with CORS¶
One last bit of configuration that is necessary is to configure CORS in the web API project. This will allow Ajax calls to be made from https://localhost:5003 to https://localhost:6001.
Configure CORS
Add the CORS services to the dependency injection system in Program.cs
:
builder.Services.AddCors(options =>
{
// this defines a CORS policy called "default"
options.AddPolicy("default", policy =>
{
policy.WithOrigins("https://localhost:5003")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
Add the CORS middleware to the pipeline:
app.UseRouting();
app.UseCors("default");
// ...
Run the JavaScript application¶
Now you should be able to run the JavaScript client application:

Click the “Login” button to sign the user in. Once the user is returned back to the JavaScript application, you should see their profile information:

And click the “API” button to invoke the web API:

And finally click “Logout” to sign the user out.

You now have the start of a JavaScript client application that uses IdentityServer for sign-in, sign-out, and authenticating calls to web APIs.
Using EntityFramework Core for configuration and operational data¶
In the previous quickstarts, we created our client and scope data in code. On startup, IdentityServer loaded this configuration data into memory. If we wanted to modify this configuration data, we had to stop and start IdentityServer.
IdentityServer also generates temporary data, such as authorization codes, consent choices, and refresh tokens. By default, these are also stored in-memory.
To move this data into a database that is persistent between restarts and across multiple IdentityServer instances, we can use the IdentityServer Entity Framework library.
Ourstudio.IdentityServer.EntityFramework¶
Ourstudio.IdentityServer.EntityFramework
implements the required stores and services using the following DbContexts:
ConfigurationDbContext - used for configuration data such as clients, resources, and scopes
PersistedGrantDbContext - used for temporary operational data such as authorization codes, and refresh tokens
These contexts are suitable for any Entity Framework Core compatible relational database.
You can find these contexts, their entities, and the IdentityServer stores that use them in the Ourstudio.IdentityServer.EntityFramework.Storage
nuget package.
You can find the extension methods to register them in your IdentityServer in Ourstudio.IdentityServer.EntityFramework
, which we will do now:
dotnet add package Ourstudio.IdentityServer.EntityFramework
Using SqlServer¶
For this quickstart, we will use the LocalDb version of SQLServer that comes with Visual Studio. To add SQL Server support to our IdentityServer project, you’ll need the following nuget package:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Database Schema Changes and Using EF Migrations¶
The Ourstudio.IdentityServer.EntityFramework.Storage
package contains entity classes that map from IdentityServer’s models.
As IdentityServer’s models change, so will the entity classes in Ourstudio.IdentityServer.EntityFramework.Storage
.
As you use Ourstudio.IdentityServer.EntityFramework.Storage
and upgrade over time, you are responsible for your database schema and changes necessary to that schema as the entity classes change.
One approach for managing those changes is to use EF migrations, which is what we’ll use in this quickstart.
If migrations are not your preference, then you can manage the schema changes in any way you see fit.
Configuring the Stores¶
To start using these stores, you’ll need to replace any existing calls to AddInMemoryClients
, AddInMemoryIdentityResources
, AddInMemoryApiScopes
, AddInMemoryApiResources
, and AddInMemoryPersistedGrants
in your Startup.cs
with AddConfigurationStore
and AddOperationalStore
.
These methods each require a DbContextOptionsBuilder
, meaning your code will look something like this:
var migrationsAssembly = Assembly.GetExecutingAssembly().GetName().Name;
const string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;database=Ourstudio.IdentityServer.Demo.EntityFramework;trusted_connection=yes;";
builder.Services
.AddIdentityServer()
.AddTestUsers(TestUsers.Users)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
});
You might need these namespaces added to the file:
using Microsoft.EntityFrameworkCore;
using System.Reflection;
Because we are using EF migrations in this quickstart, the call to MigrationsAssembly
is used to inform Entity Framework that the host project will contain the migrations code.
This is necessary since the host project is in a different assembly than the one that contains the DbContext
classes.
Adding Migrations¶
Once the IdentityServer has been configured to use Entity Framework, we’ll need to generate some migrations.
To create migrations, you will need to install the Entity Framework Core CLI on your machine and the Microsoft.EntityFrameworkCore.Design
nuget package in IdentityServer:
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
To create the migrations, open a command prompt in the IdentityServer project directory and run the following two commands:
dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb
dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb
You should now see a ~/Data/Migrations/IdentityServer
folder in your project containing the code for your newly created migrations.
Initializing the Database¶
Now that we have the migrations, we can write code to create the database from the migrations. We can also seed the database with the in-memory configuration data that we already defined in the previous quickstarts.
Note
The approach used in this quickstart is used to make it easy to get IdentityServer up and running. You should devise your own database creation and maintenance strategy that is appropriate for your architecture.
In Program.cs
add this method to help initialize the database:
private void InitializeDatabase(IApplicationBuilder app)
{
using var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope()
serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
context.Database.Migrate();
if (!context.Clients.Any())
{
foreach (var client in Config.Clients)
{
context.Clients.Add(client.ToEntity());
}
context.SaveChanges();
}
if (!context.IdentityResources.Any())
{
foreach (var resource in Config.IdentityResources)
{
context.IdentityResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
if (!context.ApiScopes.Any())
{
foreach (var resource in Config.ApiScopes)
{
context.ApiScopes.Add(resource.ToEntity());
}
context.SaveChanges();
}
}
The above code may require you to add the following namespaces to your file:
using System.Linq;
using Ourstudio.IdentityServer.EntityFramework.DbContexts;
using Ourstudio.IdentityServer.EntityFramework.Mappers;
And then we can invoke this before app.Run()
:
InitializeDatabase(app);
app.Run()
Now if you run the Server project, the database should be created and seeded with the quickstart configuration data. You should be able to use SQL Server Management Studio or Visual Studio to connect and inspect the data.

Note
The above InitializeDatabase
helper API is convenient to seed the database, but this approach is not ideal to leave in to execute each time the application runs. Once your database is populated, consider removing the call to the API.
Run the client applications¶
You should now be able to run any of the existing client applications and sign-in, get tokens, and call the API – all based upon the database configuration.
Using ASP.NET Core Identity¶
IdentityServer is designed for flexibility and part of that is allowing you to use any database you want for your users and their data (including passwords). If you are starting with a new user database, then ASP.NET Core Identity is one option you could choose. This quickstart shows how to use ASP.NET Core Identity with IdentityServer.
The approach this quickstart takes to using ASP.NET Core Identity is to create a new project for the IdentityServer host. This new project will replace the prior IdentityServer project we built up in the previous quickstarts. The reason for this new project is due to the differences in UI assets when using ASP.NET Core Identity (mainly around the differences in login and logout). All the other projects in this solution (for the clients and the API) will remain the same.
Note
This quickstart assumes you are familiar with how ASP.NET Core Identity works. If you are not, it is recommended that you first learn about it.
An ASP.NET Core Identity integration project¶
Add a reference to Ourstudio.IdentityServer.AspNetIdentity
.
This NuGet package contains the ASP.NET Core Identity integration components for Ourstudio.IdentityServer.
Program.cs¶
When configuring services, make sure the necessary AddDbContext<ApplicationDbContext>
and AddIdentity<ApplicationUser, IdentityRole>
calls are done to configure ASP.NET Core Identity.
Finally, add a call to AddAspNetIdentity<ApplicationUser>
.
AddAspNetIdentity
adds the integration layer to allow Ourstudio.IdentityServer to access the user data for the ASP.NET Core Identity user database.
This is needed when Ourstudio.IdentityServer must add claims for the users into tokens.
Note that AddIdentity<ApplicationUser, IdentityRole>
must be invoked before AddIdentityServer
.
Config.cs¶
Config.cs
contains the hard-coded in-memory clients and resource definitions.
To keep the same clients and API working as the prior quickstarts, we need to copy over the configuration data from the old IdentityServer project into this one.
Do that now, and afterwards Config.cs
should look like this:
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new []
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiScope> ApiScopes =>
new []
{
new ApiScope("api1", "My API")
};
public static IEnumerable<Client> Clients =>
new []
{
// machine to machine client
new Client
{
ClientId = "client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
// scopes that client has access to
AllowedScopes = { "api1" }
},
// interactive ASP.NET Core MVC client
new Client
{
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
// where to redirect to after login
RedirectUris = { "https://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
};
}
Logging in user accounts¶
Use of the SignInManager<ApplicationUser>
and UserManager<ApplicationUser>
from ASP.NET Core Identity to validate credentials and manage the authentication session.

You should be redirected to the ASP.NET Core Identity login page. Login with your newly created user:

After login you see the normal consent page. After consent you will be redirected back to the MVC client application where your user’s claims should be listed.

You should also be able to click “Call API using application identity” to invoke the API on behalf of the user:

And now you’re using users from ASP.NET Core Identity in IdentityServer.
Startup¶
IdentityServer is a combination of middleware and services. All configuration is done in your startup class.
Configuring services¶
You add the IdentityServer services to the DI system by calling:
public void ConfigureServices(IServiceCollection services)
{
var builder = services.AddIdentityServer();
}
Optionally you can pass in options into this call. See here for details on options.
This will return you a builder object that in turn has a number of convenience methods to wire up additional services.
Key material¶
IdentityServer supports X.509 certificates (both raw files and a reference to the Windows certificate store), RSA keys and EC keys for token signatures and validation. Each key can be configured with a (compatible) signing algorithm, e.g. RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384 or ES512.
You can configure the key material with the following methods:
AddSigningCredential
Adds a signing key that provides the specified key material to the various token creation/validation services.
AddDeveloperSigningCredential
Creates temporary key material at startup time. This is for dev scenarios. The generated key will be persisted in the local directory by default.
AddValidationKey
Adds a key for validating tokens. They will be used by the internal token validator and will show up in the discovery document.
In-Memory configuration stores¶
The various “in-memory” configuration APIs allow for configuring IdentityServer from an in-memory list of configuration objects. These “in-memory” collections can be hard-coded in the hosting application, or could be loaded dynamically from a configuration file or a database. By design, though, these collections are only created when the hosting application is starting up.
Use of these configuration APIs are designed for use when prototyping, developing, and/or testing where it is not necessary to dynamically consult database at runtime for the configuration data. This style of configuration might also be appropriate for production scenarios if the configuration rarely changes, or it is not inconvenient to require restarting the application if the value must be changed.
AddInMemoryClients
Registers
IClientStore
andICorsPolicyService
implementations based on the in-memory collection ofClient
configuration objects.
AddInMemoryIdentityResources
Registers
IResourceStore
implementation based on the in-memory collection ofIdentityResource
configuration objects.
AddInMemoryApiScopes
Registers
IResourceStore
implementation based on the in-memory collection ofApiScope
configuration objects.
AddInMemoryApiResources
Registers
IResourceStore
implementation based on the in-memory collection ofApiResource
configuration objects.
Test stores¶
The TestUser
class models a user, their credentials, and claims in IdentityServer.
Use of TestUser
is similar to the use of the “in-memory” stores in that it is intended for when prototyping, developing, and/or testing.
The use of TestUser
is not recommended in production.
AddTestUsers
Registers
TestUserStore
based on a collection ofTestUser
objects.TestUserStore
is used by the default quickstart UI. Also registers implementations ofIProfileService
andIResourceOwnerPasswordValidator
.
Additional services¶
AddExtensionGrantValidator
Adds
IExtensionGrantValidator
implementation for use with extension grants.
AddSecretParser
Adds
ISecretParser
implementation for parsing client or API resource credentials.
AddSecretValidator
Adds
ISecretValidator
implementation for validating client or API resource credentials against a credential store.
AddResourceOwnerValidator
Adds
IResourceOwnerPasswordValidator
implementation for validating user credentials for the resource owner password credentials grant type.
AddProfileService
Adds
IProfileService
implementation for connecting to your custom user profile store. TheDefaultProfileService
class provides the default implementation which relies upon the authentication cookie as the only source of claims for issuing in tokens.
AddAuthorizeInteractionResponseGenerator
Adds
IAuthorizeInteractionResponseGenerator
implementation to customize logic at authorization endpoint for when a user must be shown a UI for error, login, consent, or any other custom page. TheAuthorizeInteractionResponseGenerator
class provides a default implementation, so consider deriving from this existing class if you need to augment the existing behavior.
AddCustomAuthorizeRequestValidator
Adds
ICustomAuthorizeRequestValidator
implementation to customize request parameter validation at the authorization endpoint.
AddCustomTokenRequestValidator
Adds
ICustomTokenRequestValidator
implementation to customize request parameter validation at the token endpoint.
AddRedirectUriValidator
Adds
IRedirectUriValidator
implementation to customize redirect URI validation.
AddAppAuthRedirectUriValidator
Adds a an “AppAuth” (OAuth 2.0 for Native Apps) compliant redirect URI validator (does strict validation but also allows http://127.0.0.1 with random port).
AddJwtBearerClientAuthentication
Adds support for client authentication using JWT bearer assertions.
AddMutualTlsSecretValidators
Adds the X509 secret validators for mutual TLS.
Caching¶
Client and resource configuration data is used frequently by IdentityServer. If this data is being loaded from a database or other external store, then it might be expensive to frequently re-load the same data.
AddInMemoryCaching
To use any of the caches described below, an implementation of
ICache<T>
must be registered in DI. This API registers a default in-memory implementation ofICache<T>
that’s based on ASP.NET Core’sMemoryCache
.
AddClientStoreCache
Registers a
IClientStore
decorator implementation which will maintain an in-memory cache ofClient
configuration objects. The cache duration is configurable on theCaching
configuration options on theIdentityServerOptions
.
AddResourceStoreCache
Registers a
IResourceStore
decorator implementation which will maintain an in-memory cache ofIdentityResource
andApiResource
configuration objects. The cache duration is configurable on theCaching
configuration options on theIdentityServerOptions
.
AddCorsPolicyCache
Registers a
ICorsPolicyService
decorator implementation which will maintain an in-memory cache of the results of the CORS policy service evaluation. The cache duration is configurable on theCaching
configuration options on theIdentityServerOptions
.
Further customization of the cache is possible:
The default caching relies upon the ICache<T>
implementation.
If you wish to customize the caching behavior for the specific configuration objects, you can replace this implementation in the dependency injection system.
The default implementation of the ICache<T>
itself relies upon the IMemoryCache
interface (and MemoryCache
implementation) provided by .NET.
If you wish to customize the in-memory caching behavior, you can replace the IMemoryCache
implementation in the dependency injection system.
Configuring the pipeline¶
You need to add IdentityServer to the pipeline by calling:
public void Configure(IApplicationBuilder app)
{
app.UseIdentityServer();
}
Note
UseIdentityServer
includes a call to UseAuthentication
, so it’s not necessary to have both.
There is no additional configuration for the middleware.
Be aware that order matters in the pipeline. For example, you will want to add IdentitySever before the UI framework that implements the login screen.
Defining Resources¶
The ultimate job of an OpenID Connect/OAuth token service is to control access to resources.
The two fundamental resource types in IdentityServer are:
identity resources: represent claims about a user like user ID, display name, email address etc…
API resources: represent functionality a client wants to access. Typically, they are HTTP-based endpoints (aka APIs), but could be also message queuing endpoints or similar.
Note
You can define resources using a C# object model - or load them from a data store. An implementation of IResourceStore
deals with these low-level details. For this document we are using the in-memory implementation.
Identity Resources¶
An identity resource is a named group of claims that can be requested using the scope parameter.
The OpenID Connect specification suggests a couple of standard scope name to claim type mappings that might be useful to you for inspiration, but you can freely design them yourself.
One of them is actually mandatory, the openid scope, which tells the provider to return the sub (subject id) claim in the identity token.
This is how you could define the openid scope in code:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResource(
name: "openid",
userClaims: new[] { "sub" },
displayName: "Your user identifier")
};
}
But since this is one of the standard scopes from the spec you can shorten that to:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId()
};
}
Note
see the reference section for more information on IdentityResource
.
The following example shows a custom identity resource called profile that represents the display name, email address and website claim:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResource(
name: "profile",
userClaims: new[] { "name", "email", "website" },
displayName: "Your profile data")
};
}
Once the resource is defined, you can give access to it to a client via the AllowedScopes
option (other properties omitted):
var client = new Client
{
ClientId = "client",
AllowedScopes = { "openid", "profile" }
};
The client can then request the resource using the scope parameter (other parameters omitted):
https://demo.identityserver.io/connect/authorize?client_id=client&scope=openid profile
IdentityServer will then use the scope names to create a list of requested claim types, and present that to your implementation of the profile service.
APIs¶
Designing your API surface can be a complicated task. IdentityServer provides a couple of primitives to help you with that.
The original OAuth 2.0 specification has the concept of scopes, which is just defined as the scope of access that the client requests. Technically speaking, the scope parameter is a list of space delimited values - you need to provide the structure and semantics of it.
In more complex systems, often the notion of a resource is introduced. This might be e.g. a physical or logical API. In turn each API can potentially have scopes as well. Some scopes might be exclusive to that resource, and some scopes might be shared.
Let’s start with simple scopes first, and then we’ll have a look how resources can help structure scopes.
Scopes¶
Let’s model something very simple - a system that has three logical operations read, write, and delete.
You can define them using the ApiScope
class:
public static IEnumerable<ApiScope> GetApiScopes()
{
return new List<ApiScope>
{
new ApiScope(name: "read", displayName: "Read your data."),
new ApiScope(name: "write", displayName: "Write your data."),
new ApiScope(name: "delete", displayName: "Delete your data.")
};
}
You can then assign the scopes to various clients, e.g.:
var webViewer = new Client
{
ClientId = "web_viewer",
AllowedScopes = { "openid", "profile", "read" }
};
var mobileApp = new Client
{
ClientId = "mobile_app",
AllowedScopes = { "openid", "profile", "read", "write", "delete" }
}
Parameterized Scopes¶
Sometimes scopes have a certain structure, e.g. a scope name with an additional parameter: transaction:id or read_patient:patientid.
In this case you would create a scope without the parameter part and assign that name to a client, but in addition provide some logic to parse the structure
of the scope at runtime using the IScopeParser
interface or by deriving from our default implementation, e.g.:
public class ParameterizedScopeParser : DefaultScopeParser
{
public ParameterizedScopeParser(ILogger<DefaultScopeParser> logger) : base(logger)
{
}
public override void ParseScopeValue(ParseScopeContext scopeContext)
{
const string transactionScopeName = "transaction";
const string separator = ":";
const string transactionScopePrefix = transactionScopeName + separator;
var scopeValue = scopeContext.RawValue;
if (scopeValue.StartsWith(transactionScopePrefix))
{
// we get in here with a scope like "transaction:something"
var parts = scopeValue.Split(separator, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
scopeContext.SetParsedValues(transactionScopeName, parts[1]);
}
else
{
scopeContext.SetError("transaction scope missing transaction parameter value");
}
}
else if (scopeValue != transactionScopeName)
{
// we get in here with a scope not like "transaction"
base.ParseScopeValue(scopeContext);
}
else
{
// we get in here with a scope exactly "transaction", which is to say we're ignoring it
// and not including it in the results
scopeContext.SetIgnore();
}
}
}
You then have access to the parsed value throughout the pipeline, e.g. in the profile service:
public class HostProfileService : IProfileService
{
public override async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var transaction = context.RequestedResources.ParsedScopes.FirstOrDefault(x => x.ParsedName == "transaction");
if (transaction?.ParsedParameter != null)
{
context.IssuedClaims.Add(new Claim("transaction_id", transaction.ParsedParameter));
}
}
}
API Resources¶
When the API surface gets larger, a flat list of scopes like the one used above might not be feasible.
You typically need to introduce some sort of namespacing to organize the scope names, and maybe you also want to group them together and get some higher-level constructs like an audience claim in access tokens. You might also have scenarios, where multiple resources should support the same scope names, whereas sometime you explicitly want to isolate a scope to a certain resource.
In IdentityServer, the ApiResource
class allows some additional organization. Let’s use the following scope definition:
public static IEnumerable<ApiScope> GetApiScopes()
{
return new List<ApiScope>
{
// invoice API specific scopes
new ApiScope(name: "invoice.read", displayName: "Reads your invoices."),
new ApiScope(name: "invoice.pay", displayName: "Pays your invoices."),
// customer API specific scopes
new ApiScope(name: "customer.read", displayName: "Reads you customers information."),
new ApiScope(name: "customer.contact", displayName: "Allows contacting one of your customers."),
// shared scope
new ApiScope(name: "manage", displayName: "Provides administrative access to invoice and customer data.")
};
}
With ApiResource
you can now create two logical APIs and their corresponding scopes:
public static readonly IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("invoice", "Invoice API")
{
Scopes = { "invoice.read", "invoice.pay", "manage" }
},
new ApiResource("customer", "Customer API")
{
Scopes = { "customer.read", "customer.contact", "manage" }
}
};
}
Using the API resource grouping gives you the following additional features
support for the JWT aud claim. The value(s) of the audience claim will be the name of the API resource(s)
support for adding common user claims across all contained scopes
support for introspection by assigning an API secret to the resource
support for configuring the access token signing algorithm for the resource
Let’s have a look at some example access tokens for the above resource configuration.
Client requests invoice.read and invoice.pay:
{
"typ": "at+jwt"
}.
{
"client_id": "client",
"sub": "123",
"aud": "invoice",
"scope": "invoice.read invoice.pay"
}
Client requests invoice.read and customer.read:
{
"typ": "at+jwt"
}.
{
"client_id": "client",
"sub": "123",
"aud": [ "invoice", "customer" ]
"scope": "invoice.read customer.read"
}
Client requests manage:
{
"typ": "at+jwt"
}.
{
"client_id": "client",
"sub": "123",
"aud": [ "invoice", "customer" ]
"scope": "manage"
}
Migration steps to v4¶
As described above, starting with v4, scopes have their own definition and can optionally be referenced by resources. Before v4, scopes were always contained within a resource.
To migrate to v4 you need to split up scope and resource registration, typically by first registering all your scopes
(e.g. using the AddInMemoryApiScopes
method), and then register the API resources (if any) afterwards.
The API resources will then reference the prior registered scopes by name.
Defining Clients¶
Clients represent applications that can request tokens from your identityserver.
The details vary, but you typically define the following common settings for a client:
a unique client ID
a secret if needed
the allowed interactions with the token service (called a grant type)
a network location where identity and/or access token gets sent to (called a redirect URI)
a list of scopes (aka resources) the client is allowed to access
Note
At runtime, clients are retrieved via an implementation of the IClientStore
. This allows loading them from arbitrary data sources like config files or databases. For this document we will use the in-memory version of the client store. You can wire up the in-memory store in ConfigureServices
via the AddInMemoryClients
extensions method.
Defining a client for server to server communication¶
In this scenario no interactive user is present - a service (aka client) wants to communicate with an API (aka scope):
public class Clients
{
public static IEnumerable<Client> Get()
{
return new List<Client>
{
new Client
{
ClientId = "service.client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = { "api1", "api2.read_only" }
}
};
}
}
Defining an interactive application for use authentication and delegated API access¶
Interactive applications (e.g. web applications or native desktop/mobile) applications use the authorization code flow. This flow gives you the best security because the access tokens are transmitted via back-channel calls only (and gives you access to refresh tokens):
var interactiveClient = new Client
{
ClientId = "interactive",
AllowedGrantTypes = GrantTypes.Code,
AllowOfflineAccess = true,
ClientSecrets = { new Secret("secret".Sha256()) },
RedirectUris = { "http://localhost:21402/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:21402/" },
FrontChannelLogoutUri = "http://localhost:21402/signout-oidc",
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"api1", "api2.read_only"
},
};
Note
see the grant types topic for more information on choosing the right grant type for your client.
Defining clients in appsettings.json¶
The AddInMemoryClients
extensions method also supports adding clients from the ASP.NET Core configuration file. This allows you to define static clients directly from the appsettings.json file:
"IdentityServer": {
"IssuerUri": "urn:sso.company.com",
"Clients": [
{
"Enabled": true,
"ClientId": "local-dev",
"ClientName": "Local Development",
"ClientSecrets": [ { "Value": "<Insert Sha256 hash of the secret encoded as Base64 string>" } ],
"AllowedGrantTypes": [ "client_credentials" ],
"AllowedScopes": [ "api1" ],
}
]
}
Then pass the configuration section to the AddInMemoryClients
method:
AddInMemoryClients(configuration.GetSection("IdentityServer:Clients"))
Sign-in¶
In order for IdentityServer to issue tokens on behalf of a user, that user must sign-in to IdentityServer.
Login User Interface and Identity Management System¶
IdentityServer does not provide any user-interface or user database for user authentication. These are things you are expected to provide or develop yourself.
Login Workflow¶
When IdentityServer receives a request at the authorization endpoint and the user is not authenticated, the user will be redirected to the configured login page.
You must inform IdentityServer of the path to your login page via the UserInteraction
settings on the options (the default is /account/login
).
A returnUrl
parameter will be passed informing your login page where the user should be redirected once login is complete.

Note
Beware open-redirect attacks via the returnUrl
parameter. You should validate that the returnUrl
refers to well-known location. See the interaction service for APIs to validate the returnUrl
parameter.
Login Context¶
On your login page you might require information about the context of the request in order to customize the login experience
(such as client, prompt parameter, IdP hint, or something else).
This is made available via the GetAuthorizationContextAsync
API on the interaction service.
Sign-in with External Identity Providers¶
ASP.NET Core has a flexible way to deal with external authentication. This involves a couple of steps.
Note
If you are using ASP.NET Identity, many of the underlying technical details are hidden from you. It is recommended that you also read the Microsoft docs and do the ASP.NET Identity quickstart.
Adding authentication handlers for external providers¶
The protocol implementation that is needed to talk to an external provider is encapsulated in an authentication handler. Some providers use proprietary protocols (e.g. social providers like Facebook) and some use standard protocols, e.g. OpenID Connect, WS-Federation or SAML2p.
See this quickstart for step-by-step instructions for adding external authentication and configuring it.
Triggering the authentication handler¶
You invoke an external authentication handler via the ChallengeAsync
extension method on the HttpContext
(or using the MVC ChallengeResult
).
You typically want to pass in some options to the challenge operation, e.g. the path to your callback page and the name of the provider for bookkeeping, e.g.:
var callbackUrl = Url.Action("ExternalLoginCallback");
var props = new AuthenticationProperties
{
RedirectUri = callbackUrl,
Items =
{
{ "scheme", provider },
{ "returnUrl", returnUrl }
}
};
return Challenge(provider, props);
Handling the callback and signing in the user¶
On the callback page your typical tasks are:
inspect the identity returned by the external provider.
make a decision how you want to deal with that user. This might be different based on the fact if this is a new user or a returning user.
new users might need additional steps and UI before they are allowed in.
probably create a new internal user account that is linked to the external provider.
store the external claims that you want to keep.
delete the temporary cookie
sign-in the user
Inspecting the external identity:
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
// retrieve claims of the external user
var externalUser = result.Principal;
if (externalUser == null)
{
throw new Exception("External authentication error");
}
// retrieve claims of the external user
var claims = externalUser.Claims.ToList();
// try to determine the unique id of the external user - the most common claim type for that are the sub claim and the NameIdentifier
// depending on the external provider, some other claim type might be used
var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);
if (userIdClaim == null)
{
userIdClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
}
if (userIdClaim == null)
{
throw new Exception("Unknown userid");
}
var externalUserId = userIdClaim.Value;
var externalProvider = userIdClaim.Issuer;
// use externalProvider and externalUserId to find your user, or provision a new user
Clean-up and sign-in:
// issue authentication cookie for user
await HttpContext.SignInAsync(new IdentityServerUser(user.SubjectId) {
DisplayName = user.Username,
IdentityProvider = provider,
AdditionalClaims = additionalClaims,
AuthenticationTime = DateTime.Now
});
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// validate return URL and redirect back to authorization endpoint or a local page
if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect("~/");
State, URL length, and ISecureDataFormat¶
When redirecting to an external provider for sign-in, frequently state from the client application must be round-tripped.
This means that state is captured prior to leaving the client and preserved until the user has returned to the client application.
Many protocols, including OpenID Connect, allow passing some sort of state as a parameter as part of the request, and the identity provider will return that state on the response.
The OpenID Connect authentication handler provided by ASP.NET Core utilizes this feature of the protocol, and that is how it implements the returnUrl
feature mentioned above.
The problem with storing state in a request parameter is that the request URL can get too large (over the common limit of 2000 characters).
The OpenID Connect authentication handler does provide an extensibility point to store the state in your server, rather than in the request URL.
You can implement this yourself by implementing ISecureDataFormat<AuthenticationProperties>
and configuring it on the OpenIdConnectOptions.
Fortunately, IdentityServer provides an implementation of this for you, backed by the IDistributedCache
implementation registered in the DI container (e.g. the standard MemoryDistributedCache
).
To use the IdentityServer provided secure data format implementation, simply call the AddOidcStateDataFormatterCache
extension method on the IServiceCollection
when configuring DI.
If no parameters are passed, then all OpenID Connect handlers configured will use the IdentityServer provided secure data format implementation:
public void ConfigureServices(IServiceCollection services)
{
// configures the OpenIdConnect handlers to persist the state parameter into the server-side IDistributedCache.
services.AddOidcStateDataFormatterCache();
services.AddAuthentication()
.AddOpenIdConnect("demoidsrv", "IdentityServer", options =>
{
// ...
})
.AddOpenIdConnect("aad", "Azure AD", options =>
{
// ...
})
.AddOpenIdConnect("adfs", "ADFS", options =>
{
// ...
});
}
If only particular schemes are to be configured, then pass those schemes as parameters:
public void ConfigureServices(IServiceCollection services)
{
// configures the OpenIdConnect handlers to persist the state parameter into the server-side IDistributedCache.
services.AddOidcStateDataFormatterCache("aad", "demoidsrv");
services.AddAuthentication()
.AddOpenIdConnect("demoidsrv", "IdentityServer", options =>
{
// ...
})
.AddOpenIdConnect("aad", "Azure AD", options =>
{
// ...
})
.AddOpenIdConnect("adfs", "ADFS", options =>
{
// ...
});
}
Windows Authentication¶
There are several ways how you can enable Windows authentication in ASP.NET Core (and thus in IdentityServer).
On Windows using IIS hosting (both in- and out-of process)
On Windows using HTTP.SYS hosting
On any platform using the Negotiate authentication handler (added in ASP.NET Core 3.0)
Note
We only have documentation for IIS hosting. If you want to contribute to the docs, please open a PR. thanks!
On Windows using IIS hosting¶
The typical CreateDefaultBuilder
host setup enables support for IIS-based Windows authentication when hosting in IIS.
Make sure that Windows authentication is enabled in launchSettings.json
or your IIS configuration.
The IIS integration layer will configure a Windows authentication handler into DI that can be invoked via the authentication service. Typically in IdentityServer it is advisable to disable the automatic behavior.
This is done in ConfigureServices
(details vary depending on in-proc vs out-of-proc hosting):
// configures IIS out-of-proc settings (see https://github.com/aspnet/AspNetCore/issues/14882)
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
// ..or configures IIS in-proc settings
services.Configure<IISServerOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
You trigger Windows authentication by calling ChallengeAsync
on the Windows
scheme (or if you want to use a constant: Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme
).
This will send the Www-Authenticate
header back to the browser which will then re-load the current URL including the Windows identity.
You can tell that Windows authentication was successful, when you call AuthenticateAsync
on the Windows
scheme and the principal returned
is of type WindowsPrincipal
.
The principal will have information like user and group SID and the Windows account name. The following snippet shows how to
trigger authentication, and if successful convert the information into a standard ClaimsPrincipal
for the temp-Cookie approach:
private async Task<IActionResult> ChallengeWindowsAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync("Windows");
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, treating windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", "Windows" },
}
};
var id = new ClaimsIdentity("Windows");
// the sid is a good sub value
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.FindFirst(ClaimTypes.PrimarySid).Value));
// the account name is the closest we have to a display name
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
// add the groups as claims -- be careful if the number of groups is too large
var wi = wp.Identity as WindowsIdentity;
// translate group SIDs to display names
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
await HttpContext.SignInAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props);
return Redirect(props.RedirectUri);
}
else
{
// trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge("Windows");
}
}
Sign-out¶
Signing out of IdentityServer is as simple as removing the authentication cookie, but for doing a complete federated sign-out, we must consider signing the user out of the client applications (and maybe even up-stream identity providers) as well.
Notifying clients that the user has signed-out¶
As part of the signout process you will want to ensure client applications are informed that the user has signed out. IdentityServer supports the front-channel specification for server-side clients (e.g. MVC), the back-channel specification for server-side clients (e.g. MVC), and the session management specification for browser-based JavaScript clients (e.g. SPA, React, Angular, etc.).
Front-channel server-side clients
To signout the user from the server-side client applications via the front-channel spec, the “logged out” page in IdentityServer must render an <iframe>
to notify the clients that the user has signed out.
Clients that wish to be notified must have the FrontChannelLogoutUri
configuration value set.
IdentityServer tracks which clients the user has signed into, and provides an API called GetLogoutContextAsync
on the IIdentityServerInteractionService
(details).
This API returns a LogoutRequest
object with a SignOutIFrameUrl
property that your logged out page must render into an <iframe>
.
Back-channel server-side clients
To signout the user from the server-side client applications via the back-channel spec the IBackChannelLogoutService
service can be used.
IdentityServer will automatically use this service when your logout page removes the user’s authentication cookie via a call to HttpContext.SignOutAsync
.
Clients that wish to be notified must have the BackChannelLogoutUri
configuration value set.
Browser-based JavaScript clients
Given how the session management specification is designed, there is nothing special in IdentityServer that you need to do to notify these clients that the user has signed out. The clients, though, must perform monitoring on the check_session_iframe, and this is implemented by the oidc-client JavaScript library.
Sign-out initiated by a client application¶
If sign-out was initiated by a client application, then the client first redirected the user to the end session endpoint. Processing at the end session endpoint might require some temporary state to be maintained (e.g. the client’s post logout redirect uri) across the redirect to the logout page. This state might be of use to the logout page, and the identifier for the state is passed via a logoutId parameter to the logout page.
The GetLogoutContextAsync
API on the interaction service can be used to load the state.
Of interest on the LogoutRequest
model context class is the ShowSignoutPrompt
which indicates if the request for sign-out has been authenticated, and therefore it’s safe to not prompt the user for sign-out.
By default this state is managed as a protected data structure passed via the logoutId value.
If you wish to use some other persistence between the end session endpoint and the logout page, then you can implement IMessageStore<LogoutMessage>
and register the implementation in DI.
Sign-out of External Identity Providers¶
When a user is signing-out of IdentityServer, and they have used an external identity provider to sign-in then it is likely that they should be redirected to also sign-out of the external provider. Not all external providers support sign-out, as it depends on the protocol and features they support.
To detect that a user must be redirected to an external identity provider for sign-out is typically done by using a idp
claim issued into the cookie at IdentityServer.
The value set into this claim is the AuthenticationScheme
of the corresponding authentication middleware.
At sign-out time this claim is consulted to know if an external sign-out is required.
Redirecting the user to an external identity provider is problematic due to the cleanup and state management already required by the normal sign-out workflow. The only way to then complete the normal sign-out and cleanup process at IdentityServer is to then request from the external identity provider that after its logout that the user be redirected back to IdentityServer. Not all external providers support post-logout redirects, as it depends on the protocol and features they support.
The workflow at sign-out is then to revoke IdentityServer’s authentication cookie, and then redirect to the external provider requesting a post-logout redirect.
The post-logout redirect should maintain the necessary sign-out state described here (i.e. the logoutId
parameter value).
To redirect back to IdentityServer after the external provider sign-out, the RedirectUri
should be used on the AuthenticationProperties
when using ASP.NET Core’s SignOutAsync
API, for example:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
// build a model so the logged out page knows what to display
var vm = await _account.BuildLoggedOutViewModelAsync(model.LogoutId);
var user = HttpContext.User;
if (user?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await HttpContext.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(user.GetSubjectId(), user.GetName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
}
Once the user is signed-out of the external provider and then redirected back, the normal sign-out processing at IdentityServer should execute which involves processing the logoutId
and doing all necessary cleanup.
Federated Sign-out¶
Federated sign-out is the situation where a user has used an external identity provider to log into IdentityServer, and then the user logs out of that external identity provider via a workflow unknown to IdentityServer. When the user signs out, it will be useful for IdentityServer to be notified so that it can sign the user out of IdentityServer and all of the applications that use IdentityServer.
Not all external identity providers support federated sign-out, but those that do will provide a mechanism to notify clients that the user has signed out.
This notification usually comes in the form of a request in an <iframe>
from the external identity provider’s “logged out” page.
IdentityServer must then notify all of its clients (as discussed here), also typically in the form of a request in an <iframe>
from within the external identity provider’s <iframe>
.
What makes federated sign-out a special case (when compared to a normal sign-out) is that the federated sign-out request is not to the normal sign-out endpoint in IdentityServer. In fact, each external IdentityProvider will have a different endpoint into your IdentityServer host. This is due to that fact that each external identity provider might use a different protocol, and each middleware listens on different endpoints.
The net effect of all of these factors is that there is no “logged out” page being rendered as we would on the normal sign-out workflow, which means we are missing the sign-out notifications to IdentityServer’s clients. We must add code for each of these federated sign-out endpoints to render the necessary notifications to achieve federated sign-out.
Fortunately IdentityServer already contains this code.
When requests come into IdentityServer and invoke the handlers for external authentication providers, IdentityServer detects if these are federated signout requests and if they are it will automatically render the same <iframe>
as described here for signout.
In short, federated signout is automatically supported.
Federation Gateway¶
A common architecture is the so-called federation gateway. In this approach IdentityServer acts as a gateway to one or more external identity providers.

This architecture has the following advantages
your applications only need to know about the one token service (the gateway) and are shielded from all the details about connecting to the external provider(s). This also means that you can add or change those external providers without needing to update your applications.
you control the gateway (as opposed to some external service provider) - this means you can make any changes to it and can protect your applications from changes those external providers might do to their own services.
most external providers only support a fixed set of claims and claim types - having a gateway in the middle allows post-processing the response from the providers to transform/add/amend domain specific identity information.
some providers don’t support access tokens (e.g. social providers) - since the gateway knows about your APIs, it can issue access tokens based on the external identities.
some providers charge by the number of applications you connect to them. The gateway acts as a single application to the external provider. Internally you can connect as many applications as you want.
some providers use proprietary protocols or made proprietary modifications to standard protocols - with a gateway there is only one place you need to deal with that.
forcing every authentication (internal or external) through one single place gives you tremendous flexibility with regards to identity mapping, providing a stable identity to all your applications and dealing with new requirements
In other words - owning your federation gateway gives you a lot of control over your identity infrastructure. And since the identity of your users is one of your most important assets, we recommend taking control over the gateway.
Consent¶
During an authorization request, if IdentityServer requires user consent the browser will be redirected to the consent page.
Consent is used to allow an end user to grant a client access to resources (identity or API). This is typically only necessary for third-party clients, and can be enabled/disabled per-client on the client settings.
Consent Page¶
In order for the user to grant consent, a consent page must be provided by the hosting application.
A consent page normally renders the display name of the current user, the display name of the client requesting access, the logo of the client, a link for more information about the client, and the list of resources the client is requesting access to. It’s also common to allow the user to indicate that their consent should be “remembered” so they are not prompted again in the future for the same client.
Once the user has provided consent, the consent page must inform IdentityServer of the consent, and then the browser must be redirected back to the authorization endpoint.
Informing IdentityServer of the consent result¶
The GrantConsentAsync
API on the interaction service allows the consent page to inform IdentityServer of the outcome of consent (which might also be to deny the client access).
IdentityServer will temporarily persist the outcome of the consent.
This persistence uses a cookie by default, as it only needs to last long enough to convey the outcome back to the authorization endpoint.
This temporary persistence is different than the persistence used for the “remember my consent” feature (and it is the authorization endpoint which persists the “remember my consent” for the user).
If you wish to use some other persistence between the consent page and the authorization redirect, then you can implement IMessageStore<ConsentResponse>
and register the implementation in DI.
Protecting APIs¶
IdentityServer issues access tokens in the JWT (JSON Web Token) format by default.
Every relevant platform today has support for validating JWT tokens, a good list of JWT libraries can be found here. Popular libraries are e.g.:
JWT bearer authentication handler for ASP.NET Core
JWT bearer authentication middleware for Katana
Protecting an ASP.NET Core-based API is only a matter of adding the JWT bearer authentication handler:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// base-address of your identityserver
options.Authority = "https://demo.identityserver.io";
// if you are using API resources, you can specify the name here
options.Audience = "resource1";
// IdentityServer emits a typ header by default, recommended extra check
options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
});
}
}
Note
If you are not using the audience claim, you can turn off the audience check via options.TokenValidationParameters.ValidateAudience = false;
. See here for more information on resources, scopes, audiences and authorization.
Validating reference tokens¶
If you are using reference tokens, you need an authentication handler that implements OAuth 2.0 token introspection, e.g. this one:
services.AddAuthentication("token")
.AddOAuth2Introspection("token", options =>
{
options.Authority = Constants.Authority;
// this maps to the API resource name and secret
options.ClientId = "resource1";
options.ClientSecret = "secret";
});
Supporting both JWTs and reference tokens¶
You can setup ASP.NET Core to dispatch to the right handler based on the incoming token, see this blog post for more information. In this case you setup one default handler, and some forwarding logic, e.g.:
services.AddAuthentication("token")
// JWT tokens
.AddJwtBearer("token", options =>
{
options.Authority = Constants.Authority;
options.Audience = "resource1";
options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
// if token does not contain a dot, it is a reference token
options.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection");
})
// reference tokens
.AddOAuth2Introspection("introspection", options =>
{
options.Authority = Constants.Authority;
options.ClientId = "resource1";
options.ClientSecret = "secret";
});
Deployment¶
Your identity server is just a standard ASP.NET Core application including the IdentityServer middleware. Read the official Microsoft documentation on publishing and deployment first (and especially the section about load balancers and proxies).
Typical architecture¶
Typically you will design your IdentityServer deployment for high availability:

IdentityServer itself is stateless and does not require server affinity - but there is data that needs to be shared between the instances.
Configuration data¶
This typically includes:
resources
clients
startup configuration, e.g. key material, external provider settings etc…
The way you store that data depends on your environment. In situations where configuration data rarely changes we recommend using the in-memory stores and code or configuration files.
In highly dynamic environments (e.g. Saas) we recommend using a database or configuration service to load configuration dynamically.
IdentityServer supports code configuration and configuration files (see here) out of the box. For databases we provide support for Entity Framework Core based databases.
You can also build your own configuration stores by implementing IResourceStore
and IClientStore
.
Key material¶
Another important piece of startup configuration is your key material, see here for more details on key material and cryptography.
Operational data¶
For certain operations, IdentityServer needs a persistence store to keep state, this includes:
issuing authorization codes
issuing reference and refresh tokens
storing consent
You can either use a traditional database for storing operational data, or use a cache with persistence features like Redis. The EF Core implementation mentioned above has also support for operational data.
You can also implement support for your own custom storage mechanism by implementing IPersistedGrantStore
- by default IdentityServer injects an in-memory version.
ASP.NET Core data protection¶
ASP.NET Core itself needs shared key material for protecting sensitive data like cookies, state strings etc. See the official docs here.
You can either re-use one of the above persistence store or use something simple like a shared file if possible.
ASP.NET Core distributed caching¶
Some components rely on ASP.NET Core distributed caching. In order to work in a multi server environment, this needs to be set up correctly. The official docs describe several options.
The following components rely on IDistributedCache
:
services.AddOidcStateDataFormatterCache()
configures the OpenIdConnect handlers to persist the state parameter into the server-sideIDistributedCache
.DefaultReplayCache
DistributedDeviceFlowThrottlingService
DistributedCacheAuthorizationParametersMessageStore
Logging¶
IdentityServer uses the standard logging facilities provided by ASP.NET Core. The Microsoft documentation has a good intro and a description of the built-in logging providers.
We are roughly following the Microsoft guidelines for usage of log levels:
Trace
For information that is valuable only to a developer troubleshooting an issue. These messages may contain sensitive application data like tokens and should not be enabled in a production environment.Debug
For following the internal flow and understanding why certain decisions are made. Has short-term usefulness during development and debugging.Information
For tracking the general flow of the application. These logs typically have some long-term value.Warning
For abnormal or unexpected events in the application flow. These may include errors or other conditions that do not cause the application to stop, but which may need to be investigated.Error
For errors and exceptions that cannot be handled. Examples: failed validation of a protocol request.Critical
For failures that require immediate attention. Examples: missing store implementation, invalid key material…
Setup for Serilog¶
We personally like Serilog and the Serilog.AspNetCore
package a lot. Give it a try:
public class Program
{
public static int Main(string[] args)
{
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code)
.CreateLogger();
try
{
Log.Information("Starting host...");
CreateHostBuilder(args).Build().Run();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly.");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Events¶
While logging is more low level “printf” style - events represent higher level information about certain operations in IdentityServer. Events are structured data and include event IDs, success/failure information, categories and details. This makes it easy to query and analyze them and extract useful information that can be used for further processing.
Events work great with event stores like ELK, Seq or Splunk.
Emitting events¶
Events are not turned on by default - but can be globally configured in the ConfigureServices
method, e.g.:
services.AddIdentityServer(options =>
{
options.Events.RaiseSuccessEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseErrorEvents = true;
});
To emit an event use the IEventService
from the DI container and call the RaiseAsync
method, e.g.:
public async Task<IActionResult> Login(LoginInputModel model)
{
if (_users.ValidateCredentials(model.Username, model.Password))
{
// issue authentication cookie with subject ID and username
var user = _users.FindByUsername(model.Username);
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username));
}
else
{
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
}
}
Custom sinks¶
Our default event sink will simply serialize the event class to JSON and forward it to the ASP.NET Core logging system.
If you want to connect to a custom event store, implement the IEventSink
interface and register it with DI.
The following example uses Seq to emit events:
public class SeqEventSink : IEventSink
{
private readonly Logger _log;
public SeqEventSink()
{
_log = new LoggerConfiguration()
.WriteTo.Seq("http://localhost:5341")
.CreateLogger();
}
public Task PersistAsync(Event evt)
{
if (evt.EventType == EventTypes.Success ||
evt.EventType == EventTypes.Information)
{
_log.Information("{Name} ({Id}), Details: {@details}",
evt.Name,
evt.Id,
evt);
}
else
{
_log.Error("{Name} ({Id}), Details: {@details}",
evt.Name,
evt.Id,
evt);
}
return Task.CompletedTask;
}
}
Add the Serilog.Sinks.Seq
package to your host to make the above code work.
Built-in events¶
The following events are defined in IdentityServer:
ApiAuthenticationFailureEvent
&ApiAuthenticationSuccessEvent
Gets raised for successful/failed API authentication at the introspection endpoint.
ClientAuthenticationSuccessEvent
&ClientAuthenticationFailureEvent
Gets raised for successful/failed client authentication at the token endpoint.
TokenIssuedSuccessEvent
&TokenIssuedFailureEvent
Gets raised for successful/failed attempts to request identity tokens, access tokens, refresh tokens and authorization codes.
TokenIntrospectionSuccessEvent
&TokenIntrospectionFailureEvent
Gets raised for successful token introspection requests.
TokenRevokedSuccessEvent
Gets raised for successful token revocation requests.
UserLoginSuccessEvent
&UserLoginFailureEvent
Gets raised by the quickstart UI for successful/failed user logins.
UserLogoutSuccessEvent
Gets raised for successful logout requests.
ConsentGrantedEvent
&ConsentDeniedEvent
Gets raised in the consent UI.
UnhandledExceptionEvent
Gets raised for unhandled exceptions.
DeviceAuthorizationFailureEvent
&DeviceAuthorizationSuccessEvent
Gets raised for successful/failed device authorization requests.
Custom events¶
You can create your own events and emit them via our infrastructure.
You need to derive from our base Event
class which injects contextual information like activity ID, timestamp, etc.
Your derived class can then add arbitrary data fields specific to the event context:
public class UserLoginFailureEvent : Event
{
public UserLoginFailureEvent(string username, string error)
: base(EventCategories.Authentication,
"User Login Failure",
EventTypes.Failure,
EventIds.UserLoginFailure,
error)
{
Username = username;
}
public string Username { get; set; }
}
Cryptography, Keys and HTTPS¶
IdentityServer relies on a couple of crypto mechanisms to do its job.
Token signing and validation¶
IdentityServer needs an asymmetric key pair to sign and validate JWTs. This keymaterial can be either packaged as a certificate or just raw keys. Both RSA and ECDSA keys are supported and the supported signing algorithms are: RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384 and ES512.
You can use multiple signing keys simultaneously, but only one signing key per algorithm is supported. The first signing key you register is considered the default signing key.
Both clients and API resources can express preferences on the signing algorithm. If you request a single token for multiple API resources, all resources need to agree on at least one allowed signing algorithm.
Loading of signing key and the corresponding validation part is done by implementations of ISigningCredentialStore
and IValidationKeysStore
.
If you want to customize the loading of the keys, you can implement those interfaces and register them with DI.
The DI builder extensions has a couple of convenience methods to set signing and validation keys - see here.
Signing key rollover¶
While you can only use one signing key at a time, you can publish more than one validation key to the discovery document. This is useful for key rollover.
In a nutshell, a rollover typically works like this:
you request/create new key material
you publish the new validation key in addition to the current one. You can use the
AddValidationKey
builder extension method for that.all clients and APIs now have a chance to learn about the new key the next time they update their local copy of the discovery document
after a certain amount of time (e.g. 24h) all clients and APIs should now accept both the old and the new key material
keep the old key material around for as long as you like, maybe you have long-lived tokens that need validation
retire the old key material when it is not used anymore
all clients and APIs will “forget” the old key next time they update their local copy of the discovery document
This requires that clients and APIs use the discovery document, and also have a feature to periodically refresh their configuration.
Brock wrote a more detailed blog post about key rotation that can automatically take care of all those details.
Data protection¶
Cookie authentication in ASP.NET Core (or anti-forgery in MVC) use the ASP.NET Core data protection feature. Depending on your deployment scenario, this might require additional configuration. See the Microsoft docs for more information.
HTTPS¶
We don’t enforce the use of HTTPS, but for production it is mandatory for every interaction with IdentityServer.
Grant Types¶
The OpenID Connect and OAuth 2.0 specifications define so-called grant types (often also called flows - or protocol flows). Grant types specify how a client can interact with the token service.
You need to specify which grant types a client can use via the AllowedGrantTypes
property on the Client
configuration.
This allows locking down the protocol interactions that are allowed for a given client.
A client can be configured to use more than a single grant type (e.g. Authorization Code flow for user centric operations and client credentials for server to server communication).
The GrantTypes
class can be used to pick from typical grant type combinations:
Client.AllowedGrantTypes = GrantTypes.CodeAndClientCredentials;
You can also specify the grant types list manually:
Client.AllowedGrantTypes =
{
GrantType.Code,
GrantType.ClientCredentials,
"my_custom_grant_type"
};
While IdentityServer supports all standard grant types, you really only need to know two of them for common application scenarios.
Machine to Machine Communication¶
This is the simplest type of communication. Tokens are always requested on behalf of a client, no interactive user is present.
In this scenario, you send a token request to the token endpoint using the client credentials
grant type.
The client typically has to authenticate with the token endpoint using its client ID and secret.
See the Client Credentials Quick Start for a sample how to use it.
Interactive Clients¶
This is the most common type of client scenario: web applications, SPAs or native/mobile apps with interactive users.
Note
Feel free to skip to the summary, if you don’t care about all the technical details.
For this type of clients, the authorization code
flow was designed. That flow consists of two physical operations:
a front-channel step via the browser where all “interactive” things happen, e.g. login page, consent etc. This step results in an authorization code that represents the outcome of the front-channel operation.
a back-channel step where the authorization code from step 1 gets exchanged with the requested tokens. Confidential clients need to authenticate at this point.
This flow has the following security properties:
no data (besides the authorization code which is basically a random string) gets leaked over the browser channel
authorization codes can only be used once
the authorization code can only be turned into tokens when (for confidential clients - more on that later) the client secret is known
This sounds all very good - still there is one problem called code substitution attack. There are two modern mitigation techniques for this:
OpenID Connect Hybrid Flow
This uses a response type of code id_token
to add an additional identity token to the response. This token is signed and protected against substitution.
In addition it contains the hash of the code via the c_hash
claim. This allows checking that you indeed got the right code (experts call this a detached signature).
This solves the problem but has the following down-sides:
the
id_token
gets transmitted over the front-channel and might leak additional (personal identifiable) dataall the mitigation steps (e.g. crypto) need to be implemented by the client. This results in more complicated client library implementations.
RFC 7636 - Proof Key for Code Exchange (PKCE)
This essentially introduces a per-request secret for code flow (please read up on the details here). All the client has to implement for this, is creating a random string and hashing it using SHA256.
This also solves the substitution problem, because the client can prove that it is the same client on front and back-channel, and has the following additional advantages:
the client implementation is very simple compared to hybrid flow
it also solves the problem of the absence of a static secret for public clients
no additional front-channel response artifacts are needed
Summary
Interactive clients should use an authorization code-based flow. To protect against code substitution, either hybrid flow or PKCE should be used. If PKCE is available, this is the simpler solution to the problem.
PKCE is already the official recommendation for native applications and SPAs - and with the release of ASP.NET Core 3 also by default supported in the OpenID Connect handler as well.
This is how you would configure an interactive client:
var client = new Client
{
ClientId = "...",
// set client secret for confidential clients
ClientSecret = { ... },
// ...or turn off for public clients
RequireClientSecret = false,
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true
};
Interactive clients without browsers or with constrained input devices¶
This grant type is detailed RFC 8628.
This flow outsources user authentication and consent to an external device (e.g. a smart phone). It is typically used by devices that don’t have proper keyboards (e.g. TVs, gaming consoles…) and can request both identity and API resources.
Custom scenarios¶
Extension grants allow extending the token endpoint with new grant types. See this for more details.
Client Authentication¶
In certain situations, clients need to authenticate with IdentityServer, e.g.
confidential applications (aka clients) requesting tokens at the token endpoint
APIs validating reference tokens at the introspection endpoint
For that purpose you can assign a list of secrets to a client or an API resource.
Secret parsing and validation is an extensibility point in identityserver, out of the box it supports shared secrets as well as transmitting the shared secret via a basic authentication header or the POST body.
Authentication using an asymmetric Key¶
There are other techniques to authenticate clients, e.g. based on public/private key cryptography. IdentityServer includes support for private key JWT client secrets (see RFC 7523 and here).
Secret extensibility typically consists of three things:
a secret definition
a secret parser that knows how to extract the secret from the incoming request
a secret validator that knows how to validate the parsed secret based on the definition
Secret parsers and validators are implementations of the ISecretParser
and ISecretValidator
interfaces.
To make them available to IdentityServer, you need to register them with the DI container, e.g.:
builder.AddSecretParser<JwtBearerClientAssertionSecretParser>()
builder.AddSecretValidator<PrivateKeyJwtSecretValidator>()
Our default private key JWT secret validator expects the full (leaf) certificate as base64 on the secret definition or an ESA/EC JSON web key:
var client = new Client
{
ClientId = "client.jwt",
ClientSecrets =
{
new Secret
{
Type = IdentityServerConstants.SecretTypes.X509CertificateBase64,
Value = "MIIDATCCAe2gAwIBAgIQoHUYAquk9rBJcq8W+F0FAzAJBgUrDgMCHQUAMBIxEDAOBgNVBAMTB0RldlJvb3QwHhcNMTAwMTIwMjMwMDAwWhcNMjAwMTIwMjMwMDAwWjARMQ8wDQYDVQQDEwZDbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSaY4x1eXqjHF1iXQcF3pbFrIbmNw19w/IdOQxbavmuPbhY7jX0IORu/GQiHjmhqWt8F4G7KGLhXLC1j7rXdDmxXRyVJBZBTEaSYukuX7zGeUXscdpgODLQVay/0hUGz54aDZPAhtBHaYbog+yH10sCXgV1Mxtzx3dGelA6pPwiAmXwFxjJ1HGsS/hdbt+vgXhdlzud3ZSfyI/TJAnFeKxsmbJUyqMfoBl1zFKG4MOvgHhBjekp+r8gYNGknMYu9JDFr1ue0wylaw9UwG8ZXAkYmYbn2wN/CpJl3gJgX42/9g87uLvtVAmz5L+rZQTlS1ibv54ScR2lcRpGQiQav/LAgMBAAGjXDBaMBMGA1UdJQQMMAoGCCsGAQUFBwMCMEMGA1UdAQQ8MDqAENIWANpX5DZ3bX3WvoDfy0GhFDASMRAwDgYDVQQDEwdEZXZSb290ghAsWTt7E82DjU1E1p427Qj2MAkGBSsOAwIdBQADggEBADLje0qbqGVPaZHINLn+WSM2czZk0b5NG80btp7arjgDYoWBIe2TSOkkApTRhLPfmZTsaiI3Ro/64q+Dk3z3Kt7w+grHqu5nYhsn7xQFAQUf3y2KcJnRdIEk0jrLM4vgIzYdXsoC6YO+9QnlkNqcN36Y8IpSVSTda6gRKvGXiAhu42e2Qey/WNMFOL+YzMXGt/nDHL/qRKsuXBOarIb++43DV3YnxGTx22llhOnPpuZ9/gnNY7KLjODaiEciKhaKqt/b57mTEz4jTF4kIg6BP03MUfDXeVlM1Qf1jB43G2QQ19n5lUiqTpmQkcfLfyci2uBZ8BkOhXr3Vk9HIk/xBXQ="
}
new Secret
{
Type = IdentityServerConstants.SecretTypes.JsonWebKey,
Value = "{'e':'AQAB','kid':'ZzAjSnraU3bkWGnnAqLapYGpTyNfLbjbzgAPbbW2GEA','kty':'RSA','n':'wWwQFtSzeRjjerpEM5Rmqz_DsNaZ9S1Bw6UbZkDLowuuTCjBWUax0vBMMxdy6XjEEK4Oq9lKMvx9JzjmeJf1knoqSNrox3Ka0rnxXpNAz6sATvme8p9mTXyp0cX4lF4U2J54xa2_S9NF5QWvpXvBeC4GAJx7QaSw4zrUkrc6XyaAiFnLhQEwKJCwUw4NOqIuYvYp_IXhw-5Ti_icDlZS-282PcccnBeOcX7vc21pozibIdmZJKqXNsL1Ibx5Nkx1F1jLnekJAmdaACDjYRLL_6n3W4wUp19UvzB1lGtXcJKLLkqB6YDiZNu16OSiSprfmrRXvYmvD8m6Fnl5aetgKw'}"
}
},
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = { "api1", "api2" }
};
Extension Grants¶
OAuth 2.0 defines standard grant types for the token endpoint, such as password
, authorization_code
and refresh_token
. Extension grants are a way to add support for non-standard token issuance scenarios like token translation, delegation, or custom credentials.
You can add support for additional grant types by implementing the IExtensionGrantValidator
interface:
public interface IExtensionGrantValidator
{
/// <summary>
/// Handles the custom grant request.
/// </summary>
/// <param name="request">The validation context.</param>
Task ValidateAsync(ExtensionGrantValidationContext context);
/// <summary>
/// Returns the grant type this validator can deal with
/// </summary>
/// <value>
/// The type of the grant.
/// </value>
string GrantType { get; }
}
The ExtensionGrantValidationContext
object gives you access to:
the incoming token request - both the well-known validated values, as well as any custom values (via the
Raw
collection)the result - either error or success
custom response parameters
To register the extension grant, add it to DI:
builder.AddExtensionGrantValidator<MyExtensionsGrantValidator>();
Example: Simple delegation using an extension grant¶
Imagine the following scenario - a front end client calls a middle tier API using a token acquired via an interactive flow (e.g. hybrid flow). This middle tier API (API 1) now wants to call a back end API (API 2) on behalf of the interactive user:

In other words, the middle tier API (API 1) needs an access token containing the user’s identity, but with the scope of the back end API (API 2).
Note
You might have heard of the term poor man’s delegation where the access token from the front end is simply forwarded to the back end. This has some shortcomings, e.g. API 2 must now accept the API 1 scope which would allow the user to call API 2 directly. Also - you might want to add some delegation specific claims into the token, e.g. the fact that the call path is via API 1.
Implementing the extension grant
The front end would send the token to API 1, and now this token needs to be exchanged at IdentityServer with a new token for API 2.
On the wire the call to token service for the exchange could look like this:
POST /connect/token
grant_type=delegation&
scope=api2&
token=...&
client_id=api1.client
client_secret=secret
It’s the job of the extension grant validator to handle that request by validating the incoming token, and returning a result that represents the new token:
public class DelegationGrantValidator : IExtensionGrantValidator
{
private readonly ITokenValidator _validator;
public DelegationGrantValidator(ITokenValidator validator)
{
_validator = validator;
}
public string GrantType => "delegation";
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var userToken = context.Request.Raw.Get("token");
if (string.IsNullOrEmpty(userToken))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var result = await _validator.ValidateAccessTokenAsync(userToken);
if (result.IsError)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
// get user's identity
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub").Value;
context.Result = new GrantValidationResult(sub, GrantType);
return;
}
}
Don’t forget to register the validator with DI.
Registering the delegation client
You need a client registration in IdentityServer that allows a client to use this new extension grant, e.g.:
var client = new Client
{
ClientId = "api1.client",
ClientSecrets = new List<Secret>
{
new Secret("secret".Sha256())
},
AllowedGrantTypes = { "delegation" },
AllowedScopes = new List<string>
{
"api2"
}
}
Calling the token endpoint
In API 1 you can now construct the HTTP payload yourself, or use the IdentityModel helper library:
public async Task<TokenResponse> DelegateAsync(string userToken)
{
var client = _httpClientFactory.CreateClient();
// or
// var client = new HttpClient();
// send custom grant to token endpoint, return response
return await client.RequestTokenAsync(new TokenRequest
{
Address = disco.TokenEndpoint,
GrantType = "delegation",
ClientId = "api1.client",
ClientSecret = "secret",
Parameters =
{
{ "scope", "api2" },
{ "token", userToken}
}
});
}
The TokenResponse.AccessToken
will now contain the delegation access token.
Resource Owner Password Validation¶
If you want to use the OAuth 2.0 resource owner password credential grant (aka password
), you need to implement and register the IResourceOwnerPasswordValidator
interface:
public interface IResourceOwnerPasswordValidator
{
/// <summary>
/// Validates the resource owner password credential
/// </summary>
/// <param name="context">The context.</param>
Task ValidateAsync(ResourceOwnerPasswordValidationContext context);
}
On the context you will find already parsed protocol parameters like UserName
and Password
, but also the raw request if you want to look at other input data.
Your job is then to implement the password validation and set the Result
on the context accordingly. See the GrantValidationResult documentation.
Refresh Tokens¶
Since access tokens have finite lifetimes, refresh tokens allow requesting new access tokens without user interaction.
Refresh tokens are supported for the following flows: authorization code, hybrid and resource owner password credential flow.
The clients needs to be explicitly authorized to request refresh tokens by setting AllowOfflineAccess
to true
.
Additional client settings¶
AbsoluteRefreshTokenLifetime
Maximum lifetime of a refresh token in seconds. Defaults to 2592000 seconds / 30 days. Zero allows refresh tokens that, when used with
RefreshTokenExpiration = Sliding
only expire after the SlidingRefreshTokenLifetime is passed.SlidingRefreshTokenLifetime
Sliding lifetime of a refresh token in seconds. Defaults to 1296000 seconds / 15 days
RefreshTokenUsage
ReUse
the refresh token handle will stay the same when refreshing tokensOneTimeOnly
the refresh token handle will be updated when refreshing tokensRefreshTokenExpiration
Absolute
the refresh token will expire on a fixed point in time (specified by the AbsoluteRefreshTokenLifetime). This is the default.Sliding
when refreshing the token, the lifetime of the refresh token will be renewed (by the amount specified in SlidingRefreshTokenLifetime). The lifetime will not exceed AbsoluteRefreshTokenLifetime.UpdateAccessTokenClaimsOnRefresh
Gets or sets a value indicating whether the access token (and its claims) should be updated on a refresh token request.
Note
Public clients (clients without a client secret) should rotate their refresh tokens. Set the RefreshTokenUsage
to OneTimeOnly
.
Requesting a refresh token¶
You can request a refresh token by adding a scope called offline_access
to the scope parameter.
Requesting an access token using a refresh token¶
To get a new access token, you send the refresh token to the token endpoint. This will result in a new token response containing a new access token and its expiration and potentially also a new refresh token depending on the client configuration (see above).
POST /connect/token
client_id=client&
client_secret=secret&
grant_type=refresh_token&
refresh_token=hdh922
(Form-encoding removed and line breaks added for readability)
Note
You can use the IdentityModel client library to programmatically access the token endpoint from .NET code. For more information check the IdentityModel docs.
Note
The refresh token, must be valid or an invalid_grant error is returned. By default, a refresh_token can only be used once. Using an already used refresh_token will result in an invalid_grant error.
Customizing refresh token behavior¶
All refresh token handling is implemented in the DefaultRefreshTokenService
(which is the default implementation of the IRefreshTokenService
interface):
public interface IRefreshTokenService
{
/// <summary>
/// Validates a refresh token.
/// </summary>
Task<TokenValidationResult> ValidateRefreshTokenAsync(string token, Client client);
/// <summary>
/// Creates the refresh token.
/// </summary>
Task<string> CreateRefreshTokenAsync(ClaimsPrincipal subject, Token accessToken, Client client);
/// <summary>
/// Updates the refresh token.
/// </summary>
Task<string> UpdateRefreshTokenAsync(string handle, RefreshToken refreshToken, Client client);
}
The logic around refresh token handling is pretty involved, and we don’t recommend implementing the interface from scratch, unless you exactly know what you are doing. If you want to customize certain behavior, it is more recommended to derive from the default implementation and call the base checks first.
The most common customization that you probably want to do is how to deal with refresh token replays. This is for situations where the token usage has been set to one-time only, but the same token gets sent more than once. This could either point to a replay attack of the refresh token, or to faulty client code like logic bugs or race conditions.
It is important to note, that a refresh token is never deleted in the database.
Once it has been used, the ConsumedTime
property will be set.
If a token is received that has already been consumed, the default service will call a virtual method called AcceptConsumedTokenAsync
.
The default implementation will reject the request, but here you can implement custom logic like grace periods, or revoking additional refresh or access tokens.
Reference Tokens¶
Access tokens can come in two flavours - self-contained or reference.
A JWT token would be a self-contained access token - it’s a protected data structure with claims and an expiration. Once an API has learned about the key material, it can validate self-contained tokens without needing to communicate with the issuer. This makes JWTs hard to revoke. They will stay valid until they expire.
When using reference tokens - IdentityServer will store the contents of the token in a data store and will only issue a unique identifier for this token back to the client. The API receiving this reference must then open a back-channel communication to IdentityServer to validate the token.

You can switch the token type of a client using the following setting:
client.AccessTokenType = AccessTokenType.Reference;
IdentityServer provides an implementation of the OAuth 2.0 introspection specification which allows APIs to dereference the tokens. For a more flexible approach, please read Flexible access token validation in ASP.NET Core.
The introspection endpoint requires authentication - since the client of an introspection endpoint is an API, you configure the secret on the ApiResource
:
var api = new ApiResource("api1")
{
ApiSecrets = { new Secret("secret".Sha256()) }
}
See here for more information on how to configure the IdentityServer authentication middleware for APIs.
Persisted Grants¶
Many grant types require persistence in IdentityServer. These include authorization codes, refresh tokens, reference tokens, and remembered user consents. Internally in IdentityServer, the default storage for these grants is in a common store called the persisted grants store.
Persisted Grant¶
The persisted grant is the data type that maintains the values for a grant. It has these properties:
Key
The unique identifier for the persisted grant in the store.
Type
The type of the grant.
SubjectId
The subject id to which the grant belongs.
ClientId
The client identifier for which the grant was created.
Description
The description the user assigned to the grant or device being authorized.
CreationTime
The date/time the grant was created.
Expiration
The expiration of the grant.
ConsumedTime
The date/time the grant was “consumed” (see below).
Data
The grant specific serialized data.
Note
The Data
property contains a copy of all of the values (and more) and is considered authoritative by IdentityServer, thus the above values, by default, are considered informational and read-only.
The presence of the record in the store without a ConsumedTime
and while still within the Expiration
represents the validity of the grant.
Setting either of these two values, or removing the record from the store effectively revokes the grant.
Grant Consumption¶
Some grant types are one-time use only (either by definition or configuration).
Once they are “used”, rather than deleting the record, the ConsumedTime
value is set in the database marking them as having been used.
This “soft delete” allows for custom implementations to either have flexibility in allowing a grant to be re-used (typically within a short window of time),
or to be used in risk assessment and threat mitigation scenarios (where suspicious activity is detected) to revoke access.
For refresh tokens, this sort of custom logic would be performed in the IRefreshTokenService
.
Persisted Grant Service¶
Working with the grants store directly might be too low level.
As such, a higher level service called IPersistedGrantService
is provided.
It abstracts and aggregates the different grant types into one concept, and allows querying and revoking the persisted grants for a user.
It contains these APIs:
GetAllGrantsAsync
Gets all the grants for a user based upon subject id.
RemoveAllGrantsAsync
Removes grants from the store based on the subject id and optionally a client id and/or a session id.
Proof-of-Possession Access Tokens¶
By default, OAuth access tokens are so called bearer tokens. This means they are not bound to a client and anybody who possess the token can use it (compare to cash).
Proof-of-Possession (short PoP) tokens are bound to the client that requested the token. If that token leaks, it cannot be used by anyone else (compare to a credit card - well at least in an ideal world).
See this blog post for more history and motivation.
IdentityServer supports PoP tokens by using the Mutual TLS mechanism.
Mutual TLS¶
Mutual TLS support in IdentityServer allows for two features:
Client authentication to IdentityServer endpoints using a TLS X.509 client certificate
Binding of access tokens to clients using a TLS X.509 client certificate
Note
See the “OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens” spec for more information
Setting up MTLS involves a couple of steps.
Server setup¶
It’s the hosting layer’s responsibility to do the actual validation of the client certificate. IdentityServer will then use that information to associate the certificate with a client and embed the certificate information in the access tokens.
Depending which server you are using, those steps are different. See this blog post for more information.
Note
mkcert is a nice utility for creating certificates for development purposes.
ASP.NET Core setup¶
Depending on the server setup, there are different ways how the ASP.NET Core host will receive the client certificate. While for IIS and pure Kestrel hosting, there are no additional steps, typically you have a reverse proxy in front of the application server.
This means that in addition to the typical forwarded headers handling, you also need to process the header that contains the client certificate.
Add a call to app.UseCertificateForwarding();
in the beginning of your middleware pipeline for that.
The exact format how proxies transmit the certificates is not standardized, that’s why you need to register a callback to do the actual header parsing. The Microsoft docs show how that would work for Azure Web Apps.
If you are using Nginx (which we found is the most flexible hosting option), you need to register the following service in ConfigureServices
:
services.AddCertificateForwarding(options =>
{
// header name might be different, based on your nginx config
options.CertificateHeader = "X-SSL-CERT";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if(!string.IsNullOrWhiteSpace(headerValue))
{
var bytes = Encoding.UTF8.GetBytes(Uri.UnescapeDataString(headerValue));
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
Once, the certificate has been loaded, you also need to setup the authentication handler.
In this scenario we want to support self-signed certificates, hence the CertificateType.All
and no revocation checking.
These settings might be different in your environment:
services.AddAuthentication()
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.RevocationMode = X509RevocationMode.NoCheck;
});
IdentityServer setup¶
Next step is to enable MTLS in IdentityServer. For that you need to specify the name of the certificate authentication handler you set-up in the last step (defaults to Certificate
),
and the MTLS hosting strategy.
In IdentityServer, the mutual TLS endpoints, can be configured in three ways:
path-based - endpoints located beneath the path
~/connect/mtls
.sub-domain based - endpoints are on a sub-domain of the main server.
domain-based - endpoints are on a different domain.
For example:
var builder = services.AddIdentityServer(options =>
{
options.MutualTls.Enabled = true;
options.MutualTls.ClientCertificateAuthenticationScheme = "Certificate";
// uses sub-domain hosting
options.MutualTls.DomainName = "mtls";
});
IdentityServer’s discovery document reflects those endpoints:

Client authentication¶
Clients can use a X.509 client certificate as an authentication mechanism to endpoints in IdentityServer.
For this you need to associate a client certificate with a client in IdentityServer. Use the IdentityServer builder to add the services to DI which contain a default implementation to do that either thumbprint or common-name based:
builder.AddMutualTlsSecretValidators();
Finally, for the client configuration add to the ClientSecrets
collection a secret type of either SecretTypes.X509CertificateName
if you wish to authenticate the client from the certificate distinguished name or SecretTypes.X509CertificateThumbprint
if you wish to authenticate the client by certificate thumbprint.
For example:
new Client
{
ClientId = "mtls",
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = { "api1" }
ClientSecrets =
{
// name based
new Secret(@"CN=mtls.test, OU=ROO\ballen@roo, O=mkcert development certificate", "mtls.test")
{
Type = SecretTypes.X509CertificateName
},
// or thumbprint based
//new Secret("bca0d040847f843c5ee0fa6eb494837470155868", "mtls.test")
//{
// Type = SecretTypes.X509CertificateThumbprint
//},
},
}
Using a client certificate to authenticate to IdentityServer¶
When writing a client to connect to IdentityServer, the SocketsHttpHandler
(or HttpClientHandler
if you are on older .NET Framework versions)
class provides a convenient mechanism to add a client certificate to outgoing requests.
And then HTTP calls (including using the various IdentityModel extension methods) with the HttpClient
will perform client certificate authentication at the TLS channel.
For example:
static async Task<TokenResponse> RequestTokenAsync()
{
var handler = new SocketsHttpHandler();
var cert = new X509Certificate2("client.p12", "password");
handler.SslOptions.ClientCertificates = new X509CertificateCollection { cert };
var client = new HttpClient(handler);
var disco = await client.GetDiscoveryDocumentAsync(Constants.Authority);
if (disco.IsError) throw new Exception(disco.Error);
var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco
.TryGetValue(OidcConstants.Discovery.MtlsEndpointAliases)
.Value<string>(OidcConstants.Discovery.TokenEndpoint)
.ToString(),
ClientId = "mtls",
Scope = "api1"
});
if (response.IsError) throw new Exception(response.Error);
return response;
}
Sender-constrained access tokens¶
Whenever a client authenticates to IdentityServer using a client certificate, the thumbprint of that certificate will be embedded in the access token.
Clients can use a X.509 client certificate as a mechanism for sender-constrained access tokens when authenticating to APIs. The use of these sender-constrained access tokens requires the client to use the same X.509 client certificate to authenticate to the API as the one used for IdentityServer.
Confirmation claim¶
When a client obtains an access token and has authenticated with mutual TLS, IdentityServer issues a confirmation claim (or cnf
) in the access token.
This value is a hash of the thumbprint of the client certificate used to authenticate with IdentityServer.
This value can be seen in this screen shot of a decoded access token:

The API will then use this value to ensure the client certificate being used at the API matches the confirmation value in the access token.
Validating and accepting a client certificate in APIs¶
As mentioned above for client authentication in IdentityServer, in the API the web server is expected to perform the client certificate validation at the TLS layer.
Additionally, the API hosting application will need a mechanism to accept the client certificate in order to obtain the thumbprint to perform the confirmation claim validation. Below is an example how an API in ASP.NET Core might be configured for both access tokens and client certificates:
services.AddAuthentication("token")
.AddIdentityServerAuthentication("token", options =>
{
options.Authority = "https://idp-server.io";
options.ApiName = "api1";
})
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
});
Finally, a mechanism is needed that runs after the authentication middleware to authenticate the client certificate and compare the thumbprint to the cnf
from the access token.
Below is a simple middleware that checks the claims:
public class ConfirmationValidationMiddlewareOptions
{
public string CertificateSchemeName { get; set; } = CertificateAuthenticationDefaults.AuthenticationScheme;
public string JwtBearerSchemeName { get; set; } = JwtBearerDefaults.AuthenticationScheme;
}
// this middleware validate the cnf claim (if present) against the thumbprint of the X.509 client certificate for the current client
public class ConfirmationValidationMiddleware
{
private readonly RequestDelegate _next;
private readonly ConfirmationValidationMiddlewareOptions _options;
public ConfirmationValidationMiddleware(RequestDelegate next, ConfirmationValidationMiddlewareOptions options = null)
{
_next = next;
_options = options ?? new ConfirmationValidationMiddlewareOptions();
}
public async Task Invoke(HttpContext ctx)
{
if (ctx.User.Identity.IsAuthenticated)
{
var cnfJson = ctx.User.FindFirst("cnf")?.Value;
if (!String.IsNullOrWhiteSpace(cnfJson))
{
var certResult = await ctx.AuthenticateAsync(_options.CertificateSchemeName);
if (!certResult.Succeeded)
{
await ctx.ChallengeAsync(_options.CertificateSchemeName);
return;
}
var certificate = await ctx.Connection.GetClientCertificateAsync();
var thumbprint = Base64UrlTextEncoder.Encode(certificate.GetCertHash(HashAlgorithmName.SHA256));
var cnf = JObject.Parse(cnfJson);
var sha256 = cnf.Value<string>("x5t#S256");
if (String.IsNullOrWhiteSpace(sha256) ||
!thumbprint.Equals(sha256, StringComparison.Ordinal))
{
await ctx.ChallengeAsync(_options.JwtBearerSchemeName);
return;
}
}
}
await _next(ctx);
}
Below is an example pipeline for an API:
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseCertificateForwarding();
app.UseRouting();
app.UseAuthentication();
app.UseMiddleware<ConfirmationValidationMiddleware>(new ConfirmationValidationMiddlewareOptions
{
CertificateSchemeName = CertificateAuthenticationDefaults.AuthenticationScheme,
JwtBearerSchemeName = "token"
});
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
Once the above middleware succeeds, then the caller has been authenticated with a sender-constrained access token.
Introspection and the confirmation claim¶
When the access token is a JWT, then the confirmation claim is contained in the token as a claim.
When using reference tokens, the claims that the access token represents must be obtained via introspection.
The introspection endpoint in IdentityServer will return a cnf
claim for reference tokens obtained via mutual TLS.
Ephemeral client certificates¶
You can use the IdentityServer MTLS support also to create sender-constrained access tokens without using the client certificate for client authentication. This is useful for situations where you already have client secrets in place that you don’t want to change, e.g. shared secrets, or better private key JWTs.
Still, if a client certificate is present, the confirmation claim can be embedded in outgoing access tokens. And as long as the client is using the same client certificate to request the token and calling the API, this will give you the desired proof-of-possession properties.
For this enable the following setting in the options:
var builder = services.AddIdentityServer(options =>
{
// other settings
options.MutualTls.AlwaysEmitConfirmationClaim = true;
});
Using an ephemeral certificate to request a token¶
In this scenario, the client uses some client secret (a shared secret in the below sample), but attaches an additional client certificate to the token request. Since this certificate does not need to be associated with the client at the token services, it can be created on the fly:
static X509Certificate2 CreateClientCertificate(string name)
{
X500DistinguishedName distinguishedName = new X500DistinguishedName($"CN={name}");
using (RSA rsa = RSA.Create(2048))
{
var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(
new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature , false));
request.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension(
new OidCollection { new Oid("1.3.6.1.5.5.7.3.2") }, false));
return request.CreateSelfSigned(new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(10)));
}
}
Then use this client certificate in addition to the already setup-up client secret:
static async Task<TokenResponse> RequestTokenAsync()
{
var client = new HttpClient(GetHandler(ClientCertificate));
var disco = await client.GetDiscoveryDocumentAsync("https://identityserver.local");
if (disco.IsError) throw new Exception(disco.Error);
var endpoint = disco
.TryGetValue(OidcConstants.Discovery.MtlsEndpointAliases)
.Value<string>(OidcConstants.Discovery.TokenEndpoint)
.ToString();
var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = endpoint,
ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
});
if (response.IsError) throw new Exception(response.Error);
return response;
}
static SocketsHttpHandler GetHandler(X509Certificate2 certificate)
{
var handler = new SocketsHttpHandler();
handler.SslOptions.ClientCertificates = new X509CertificateCollection { certificate };
return handler;
}
Custom Token Request Validation and Issuance¶
You can run custom code as part of the token issuance pipeline at the token endpoint. This allows e.g. for
adding additional validation logic
changing certain parameters (e.g. token lifetime) dynamically
For this purpose, implement (and register) the ICustomTokenRequestValidator
interface:
/// <summary>
/// Allows inserting custom validation logic into token requests
/// </summary>
public interface ICustomTokenRequestValidator
{
/// <summary>
/// Custom validation logic for a token request.
/// </summary>
/// <param name="context">The context.</param>
/// <returns>
/// The validation result
/// </returns>
Task ValidateAsync(CustomTokenRequestValidationContext context);
}
The context object gives you access to:
adding custom response parameters
return an error and error description
modifying the request parameters, e.g. access token lifetime and type, client claims, and the confirmation method
You can register your implementation of the validator using the AddCustomTokenRequestValidator
extension method on the configuration builder.
CORS¶
Many endpoints in IdentityServer will be accessed via Ajax calls from JavaScript-based clients. Given that IdentityServer will most likely be hosted on a different origin than these clients, this implies that Cross-Origin Resource Sharing (CORS) will need to be configured.
Client-based CORS Configuration¶
One approach to configuring CORS is to use the AllowedCorsOrigins
collection on the client configuration.
Simply add the origin of the client to the collection and the default configuration in IdentityServer will consult these values to allow cross-origin calls from the origins.
Note
Be sure to use an origin (not a URL) when configuring CORS. For example: https://foo:123/
is a URL, whereas https://foo:123
is an origin.
This default CORS implementation will be in use if you are using either the “in-memory” or EF-based client configuration that we provide.
If you define your own IClientStore
, then you will need to implement your own custom CORS policy service (see below).
Custom Cors Policy Service¶
IdentityServer allows the hosting application to implement the ICorsPolicyService
to completely control the CORS policy.
The single method to implement is: Task<bool> IsOriginAllowedAsync(string origin)
.
Return true
if the origin is allowed, false
otherwise.
Once implemented, simply register the implementation in DI and IdentityServer will then use your custom implementation.
DefaultCorsPolicyService
If you simply wish to hard-code a set of allowed origins, then there is a pre-built ICorsPolicyService
implementation you can use called DefaultCorsPolicyService
.
This would be configured as a singleton in DI, and hard-coded with its AllowedOrigins
collection, or setting the flag AllowAll
to true
to allow all origins.
For example, in ConfigureServices
:
services.AddSingleton<ICorsPolicyService>((container) => {
var logger = container.GetRequiredService<ILogger<DefaultCorsPolicyService>>();
return new DefaultCorsPolicyService(logger) {
AllowedOrigins = { "https://foo", "https://bar" }
};
});
Note
Use AllowAll
with caution.
Mixing IdentityServer’s CORS policy with ASP.NET Core’s CORS policies¶
IdentityServer uses the CORS middleware from ASP.NET Core to provide its CORS implementation. It is possible that your application that hosts IdentityServer might also require CORS for its own custom endpoints. In general, both should work together in the same application.
Your code should use the documented CORS features from ASP.NET Core without regard to IdentityServer.
This means you should define policies and register the middleware as normal.
If your application defines policies in ConfigureServices
, then those should continue to work in the same places you are using them (either where you configure the CORS middleware or where you use the MVC EnableCors
attributes in your controller code).
If instead you define an inline policy in the use of the CORS middleware (via the policy builder callback), then that too should continue to work normally.
The one scenario where there might be a conflict between your use of the ASP.NET Core CORS services and IdentityServer is if you decide to create a custom ICorsPolicyProvider
.
Given the design of the ASP.NET Core’s CORS services and middleware, IdentityServer implements its own custom ICorsPolicyProvider
and registers it in the DI system.
Fortunately, the IdentityServer implementation is designed to use the decorator pattern to wrap any existing ICorsPolicyProvider
that is already registered in DI.
What this means is that you can also implement the ICorsPolicyProvider
, but it simply needs to be registered prior to IdentityServer in DI (e.g. in ConfigureServices
).
Discovery¶
The discovery document can be found at https://baseaddress/.well-known/openid-configuration. It contains information about the endpoints, key material and features of your IdentityServer.
By default all information is included in the discovery document, but by using configuration options, you can hide individual sections, e.g.:
services.AddIdentityServer(options =>
{
options.Discovery.ShowIdentityScopes = false;
options.Discovery.ShowApiScopes = false;
options.Discovery.ShowClaims = false;
options.Discovery.ShowExtensionGrantTypes = false;
});
Extending discovery¶
You can add custom entries to the discovery document, e.g:
services.AddIdentityServer(options =>
{
options.Discovery.CustomEntries.Add("my_setting", "foo");
options.Discovery.CustomEntries.Add("my_complex_setting",
new
{
foo = "foo",
bar = "bar"
});
});
When you add a custom value that starts with ~/ it will be expanded to an absolute path below the IdentityServer base address, e.g.:
options.Discovery.CustomEntries.Add("my_custom_endpoint", "~/custom");
If you want to take full control over the rendering of the discovery (and jwks) document, you can implement the IDiscoveryResponseGenerator
interface (or derive from our default implementation).
Adding more API Endpoints¶
It’s a common scenario to add additional API endpoints to the application hosting IdentityServer. These endpoints are typically protected by IdentityServer itself.
For simple scenarios, we give you some helpers. See the advanced section to understand more of the internal plumbing.
Note
You could achieve the same by using either our IdentityServerAuthentication
handler or Microsoft’s JwtBearer
handler. But this is not recommended since it requires more configuration and creates dependencies on external libraries that might lead to conflicts in future updates.
Start by registering your API as an ApiResource
, e.g.:
public static IEnumerable<ApiResource> Apis = new List<ApiResource>
{
// local API
new ApiResource(IdentityServerConstants.LocalApi.ScopeName),
};
..and give your clients access to this API, e.g.:
new Client
{
// rest omitted
AllowedScopes = { IdentityServerConstants.LocalApi.ScopeName },
}
Note
The value of IdentityServerConstants.LocalApi.ScopeName
is IdentityServerApi
.
To enable token validation for local APIs, add the following to your IdentityServer startup:
services.AddLocalApiAuthentication();
To protect an API controller, decorate it with an Authorize
attribute using the LocalApi.PolicyName
policy:
[Route("localApi")]
[Authorize(LocalApi.PolicyName)]
public class LocalApiController : ControllerBase
{
public IActionResult Get()
{
// omitted
}
}
Authorized clients can then request a token for the IdentityServerApi
scope and use it to call the API.
Discovery¶
You can also add your endpoints to the discovery document if you want, e.g like this:
services.AddIdentityServer(options =>
{
options.Discovery.CustomEntries.Add("local_api", "~/localapi");
})
Advanced¶
Under the covers, the AddLocalApiAuthentication
helper does a couple of things:
adds an authentication handler that validates incoming tokens using IdentityServer’s built-in token validation engine (the name of this handler is
IdentityServerAccessToken
orIdentityServerConstants.LocalApi.AuthenticationScheme
configures the authentication handler to require a scope claim inside the access token of value
IdentityServerApi
sets up an authorization policy that checks for a scope claim of value
IdentityServerApi
This covers the most common scenarios. You can customize this behavior in the following ways:
- Add the authentication handler yourself by calling
services.AddAuthentication().AddLocalApi(...)
this way you can specify the required scope name yourself, or (by specifying no scope at all) accept any token from the current IdentityServer instance
- Add the authentication handler yourself by calling
Do your own scope validation/authorization in your controllers using custom policies or code, e.g.:
services.AddAuthorization(options => { options.AddPolicy(IdentityServerConstants.LocalApi.PolicyName, policy => { policy.AddAuthenticationSchemes(IdentityServerConstants.LocalApi.AuthenticationScheme); policy.RequireAuthenticatedUser(); // custom requirements }); });
Claims Transformation¶
You can provide a callback to transform the claims of the incoming token after validation. Either use the helper method, e.g.:
services.AddLocalApiAuthentication(principal =>
{
principal.Identities.First().AddClaim(new Claim("additional_claim", "additional_value"));
return Task.FromResult(principal);
});
…or implement the event on the options if you add the authentication handler manually.
Tools¶
The IdentityServerTools
class is a collection of useful internal tools that you might need when writing extensibility code
for IdentityServer. To use it, inject it into your code, e.g. a controller:
public MyController(IdentityServerTools tools)
{
_tools = tools;
}
The IssueJwtAsync
method allows creating JWT tokens using the IdentityServer token creation engine. The IssueClientJwtAsync
is an easier
version of that for creating tokens for server-to-server communication (e.g. when you have to call an IdentityServer protected API from your code):
public async Task<IActionResult> MyAction()
{
var token = await _tools.IssueClientJwtAsync(
clientId: "client_id",
lifetime: 3600,
audiences: new[] { "backend.api" });
// more code
}
Discovery Endpoint¶
The discovery endpoint can be used to retrieve metadata about your IdentityServer - it returns information like the issuer name, key material, supported scopes etc. See the spec for more details.
The discovery endpoint is available via /.well-known/openid-configuration relative to the base address.
Note
You can use the IdentityModel client library to programmatically access the discovery endpoint from .NET code. For more information check the IdentityModel docs.
Token Endpoint¶
The token endpoint can be used to programmatically request tokens.
It supports the password
, authorization_code
, client_credentials
, refresh_token
and urn:ietf:params:oauth:grant-type:device_code
grant types.
Furthermore the token endpoint can be extended to support extension grant types.
Note
IdentityServer supports a subset of the OpenID Connect and OAuth 2.0 token request parameters. For a full list, see here.
client_id
client identifier (required – Either in the body or as part of the authorization header.)
client_secret
client secret either in the post body, or as a basic authentication header. Optional.
grant_type
authorization_code
,client_credentials
,password
,refresh_token
,urn:ietf:params:oauth:grant-type:device_code
or customscope
one or more registered scopes. If not specified, a token for all explicitly allowed scopes will be issued.
redirect_uri
required for the
authorization_code
grant typecode
the authorization code (required for
authorization_code
grant type)code_verifier
PKCE proof key
username
resource owner username (required for
password
grant type)password
resource owner password (required for
password
grant type)acr_values
allows passing in additional authentication related information for the
password
grant type - identityserver special cases the following proprietary acr_values:idp:name_of_idp
bypasses the login/home realm screen and forwards the user directly to the selected identity provider (if allowed per client configuration)tenant:name_of_tenant
can be used to pass a tenant name to the token endpointrefresh_token
the refresh token (required for
refresh_token
grant type)device_code
the device code (required for
urn:ietf:params:oauth:grant-type:device_code
grant type)
Example¶
POST /connect/token
CONTENT-TYPE application/x-www-form-urlencoded
client_id=client1&
client_secret=secret&
grant_type=authorization_code&
code=hdh922&
redirect_uri=https://myapp.com/callback
(Form-encoding removed and line breaks added for readability)
Note
You can use the IdentityModel client library to programmatically access the token endpoint from .NET code. For more information check the IdentityModel docs.
UserInfo Endpoint¶
The UserInfo endpoint can be used to retrieve identity information about a user (see spec).
The caller needs to send a valid access token representing the user. Depending on the granted scopes, the UserInfo endpoint will return the mapped claims (at least the openid scope is required).
Example¶
GET /connect/userinfo
Authorization: Bearer <access_token>
HTTP/1.1 200 OK
Content-Type: application/json
{
"sub": "248289761001",
"name": "Bob Smith",
"given_name": "Bob",
"family_name": "Smith",
"role": [
"user",
"admin"
]
}
Note
You can use the IdentityModel client library to programmatically access the userinfo endpoint from .NET code. For more information check the IdentityModel docs.
Introspection Endpoint¶
The introspection endpoint is an implementation of RFC 7662.
It can be used to validate reference tokens (or JWTs if the consumer does not have support for appropriate JWT or cryptographic libraries).
The introspection endpoint requires authentication - since the client of an introspection endpoint is an API, you configure the secret on the ApiResource
.
Example¶
POST /connect/introspect
Authorization: Basic xxxyyy
token=<token>
A successful response will return a status code of 200 and either an active or inactive token:
{
"active": true,
"sub": "123"
}
Unknown or expired tokens will be marked as inactive:
{
"active": false,
}
An invalid request will return a 400, an unauthorized request 401.
Note
You can use the IdentityModel client library to programmatically access the introspection endpoint from .NET code. For more information check the IdentityModel docs.
Revocation Endpoint¶
This endpoint allows revoking access tokens (reference tokens only) and refresh token. It implements the token revocation specification (RFC 7009).
token
the token to revoke (required)
token_type_hint
either
access_token
orrefresh_token
(optional)
Example¶
POST /connect/revocation HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token
Note
You can use the IdentityModel client library to programmatically access the revocation endpoint from .NET code. For more information check the IdentityModel docs.
End Session Endpoint¶
The end session endpoint can be used to trigger single sign-out (see spec).
To use the end session endpoint a client application will redirect the user’s browser to the end session URL. All applications that the user has logged into via the browser during the user’s session can participate in the sign-out.
Note
The URL for the end session endpoint is available via the discovery endpoint.
Parameters¶
id_token_hint
When the user is redirected to the endpoint, they will be prompted if they really want to sign-out.
This prompt can be bypassed by a client sending the original id_token received from authentication.
This is passed as a query string parameter called id_token_hint
.
post_logout_redirect_uri
If a valid id_token_hint
is passed, then the client may also send a post_logout_redirect_uri
parameter.
This can be used to allow the user to redirect back to the client after sign-out.
The value must match one of the client’s pre-configured PostLogoutRedirectUris (client docs).
state
If a valid post_logout_redirect_uri
is passed, then the client may also send a state
parameter.
This will be returned back to the client as a query string parameter after the user redirects back to the client.
This is typically used by clients to round-trip state across the redirect.
Example¶
GET /connect/endsession?id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlOGFkZmMzMjU1OTEyNzI0ZDY4NWZmYmIwOThjNDEyIiwidHlwIjoiSldUIn0.eyJuYmYiOjE0OTE3NjUzMjEsImV4cCI6MTQ5MTc2NTYyMSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoianNfb2lkYyIsIm5vbmNlIjoiYTQwNGFjN2NjYWEwNGFmNzkzNmJjYTkyNTJkYTRhODUiLCJpYXQiOjE0OTE3NjUzMjEsInNpZCI6IjI2YTYzNWVmOTQ2ZjRiZGU3ZWUzMzQ2ZjFmMWY1NTZjIiwic3ViIjoiODg0MjExMTMiLCJhdXRoX3RpbWUiOjE0OTE3NjUzMTksImlkcCI6ImxvY2FsIiwiYW1yIjpbInB3ZCJdfQ.STzOWoeVYMtZdRAeRT95cMYEmClixWkmGwVH2Yyiks9BETotbSZiSfgE5kRh72kghN78N3-RgCTUmM2edB3bZx4H5ut3wWsBnZtQ2JLfhTwJAjaLE9Ykt68ovNJySbm8hjZhHzPWKh55jzshivQvTX0GdtlbcDoEA1oNONxHkpDIcr3pRoGi6YveEAFsGOeSQwzT76aId-rAALhFPkyKnVc-uB8IHtGNSyRWLFhwVqAdS3fRNO7iIs5hYRxeFSU7a5ZuUqZ6RRi-bcDhI-djKO5uAwiyhfpbpYcaY_TxXWoCmq8N8uAw9zqFsQUwcXymfOAi2UF3eFZt02hBu-shKA&post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A7017%2Findex.html
Note
You can use the IdentityModel client library to programmatically create end_session requests .NET code. For more information check the IdentityModel docs.
IdentityServer Options¶
IssuerUri
Set the issuer name that will appear in the discovery document and the issued JWT tokens. It is recommended to not set this property, which infers the issuer name from the host name that is used by the clients.
LowerCaseIssuerUri
Set to
false
to preserve the original casing of the IssuerUri. Defaults totrue
.
AccessTokenJwtType
Specifies the value used for the JWT typ header for access tokens (defaults to
at+jwt
).
EmitScopesAsSpaceDelimitedStringInJwt
Specifies whether scopes in JWTs are emitted as array or string
EmitStaticAudienceClaim
Emits an
aud
claim with the format issuer/resources. Defaults to false.
Endpoints¶
Allows enabling/disabling individual endpoints, e.g. token, authorize, userinfo etc.
By default all endpoints are enabled, but you can lock down your server by disabling endpoint that you don’t need.
EnableJwtRequestUri
JWT request_uri processing is enabled on the authorize endpoint. Defaults to
false
.
Discovery¶
Allows enabling/disabling various sections of the discovery document, e.g. endpoints, scopes, claims, grant types etc.
The CustomEntries
dictionary allows adding custom elements to the discovery document.
Authentication¶
CookieAuthenticationScheme
Sets the cookie authentication scheme configured by the host used for interactive users. If not set, the scheme will be inferred from the host’s default authentication scheme. This setting is typically used when AddPolicyScheme is used in the host as the default scheme.
CookieLifetime
The authentication cookie lifetime (only effective if the IdentityServer-provided cookie handler is used).
CookieSlidingExpiration
Specifies if the cookie should be sliding or not (only effective if the IdentityServer-provided cookie handler is used).
CookieSameSiteMode
Specifies the SameSite mode for the internal cookies.
RequireAuthenticatedUserForSignOutMessage
Indicates if user must be authenticated to accept parameters to end session endpoint. Defaults to false.
CheckSessionCookieName
The name of the cookie used for the check session endpoint.
CheckSessionCookieDomain
The domain of the cookie used for the check session endpoint.
CheckSessionCookieSameSiteMode
The SameSite mode of the cookie used for the check session endpoint.
RequireCspFrameSrcForSignout
If set, will require frame-src CSP headers being emitting on the end session callback endpoint which renders iframes to clients for front-channel signout notification. Defaults to true.
Events¶
Allows configuring if and which events should be submitted to a registered event sink. See here for more information on events.
InputLengthRestrictions¶
Allows setting length restrictions on various protocol parameters like client id, scope, redirect URI etc.
UserInteraction¶
LoginUrl
,LogoutUrl
,ConsentUrl
,ErrorUrl
,DeviceVerificationUrl
Sets the URLs for the login, logout, consent, error and device verification pages.
LoginReturnUrlParameter
Sets the name of the return URL parameter passed to the login page. Defaults to returnUrl.
LogoutIdParameter
Sets the name of the logout message id parameter passed to the logout page. Defaults to logoutId.
ConsentReturnUrlParameter
Sets the name of the return URL parameter passed to the consent page. Defaults to returnUrl.
ErrorIdParameter
Sets the name of the error message id parameter passed to the error page. Defaults to errorId.
CustomRedirectReturnUrlParameter
Sets the name of the return URL parameter passed to a custom redirect from the authorization endpoint. Defaults to returnUrl.
DeviceVerificationUserCodeParameter
Sets the name of the user code parameter passed to the device verification page. Defaults to userCode.
CookieMessageThreshold
Certain interactions between IdentityServer and some UI pages require a cookie to pass state and context (any of the pages above that have a configurable “message id” parameter). Since browsers have limits on the number of cookies and their size, this setting is used to prevent too many cookies being created. The value sets the maximum number of message cookies of any type that will be created. The oldest message cookies will be purged once the limit has been reached. This effectively indicates how many tabs can be opened by a user when using IdentityServer.
Caching¶
These settings only apply if the respective caching has been enabled in the services configuration in startup.
ClientStoreExpiration
Cache duration of client configuration loaded from the client store.
ResourceStoreExpiration
Cache duration of identity and API resource configuration loaded from the resource store.
CORS¶
IdentityServer supports CORS for some of its endpoints. The underlying CORS implementation is provided from ASP.NET Core, and as such it is automatically registered in the dependency injection system.
CorsPolicyName
Name of the CORS policy that will be evaluated for CORS requests into IdentityServer (defaults to
"IdentityServer"
). The policy provider that handles this is implemented in terms of theICorsPolicyService
registered in the dependency injection system. If you wish to customize the set of CORS origins allowed to connect, then it is recommended that you provide a custom implementation ofICorsPolicyService
.
CorsPaths
The endpoints within IdentityServer where CORS is supported. Defaults to the discovery, user info, token, and revocation endpoints.
PreflightCacheDuration
Nullable<TimeSpan> indicating the value to be used in the preflight Access-Control-Max-Age response header. Defaults to null indicating no caching header is set on the response.
CSP (Content Security Policy)¶
IdentityServer emits CSP headers for some responses, where appropriate.
Level
The level of CSP to use. CSP Level 2 is used by default, but if older browsers must be supported then this be changed to
CspLevel.One
to accommodate them.
AddDeprecatedHeader
Indicates if the older
X-Content-Security-Policy
CSP header should also be emitted (in addition to the standards-based header value). Defaults to true.
Device Flow¶
DefaultUserCodeType
The user code type to use, unless set at the client level. Defaults to Numeric, a 9-digit code.
Interval
Defines the minimum allowed polling interval on the token endpoint. Defaults to 5.
Mutual TLS¶
Enabled
Specifies if MTLS support should be enabled. Defaults to
false
.
ClientCertificateAuthenticationScheme
Specifies the name of the authentication handler for X.509 client certificates. Defaults to
"Certificate"
.
DomainName
Specifies either the name of the sub-domain or full domain for running the MTLS endpoints (will use path-based endpoints if not set). Use a simple string (e.g. “mtls”) to set a sub-domain, use a full domain name (e.g. “identityserver-mtls.io”) to set a full domain name. When a full domain name is used, you also need to set the
IssuerName
to a fixed value.
AlwaysEmitConfirmationClaim
Specifies whether a cnf claim gets emitted for access tokens if a client certificate was present. Normally the cnf claims only gets emitted if the client used the client certificate for authentication, setting this to true, will set the claim regardless of the authentication method. (defaults to false).
Identity Resource¶
This class models an identity resource.
Enabled
Indicates if this resource is enabled and can be requested. Defaults to true.
Name
The unique name of the identity resource. This is the value a client will use for the scope parameter in the authorize request.
DisplayName
This value will be used e.g. on the consent screen.
Description
This value will be used e.g. on the consent screen.
Required
Specifies whether the user can de-select the scope on the consent screen (if the consent screen wants to implement such a feature). Defaults to false.
Emphasize
Specifies whether the consent screen will emphasize this scope (if the consent screen wants to implement such a feature). Use this setting for sensitive or important scopes. Defaults to false.
ShowInDiscoveryDocument
Specifies whether this scope is shown in the discovery document. Defaults to true.
UserClaims
List of associated user claim types that should be included in the identity token.
API Scope¶
This class models an OAuth scope.
Enabled
Indicates if this resource is enabled and can be requested. Defaults to true.
Name
The unique name of the API. This value is used for authentication with introspection and will be added to the audience of the outgoing access token.
DisplayName
This value can be used e.g. on the consent screen.
Description
This value can be used e.g. on the consent screen.
UserClaims
List of associated user claim types that should be included in the access token.
Defining API scope in appsettings.json¶
The AddInMemoryApiResource
extension method also supports adding clients from the ASP.NET Core configuration file:
"IdentityServer": {
"IssuerUri": "urn:sso.company.com",
"ApiScopes": [
{
"Name": "IdentityServerApi"
},
{
"Name": "resource1.scope1"
},
{
"Name": "resource2.scope1"
},
{
"Name": "scope3"
},
{
"Name": "shared.scope"
},
{
"Name": "transaction",
"DisplayName": "Transaction",
"Description": "A transaction"
}
]
}
Then pass the configuration section to the AddInMemoryApiScopes
method:
AddInMemoryApiScopes(configuration.GetSection("IdentityServer:ApiScopes"))
API Resource¶
This class models an API resource.
Enabled
Indicates if this resource is enabled and can be requested. Defaults to true.
Name
The unique name of the API. This value is used for authentication with introspection and will be added to the audience of the outgoing access token.
DisplayName
This value can be used e.g. on the consent screen.
Description
This value can be used e.g. on the consent screen.
ApiSecrets
The API secret is used for the introspection endpoint. The API can authenticate with introspection using the API name and secret.
AllowedAccessTokenSigningAlgorithms
List of allowed signing algorithms for access token. If empty, will use the server default signing algorithm.
UserClaims
List of associated user claim types that should be included in the access token.
Scopes
List of API scope names.
Defining API resources in appsettings.json¶
The AddInMemoryApiResource
extensions method also supports adding API resources from the ASP.NET Core configuration file:
"IdentityServer": {
"IssuerUri": "urn:sso.company.com",
"ApiResources": [
{
"Name": "resource1",
"DisplayName": "Resource #1",
"Scopes": [
"resource1.scope1",
"shared.scope"
]
},
{
"Name": "resource2",
"DisplayName": "Resource #2",
"UserClaims": [
"name",
"email"
],
"Scopes": [
"resource2.scope1",
"shared.scope"
]
}
]
}
Then pass the configuration section to the AddInMemoryApiResource
method:
AddInMemoryApiResources(configuration.GetSection("IdentityServer:ApiResources"))
Client¶
The Client
class models an OpenID Connect or OAuth 2.0 client -
e.g. a native application, a web application or a JS-based application.
Basics¶
Enabled
Specifies if client is enabled. Defaults to true.
ClientId
Unique ID of the client
ClientSecrets
List of client secrets - credentials to access the token endpoint.
RequireClientSecret
Specifies whether this client needs a secret to request tokens from the token endpoint (defaults to
true
)RequireRequestObject
Specifies whether this client needs to wrap the authorize request parameters in a JWT (defaults to
false
)AllowedGrantTypes
Specifies the grant types the client is allowed to use. Use the
GrantTypes
class for common combinations.RequirePkce
Specifies whether clients using an authorization code based grant type must send a proof key (defaults to
true
).AllowPlainTextPkce
Specifies whether clients using PKCE can use a plain text code challenge (not recommended - and default to
false
)RedirectUris
Specifies the allowed URIs to return tokens or authorization codes to
AllowedScopes
By default a client has no access to any resources - specify the allowed resources by adding the corresponding scopes names
AllowOfflineAccess
Specifies whether this client can request refresh tokens (be requesting the
offline_access
scope)AllowAccessTokensViaBrowser
Specifies whether this client is allowed to receive access tokens via the browser. This is useful to harden flows that allow multiple response types (e.g. by disallowing a hybrid flow client that is supposed to use code id_token to add the token response type and thus leaking the token to the browser.
Properties
Dictionary to hold any custom client-specific values as needed.
Authentication/Logout¶
PostLogoutRedirectUris
Specifies allowed URIs to redirect to after logout. See the OIDC Connect Session Management spec for more details.
FrontChannelLogoutUri
Specifies logout URI at client for HTTP based front-channel logout. See the OIDC Front-Channel spec for more details.
FrontChannelLogoutSessionRequired
Specifies if the user’s session id should be sent to the FrontChannelLogoutUri. Defaults to true.
BackChannelLogoutUri
Specifies logout URI at client for HTTP based back-channel logout. See the OIDC Back-Channel spec for more details.
BackChannelLogoutSessionRequired
Specifies if the user’s session id should be sent in the request to the BackChannelLogoutUri. Defaults to true.
EnableLocalLogin
Specifies if this client can use local accounts, or external IdPs only. Defaults to true.
IdentityProviderRestrictions
Specifies which external IdPs can be used with this client (if list is empty all IdPs are allowed). Defaults to empty.
UserSsoLifetime
added in 2.3The maximum duration (in seconds) since the last time the user authenticated. Defaults to
null
. You can adjust the lifetime of a session token to control when and how often a user is required to reenter credentials instead of being silently authenticated, when using a web application.
Token¶
IdentityTokenLifetime
Lifetime to identity token in seconds (defaults to 300 seconds / 5 minutes)
AllowedIdentityTokenSigningAlgorithms
List of allowed signing algorithms for identity token. If empty, will use the server default signing algorithm.
AccessTokenLifetime
Lifetime of access token in seconds (defaults to 3600 seconds / 1 hour)
AuthorizationCodeLifetime
Lifetime of authorization code in seconds (defaults to 300 seconds / 5 minutes)
AbsoluteRefreshTokenLifetime
Maximum lifetime of a refresh token in seconds. Defaults to 2592000 seconds / 30 days
SlidingRefreshTokenLifetime
Sliding lifetime of a refresh token in seconds. Defaults to 1296000 seconds / 15 days
RefreshTokenUsage
ReUse
the refresh token handle will stay the same when refreshing tokensOneTime
the refresh token handle will be updated when refreshing tokens. This is the default.RefreshTokenExpiration
Absolute
the refresh token will expire on a fixed point in time (specified by the AbsoluteRefreshTokenLifetime). This is the default.Sliding
when refreshing the token, the lifetime of the refresh token will be renewed (by the amount specified in SlidingRefreshTokenLifetime). The lifetime will not exceed AbsoluteRefreshTokenLifetime.UpdateAccessTokenClaimsOnRefresh
Gets or sets a value indicating whether the access token (and its claims) should be updated on a refresh token request.
AccessTokenType
Specifies whether the access token is a reference token or a self contained JWT token (defaults to Jwt).
IncludeJwtId
Specifies whether JWT access tokens should have an embedded unique ID (via the jti claim). Defaults to
true
.AllowedCorsOrigins
If specified, will be used by the default CORS policy service implementations (In-Memory and EF) to build a CORS policy for JavaScript clients.
Claims
Allows settings claims for the client (will be included in the access token).
AlwaysSendClientClaims
If set, the client claims will be sent for every flow. If not, only for client credentials flow (default is false)
AlwaysIncludeUserClaimsInIdToken
When requesting both an id token and access token, should the user claims always be added to the id token instead of requiring the client to use the userinfo endpoint. Default is false.
ClientClaimsPrefix
If set, the prefix client claim types will be prefixed with. Defaults to client_. The intent is to make sure they don’t accidentally collide with user claims.
PairWiseSubjectSalt
Salt value used in pair-wise subjectId generation for users of this client.
Consent Screen¶
RequireConsent
Specifies whether a consent screen is required. Defaults to
false
.AllowRememberConsent
Specifies whether user can choose to store consent decisions. Defaults to
true
.ConsentLifetime
Lifetime of a user consent in seconds. Defaults to null (no expiration).
ClientName
Client display name (used for logging and consent screen)
ClientUri
URI to further information about client (used on consent screen)
LogoUri
URI to client logo (used on consent screen)
Device flow¶
UserCodeType
Specifies the type of user code to use for the client. Otherwise falls back to default.
DeviceCodeLifetime
Lifetime to device code in seconds (defaults to 300 seconds / 5 minutes)
GrantValidationResult¶
The GrantValidationResult
class models the outcome of grant validation for extensions grants and resource owner password grants.
The most common usage is to either new it up using an identity (success case):
context.Result = new GrantValidationResult(
subject: "818727",
authenticationMethod: "custom",
claims: optionalClaims);
…or using an error and description (failure case):
context.Result = new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
"invalid custom credential");
In both case you can pass additional custom values that will be included in the token response.
Profile Service¶
Often IdentityServer requires identity information about users when creating tokens or when handling requests to the userinfo or introspection endpoints. By default, IdentityServer only has the claims in the authentication cookie to draw upon for this identity data.
It is impractical to put all of the possible claims needed for users into the cookie, so IdentityServer defines an extensibility point for allowing claims to be dynamically loaded as needed for a user.
This extensibility point is the IProfileService
and it is common for a developer to implement this interface to access a custom database or API that contains the identity data for users.
IProfileService APIs¶
GetProfileDataAsync
The API that is expected to load claims for a user. It is passed an instance of
ProfileDataRequestContext
.IsActiveAsync
The API that is expected to indicate if a user is currently allowed to obtain tokens. It is passed an instance of
IsActiveContext
.
ProfileDataRequestContext¶
Models the request for user claims and is the vehicle to return those claims. It contains these properties:
Subject
The
ClaimsPrincipal
modeling the user.Client
The
Client
for which the claims are being requested.RequestedClaimTypes
The collection of claim types being requested.
Caller
An identifier for the context in which the claims are being requested (e.g. an identity token, an access token, or the user info endpoint). The constant
IdentityServerConstants.ProfileDataCallers
contains the different constant values.IssuedClaims
The list of
Claim
s that will be returned. This is expected to be populated by the customIProfileService
implementation.AddRequestedClaims
Extension method on the
ProfileDataRequestContext
to populate theIssuedClaims
, but first filters the claims based onRequestedClaimTypes
.
Requested scopes and claims mapping¶
The scopes requested by the client control what user claims are returned in the tokens to the client.
The GetProfileDataAsync
method is responsible for dynamically obtaining those claims based on the RequestedClaimTypes
collection on the ProfileDataRequestContext
.
The RequestedClaimTypes
collection is populated based on the user claims defined on the resources that model the scopes.
If requesting an identity token and the scopes requested are an identity resources, then the claims in the RequestedClaimTypes
will be populated based on the user claim types defined in the IdentityResource
.
If requesting an access token and the scopes requested are an API resources, then the claims in the RequestedClaimTypes
will be populated based on the user claim types defined in the ApiResource
and/or the Scope
.
IsActiveContext¶
Models the request to determine if the user is currently allowed to obtain tokens. It contains these properties:
Subject
The
ClaimsPrincipal
modeling the user.Client
The
Client
for which the claims are being requested.Caller
An identifier for the context in which the claims are being requested (e.g. an identity token, an access token, or the user info endpoint). The constant
IdentityServerConstants.ProfileDataCallers
contains the different constant values.IsActive
The flag indicating if the user is allowed to obtain tokens. This is expected to be assigned by the custom
IProfileService
implementation.
IdentityServer Interaction Service¶
The IIdentityServerInteractionService
interface is intended to provide services to be used by the user interface to communicate with IdentityServer, mainly pertaining to user interaction.
It is available from the dependency injection system and would normally be injected as a constructor parameter into your MVC controllers for the user interface of IdentityServer.
IIdentityServerInteractionService APIs¶
GetAuthorizationContextAsync
Returns the
AuthorizationRequest
based on thereturnUrl
passed to the login or consent pages.IsValidReturnUrl
Indicates if the
returnUrl
is a valid URL for redirect after login or consent.GetErrorContextAsync
Returns the
ErrorMessage
based on theerrorId
passed to the error page.GetLogoutContextAsync
Returns the
LogoutRequest
based on thelogoutId
passed to the logout page.CreateLogoutContextAsync
Used to create a
logoutId
if there is not one presently. This creates a cookie capturing all the current state needed for signout and thelogoutId
identifies that cookie. This is typically used when there is no currentlogoutId
and the logout page must capture the current user’s state needed for sign-out prior to redirecting to an external identity provider for signout. The newly createdlogoutId
would need to be round-tripped to the external identity provider at signout time, and then used on the signout callback page in the same way it would be on the normal logout page.GrantConsentAsync
Accepts a
ConsentResponse
to inform IdentityServer of the user’s consent to a particularAuthorizationRequest
.DenyAuthorizationAsync
Accepts a
AuthorizationError
to inform IdentityServer of the error to return to the client for a particularAuthorizationRequest
.GetAllUserGrantsAsync
Returns a collection of
Grant
for the user. These represent a user’s consent or a clients access to a user’s resource.RevokeUserConsentAsync
Revokes all of a user’s consents and grants for a client.
RevokeTokensForCurrentSessionAsync
Revokes all of a user’s consents and grants for clients the user has signed into during their current session.
ResourceValidationResult¶
Resources
The resources of the result.
ParsedScopes
The parsed scopes represented by the result.
RawScopeValues
The original (raw) scope values represented by the validated result.
ErrorMessage¶
DisplayMode
The display mode passed from the authorization request.
UiLocales
The UI locales passed from the authorization request.
Error
The error code.
RequestId
The per-request identifier. This can be used to display to the end user and can be used in diagnostics.
LogoutRequest¶
ClientId
The client identifier that initiated the request.
PostLogoutRedirectUri
The URL to redirect the user to after they have logged out.
SessionId
The user’s current session id.
SignOutIFrameUrl
The URL to render in an
<iframe>
on the logged out page to enable single sign-out.Parameters
The entire parameter collection passed to the end session endpoint.
ShowSignoutPrompt
Indicates if the user should be prompted for signout based upon the parameters passed to the end session endpoint.
ConsentResponse¶
ScopesValuesConsented
The collection of scopes the user consented to.
RememberConsent
Flag indicating if the user’s consent is to be persisted.
Description
Optional description the user can set for the grant (e.g. the name of the device being used when consent is given). This can be presented back to the user from the persisted grant service.
Error
Error, if any, for the consent response. This will be returned to the client in the authorization response.
ErrorDescription
Error description. This will be returned to the client in the authorization response.
Grant¶
SubjectId
The subject id that allowed the grant.
ClientId
The client identifier for the grant.
Description
The description the user assigned to the client or device being authorized.
Scopes
The collection of scopes granted.
CreationTime
The date and time when the grant was granted.
Expiration
The date and time when the grant will expire.
Device Flow Interaction Service¶
The IDeviceFlowInteractionService
interface is intended to provide services to be used by the user interface to communicate with IdentityServer during device flow authorization.
It is available from the dependency injection system and would normally be injected as a constructor parameter into your MVC controllers for the user interface of IdentityServer.
IDeviceFlowInteractionService APIs¶
GetAuthorizationContextAsync
Returns the
DeviceFlowAuthorizationRequest
based on theuserCode
passed to the login or consent pages.DeviceFlowInteractionResult
Completes device authorization for the given
userCode
.
DeviceFlowInteractionResult¶
IsError
Specifies if the authorization request errored.
ErrorDescription
Error description upon failure.
Entity Framework Support¶
An EntityFramework-based implementation is provided for the configuration and operational data extensibility points in IdentityServer. The use of EntityFramework allows any EF-supported database to be used with this library.
The code for this library is located here (with the underlying storage code here) and the NuGet package is here.
The features provided by this library are broken down into two main areas: configuration store and operational store support. These two different areas can be used independently or together, based upon the needs of the hosting application.
Configuration Store support for Clients, Resources, and CORS settings¶
If client, identity resource, API resource, or CORS data is desired to be loaded from a EF-supported database
(rather than use in-memory configuration), then the configuration store can be used.
This support provides implementations of the IClientStore
, IResourceStore
, and the ICorsPolicyService
extensibility points.
These implementations use a DbContext
-derived class called ConfigurationDbContext
to model the tables in the database.
To use the configuration store support, use the AddConfigurationStore
extension method after the call to AddIdentityServer
:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
const string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;database=Ourstudio.IdentityServer.EntityFramework-2.0.0;trusted_connection=yes;";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer()
// this adds the config data from DB (clients, resources, CORS)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
});
}
To configure the configuration store, use the ConfigurationStoreOptions
options object passed to the configuration callback.
ConfigurationStoreOptions¶
This options class contains properties to control the configuration store and ConfigurationDbContext
.
ConfigureDbContext
Delegate of type
Action<DbContextOptionsBuilder>
used as a callback to configure the underlyingConfigurationDbContext
. The delegate can configure theConfigurationDbContext
in the same way if EF were being used directly withAddDbContext
, which allows any EF-supported database to be used.DefaultSchema
Allows setting the default database schema name for all the tables in the
ConfigurationDbContext
options.DefaultSchema = "myConfigurationSchema";
If you need to change the schema for the Migration History Table, you can chain another action to the UseSqlServer
:
options.ConfigureDbContext = b =>
b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly).MigrationsHistoryTable("MyConfigurationMigrationTable", "myConfigurationSchema"));
Operational Store support for persisted grants¶
If persisted grants are desired to be loaded from a EF-supported database (rather than the default in-memory database), then the operational store can be used.
This support provides implementations of the IPersistedGrantStore
extensibility point.
The implementation uses a DbContext
-derived class called PersistedGrantDbContext
to model the table in the database.
To use the operational store support, use the AddOperationalStore
extension method after the call to AddIdentityServer
:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
const string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;database=Ourstudio.IdentityServer.EntityFramework;trusted_connection=yes;";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer()
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 3600; // interval in seconds (default is 3600)
});
}
To configure the operational store, use the OperationalStoreOptions
options object passed to the configuration callback.
OperationalStoreOptions¶
This options class contains properties to control the operational store and PersistedGrantDbContext
.
ConfigureDbContext
Delegate of type
Action<DbContextOptionsBuilder>
used as a callback to configure the underlyingPersistedGrantDbContext
. The delegate can configure thePersistedGrantDbContext
in the same way if EF were being used directly withAddDbContext
, which allows any EF-supported database to be used.DefaultSchema
Allows setting the default database schema name for all the tables in the
PersistedGrantDbContext
.EnableTokenCleanup
Indicates whether expired grants will be automatically cleaned up from the database. The default is
false
.TokenCleanupInterval
The token cleanup interval (in seconds). The default is 3600 (1 hour).
Note
The token cleanup feature does not remove persisted grants that are consumed (see persisted grants).
Database creation and schema changes across different versions of IdentityServer¶
It is very likely that across different versions of IdentityServer (and the EF support) that the database schema will change to accommodate new and changing features.
We do not provide any support for creating your database or migrating your data from one version to another. You are expected to manage the database creation, schema changes, and data migration in any way your organization sees fit.
Using EF migrations is one possible approach to this. If you do wish to use migrations, then see the EF quickstart for samples on how to get started, or consult the Microsoft documentation on EF migrations.
ASP.NET Identity Support¶
An ASP.NET Identity-based implementation is provided for managing the identity database for users of IdentityServer. This implementation implements the extensibility points in IdentityServer needed to load identity data for your users to emit claims into tokens.
The NuGet package is here.
To use this library, configure ASP.NET Identity normally.
Then use the AddAspNetIdentity
extension method after the call to AddIdentityServer
:
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityServer()
.AddAspNetIdentity<ApplicationUser>();
}
AddAspNetIdentity
requires as a generic parameter the class that models your user for ASP.NET Identity (and the same one passed to AddIdentity
to configure ASP.NET Identity).
This configures IdentityServer to use the ASP.NET Identity implementations of IUserClaimsPrincipalFactory
, IResourceOwnerPasswordValidator
, and IProfileService
.
It also configures some of ASP.NET Identity’s options for use with IdentityServer (such as claim types to use and authentication cookie settings).
When using your own implementation of IUserClaimsPrincipalFactory
, make sure that you register it before calling the IdentityServer AddAspNetIdentity
extension method.
Blog posts¶
Team posts¶
2020¶
Automatic Token Management for ASP.NET Core and Worker Services 1.0
Mutual TLS and Proof-of-Possession Access Tokens – Part 1: Setup
Hardening OpenID Connect/OAuth Authorize Requests (and Responses)
OAuth 2.0: The long Road to Proof-of-Possession Access Tokens
Outsourcing IdentityServer4 Token Signing to Azure Key Vault
2019¶
2018¶
2017¶
Community posts¶
Blazor WebAssembly authentication and authorization with IdentityServer4
Securing Hangfire Dashboard using an OpenID Connect server (IdentityServer 4)
Connecting Zendesk and IdentityServer 4 SAML 2.0 Identity Provider
Self-issuing an IdentityServer4 token in an IdentityServer4 service
Full Server Logout with IdentityServer4 and OpenID Connect Implicit Flow
IdentityServer4, ASP.NET Identity, Web API and Angular in a single Project
Secure your .NETCore web applications using IdentityServer 4
ASP.NET Core IdentityServer4 Resource Owner Password Flow with custom UserRepository
Secure ASP.NET Core MVC with Angular using IdentityServer4 OpenID Connect Hybrid Flow
Implementing Two-factor authentication with IdentityServer4 and Twilio
Securing a Vue.js app using OpenID Connect Code Flow with PKCE and IdentityServer4
OpenID Connect back-channel logout using Azure Redis Cache and IdentityServer4
Videos¶
2020¶
2019¶
2018¶
2017¶
14/09 [Microsoft Learning] – Introduction to IdentityServer for ASP.NET Core - Brock Allen
14/06 [NDC Oslo] – Implementing Authorization for Web Applications and APIs
22/02 [NDC Mini Copenhagen] – IdentityServer4: New & Improved for ASP.NET Core - Dominick Baier
16/01 [NDC London] – IdentityServer4: New and Improved for ASP.NET Core
16/01 [NDC London] – Building JavaScript and mobile/native Clients for Token-based Architectures