Duende (Identity Server), Certificates and Postman. What could go wrong? Signed JWT Requests the "easy" way. Part 01
Have you ever wondered if there is an "easy" way to test the signed JWT request flow.
Well, there is, if you have a paid provide such as OKTA or AUTH0.
But what happens if you need to test the flow while developing a Proof of Concept (POC) or while you wait for your dev environment is ready for? (yes, it happens a lot!)
In any case you can use Duende Identity Server (previously known as Identity Server 4 or IDS4)
This post is going to be 3-part series
- Installing duende locally, docker, azure and NGNIX.
- Using postman to sign your requests and obtain bearer tokens.
- C# code to sign your requests and obtain bearer tokens.
NOTE: If you just want to use the docker image hub, go to the Docker and NGNIX section.
Duende Source Code
First things first, let us clone the repo and I let us do a bare bones host, I usually do this for testing, for Development you should consider a data store, but we will be using config files as they are easy to update and test fast.
git clone https://github.com/DuendeSoftware/IdentityServer.git
Open the solution in visual studio 2022. By the way, there is a free version for individuals, and you can get it from.

We will be working mainly on the Host.Main project.
Setup the project as the startup project and execute, congratulations, you have an IDS up and running.



But wait, I need to use the IDS in docker. Well easy enough to do.
Right click on the project and add docker support

This will create the Dockerfile and some settings to be executed. Note that you will need Docker Desktop to test and debug.
If you execute the Container (Dockerfile) run command, visual studio will create the container, start it and attached the debugger.

Now for the custom implementation.
Custom Implementation
Add an appsettings.json file to the project
{
"IsContainerExternalConfiguration": false,
"ContainerExternalConfiguration": "/appsettings/appsettings.external.duende.json",
"IsUseReverseProxy": false,
"ProxyBasePath": "/yourvirtualdirectory",
"ApiScopes": [
{
"Name": "api.readonly"
}
],
"ApiResources": [
{
"Name": "CustomResource",
"DisplayName": "Test Resource",
"Scopes": [
"api.readonly"
]
}
],
"Clients": [
{
"ClientId": "myclient",
"ClientSecrets": [
{
"Type": "X509CertificateBase64",
"Value": "MIIDA...V"
}
],
"AllowedGrantTypes": [ "client_credentials" ],
"AllowedScopes": [ "api.readonly" ]
}
],
"Users": [
{
"SubjectId": "1",
"Username": "myuser",
"Password": "horseWalkingBeachMouse4573@#%",
"Name": "Darth Seldon",
"GivenName": "Darth",
"FamilyName": "Seldon",
"Email": "someemail@server.com",
"EmailVerified": "true",
"WebSite": "https://darthseldon.net"
}
]
}
Make sure on the properties you copy newer or always to the output folder.
These settings will determine if the host will be on a container, stand alone or used on a reverse proxy and setup one user and one client using the signed request flow.
We will go over the settings later on the post.
Now we will remove the Duende provided configuration
On the configuration folder

Open each of the files and replace the code with its corresponding match
Clients.cs
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Collections.Generic;
using Duende.IdentityServer.Models;
namespace IdentityServerHost.Configuration;
public static class Clients
{
public static IEnumerable<Client> Get()
{
var clients = new List<Client>();
clients.AddRange(ClientsConsole.Get());
return clients;
}
}
ClientsConsole.cs
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Collections.Generic;
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
namespace IdentityServerHost.Configuration;
public static class ClientsConsole
{
public static IEnumerable<Client> Get()
{
return new List<Client>();
}
}
ClientsWeb.cs
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.using System.Collections.Generic;
using System.Collections.Generic;
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
namespace IdentityServerHost.Configuration;
public static class ClientsWeb
{
static string[] allowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"resource1.scope1",
"resource2.scope1",
"transaction",
"custom.profile"
};
public static IEnumerable<Client> Get()
{
return new List<Client>();
}
}
Resources.cs
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Collections.Generic;
using IdentityModel;
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
namespace IdentityServerHost.Configuration;
public static class Resources
{
// identity resources represent identity data about a user that can be requested via the scope parameter (OpenID Connect)
public static readonly IEnumerable<IdentityResource> IdentityResources =
new[]
{
// some standard scopes from the OIDC spec
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
// custom identity resource with some consolidated claims
new IdentityResource("custom.profile", new[] { JwtClaimTypes.Name, JwtClaimTypes.Email, "location", JwtClaimTypes.Address })
};
}
Now we will modify the Identity Server Extension, this will read the configuration from the appsettings file and some settings for the key management.
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Security.Cryptography.X509Certificates;
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Stores.Default;
using Duende.IdentityServer.Test;
using IdentityModel;
using IdentityServerHost.Configuration;
using IdentityServerHost.Extensions;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Tokens;
namespace IdentityServerHost;
internal static class IdentityServerExtensions
{
internal static WebApplicationBuilder ConfigureIdentityServer(this WebApplicationBuilder builder)
{
var configuration = builder.Configuration;
var testUsers = builder.Configuration.GetSection("Users").Get<List<TestUser>>();
var identityServer = builder.Services.AddIdentityServer(options =>
{
options.Events.RaiseSuccessEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.EmitScopesAsSpaceDelimitedStringInJwt = true;
options.Endpoints.EnableJwtRequestUri = true;
options.ServerSideSessions.UserDisplayNameClaimType = JwtClaimTypes.Name;
options.UserInteraction.CreateAccountUrl = "/Account/Create";
options.Endpoints.EnablePushedAuthorizationEndpoint = true;
options.PushedAuthorization.AllowUnregisteredPushedRedirectUris = true;
options.KeyManagement.RotationInterval = TimeSpan.FromDays(30);
options.KeyManagement.PropagationTime = TimeSpan.FromDays(2);
options.KeyManagement.RetentionDuration = TimeSpan.FromDays(7);
options.KeyManagement.DeleteRetiredKeys = true;
})
.AddInMemoryClients(configuration.GetSection("Clients"))
.AddInMemoryIdentityResources(Resources.IdentityResources)
.AddInMemoryApiScopes(configuration.GetSection("ApiScopes"))
.AddInMemoryApiResources(configuration.GetSection("ApiResources"))
.AddExtensionGrantValidator<Extensions.ExtensionGrantValidator>()
.AddExtensionGrantValidator<Extensions.NoSubjectExtensionGrantValidator>()
.AddJwtBearerClientAuthentication()
.AddAppAuthRedirectUriValidator()
.AddTestUsers(testUsers)
.AddProfileService<HostProfileService>()
.AddCustomTokenRequestValidator<ParameterizedScopeTokenRequestValidator>()
.AddScopeParser<ParameterizedScopeParser>()
.AddMutualTlsSecretValidators()
.AddInMemoryOidcProviders(new[]
{
new Duende.IdentityServer.Models.OidcProvider
{
Scheme = "dynamicprovider-idsvr",
DisplayName = "IdentityServer (via Dynamic Providers)",
Authority = "https://demo.duendesoftware.com",
ClientId = "login",
ResponseType = "id_token",
Scope = "openid profile"
}
});
builder.Services.AddDistributedMemoryCache();
builder.Services.AddIdentityServerConfiguration(opt =>
{
}).AddInMemoryClientConfigurationStore();
return builder;
}
}
Finally, we will need to modify the program.cs to include the additional appsettings file, environment variables and reverse proxy handling.
Program.cs
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using IdentityServerHost;
using Microsoft.AspNetCore.HttpOverrides;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
using System.Globalization;
Console.Title = "IdentityServer (Main)";
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(formatProvider: CultureInfo.InvariantCulture)
.CreateBootstrapLogger();
Log.Information("Host.Main Starting up");
try
{
var builder = WebApplication.CreateBuilder(args);
//read external configuration if from a container, i use the -v parameter to map the external configuration directory to /app/settings
if (builder.Configuration.GetSection("IsContainerExternalConfiguration").Get<bool>())
{
var configurationPath = builder.Configuration.GetSection("ContainerExternalConfiguration").Get<string>() ?? throw new InvalidOperationException("Optional configuration does not exists");
builder.Configuration.AddJsonFile(configurationPath, false);
builder.Configuration.AddEnvironmentVariables();
}
builder.Host.UseSerilog((ctx, lc) => lc
.WriteTo.Console(
outputTemplate:
"[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}",
theme: AnsiConsoleTheme.Code,
formatProvider: CultureInfo.InvariantCulture)
.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());
var app = builder
.ConfigureServices();
if (builder.Configuration.GetSection("IsUseReverseProxy").Get<bool>())
{
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
var rootUri = new Uri(builder.Configuration.GetSection("ProxyBasePath").Get<string>());
var path = rootUri.AbsolutePath;
//Deal with path base and proxies that change the request path
if (path != "/")
{
app.Use((context, next) =>
{
context.Request.PathBase = new PathString(path);
return next.Invoke();
});
}
}
app.ConfigurePipeline();
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Unhandled exception");
}
finally
{
Log.Information("Shut down complete");
Log.CloseAndFlush();
}
If you execute the project, you will have the new configuration taking place.
However, one setting is not correct, the certificate value on the appsettings.
{
"ClientId": "myclient",
"ClientSecrets": [
{
"Type": "X509CertificateBase64",
"Value": "MIIDA...V"
}
],
"AllowedGrantTypes": [ "client_credentials" ],
"AllowedScopes": [ "api.readonly" ]
}
The value must be a valid X509 public base 64 string.
You can create your own certificate and extract it from a previous post I have
https://darthseldon.net/certificate-reader/

Copy the Publick Key Base 64 value and paste it. Thats it!
You have Duende running locally! Let us deploy it to Azure!
We will be using the free instances of Azure.
Azure Deployment
On the portal create a resource and create a web app



Make sure you select the Linux plan as Free F1.
Once it finishes creating the resource, you can browse to it

Now we will deploy from Visual studio
Right click the solution and select publish

New profile

Select Azure, Azure App Service (Linux) and your created web app




Make sure it is a self contained app and linux-x64 setting and publish



And you have it running in Azure!!!!

Docker and NGNIX
Now here is where it gets a little bit complicated, in Azure you can change the appsettings file from the portal or you can redeploy, easy; locally, easy change it from an editor, but what happens in docker?
The settings are inside the image, but we want to add or remove clients by not redeploying. Enter the external configuration setting.
First, split the configuration in two files, one for behavior, and the other for the clients.
appsettings.json
{
"IsContainerExternalConfiguration": true,
"ContainerExternalConfiguration": "/appsettings/appsettings.external.duende.json",
"IsUseReverseProxy": false,
"ProxyBasePath": "/yourvirtualdirectory"
}
appsettings.external.duende.json
{
"ApiScopes": [
{
"Name": "api.readonly"
}
],
"ApiResources": [
{
"Name": "CustomResource",
"DisplayName": "Test Resource",
"Scopes": [
"api.readonly"
]
}
],
"Clients": [
{
"ClientId": "myclient",
"ClientSecrets": [
{
"Type": "X509CertificateBase64",
"Value": "MIIDA...V"
}
],
"AllowedGrantTypes": [ "client_credentials" ],
"AllowedScopes": [ "api.readonly" ]
}
],
"Users": [
{
"SubjectId": "1",
"Username": "myuser",
"Password": "horseWalkingBeachMouse4573@#%",
"Name": "Darth Seldon",
"GivenName": "Darth",
"FamilyName": "Seldon",
"Email": "someemail@server.com",
"EmailVerified": "true",
"WebSite": "https://darthseldon.net"
}
]
}
Now you see why I added those settings, set IsContainerExternalConfiguration to true.
Now we will add a flag to be used to docker.
Next to the Container (Dockerfile) execute icon, there is a dropdown
Select Host.Main Debug Properties

And the following flag
-v D:\configurations\duende:/app/settings/
We are mounting external storage from the d drive to a virtual inside the container
Thats it! you can execute it locally with docker desktop and should work.
We will be creating the image next.
You will need to delete the keys folder that was creating while testing locally from the project. These keys are created and reused, however if you use it on the image, it will break, so don't forget, delete this directory.
You can publish your project to docker hub.
I already have an image that you can use
docker pull jtenorio/dseldonduendemain
To start a container you can use the following command
docker run -d -e ASPNETCORE_URLS=http://*:3080 -p:3080:3080 -e ASPNETCORE_HTTP_PORTS:3080 -e IsContainerExternalConfiguration=true -e ContainerExternalConfiguration=/appsettings/appsettings.external.duende.json -e IsUseReverseProxy=false -v ~/appsettings:/appsettings/ jtenorio/dseldonduendemain
A quick rundown
ASPNETCORE_URLS=http://*:3080 -p:3080:3080 will startup the server at port 3080
-e IsContainerExternalConfiguration=true -e ContainerExternalConfiguration=/appsettings/appsettings.external.duende.json -e IsUseReverseProxy=false will override the settings to read the configuration from the external file and stating this server is not in a reverse proxy path.
-v ~/appsettings:/appsettings/ jtenorio/dseldonduendemain finally we mount the storage to the virtual directory and use the pulled image.
Great, we have it running in a linux machine with docker.
However, the majority of times you will have it behind a reverse proxy such as NGINX or YARP.
I will provide the sample for nginx
location /yourids/ {
proxy_pass http://127.0.0.1:3080/;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 90;
proxy_connect_timeout 90;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_upgrade;
proxy_set_header X-Fowarded-Proto https;
proxy_set_header X-Fowarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Fowarded-Port 443;
proxy_set_header Proxy "";
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
large_client_header_buffers 4 16k;
sub_filter '<base href="/" />' '<base href="/yourids/" />';
}
Pretty straight forward, you are just adding a virtual directoy mapped to the docker container url and using fowarded headers.
docker run -d -e ASPNETCORE_URLS=http://*:3080 -p:3080:3080 -e ASPNETCORE_HTTP_PORTS:3080 -e IsContainerExternalConfiguration=true -e ContainerExternalConfiguration=/appsettings/appsettings.external.duende.json -e IsUseReverseProxy=true -e ProxyBasePath=/yourids -e ASPNETCORE_FORWARDEDHEADERS_ENABLED=1 -v ~/appsettings:/appsettings/ jtenorio/dseldonduendemain
As for the command for docker, note that we are setting the IsUseReverseProxy to true and the proxybasepath as well the aspnetcore_fowardedheaders_enabled.
The last thing is to reload the nginx configuration and reload the paths
nginx -t
nginx -s reload
Long post, but you can skip the majority by just using the image that I am providing and using docker.
Next time Postman and signed requests.
Happy coding!!!