diff --git a/Scenarios/Authentication/Service-to-service-JWT/Client/Client.csproj b/Scenarios/Authentication/Service-to-service-JWT/Client/Client.csproj
new file mode 100644
index 0000000..7034dec
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Client/Client.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Scenarios/Authentication/Service-to-service-JWT/Client/Connected Services/ServiceReference1/ConnectedService.json b/Scenarios/Authentication/Service-to-service-JWT/Client/Connected Services/ServiceReference1/ConnectedService.json
new file mode 100644
index 0000000..47229be
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Client/Connected Services/ServiceReference1/ConnectedService.json
@@ -0,0 +1,47 @@
+{
+ "ExtendedData": {
+ "inputs": [
+ "https://localhost:7173/Service.svc?wsdl"
+ ],
+ "collectionTypes": [
+ "System.Array",
+ "System.Collections.Generic.Dictionary`2"
+ ],
+ "namespaceMappings": [
+ "*, ServiceReference1"
+ ],
+ "references": [
+ "IdentityModel, {IdentityModel, 6.0.0}",
+ "Microsoft.Bcl.AsyncInterfaces, {Microsoft.Bcl.AsyncInterfaces, 5.0.0}",
+ "Microsoft.Extensions.ObjectPool, {Microsoft.Extensions.ObjectPool, 5.0.10}",
+ "Microsoft.IdentityModel.Logging, {Microsoft.IdentityModel.Logging, 6.8.0}",
+ "Microsoft.IdentityModel.Protocols.WsTrust, {Microsoft.IdentityModel.Protocols.WsTrust, 6.8.0}",
+ "Microsoft.IdentityModel.Tokens, {Microsoft.IdentityModel.Tokens, 6.8.0}",
+ "Microsoft.IdentityModel.Tokens.Saml, {Microsoft.IdentityModel.Tokens.Saml, 6.8.0}",
+ "Microsoft.IdentityModel.Xml, {Microsoft.IdentityModel.Xml, 6.8.0}",
+ "System.Drawing.Common, {System.Drawing.Common, 5.0.0}",
+ "System.IO, {System.IO, 4.3.0}",
+ "System.Reflection.DispatchProxy, {System.Reflection.DispatchProxy, 4.7.1}",
+ "System.Runtime, {System.Runtime, 4.3.0}",
+ "System.Security.AccessControl, {System.Security.AccessControl, 5.0.0}",
+ "System.Security.Cryptography.Cng, {System.Security.Cryptography.Cng, 5.0.0}",
+ "System.Security.Cryptography.Xml, {System.Security.Cryptography.Xml, 5.0.0}",
+ "System.Security.Permissions, {System.Security.Permissions, 5.0.0}",
+ "System.Security.Principal.Windows, {System.Security.Principal.Windows, 5.0.0}",
+ "System.ServiceModel, {System.ServiceModel.Primitives, 4.10.0}",
+ "System.ServiceModel.Duplex, {System.ServiceModel.Duplex, 4.10.0}",
+ "System.ServiceModel.Federation, {System.ServiceModel.Federation, 4.10.0}",
+ "System.ServiceModel.Http, {System.ServiceModel.Http, 4.10.0}",
+ "System.ServiceModel.NetTcp, {System.ServiceModel.NetTcp, 4.10.0}",
+ "System.ServiceModel.Primitives, {System.ServiceModel.Primitives, 4.10.0}",
+ "System.ServiceModel.Security, {System.ServiceModel.Security, 4.10.0}",
+ "System.Text.Encoding, {System.Text.Encoding, 4.3.0}",
+ "System.Threading.Tasks, {System.Threading.Tasks, 4.3.0}",
+ "System.Windows.Extensions, {System.Windows.Extensions, 5.0.0}",
+ "System.Xml.ReaderWriter, {System.Xml.ReaderWriter, 4.3.0}",
+ "System.Xml.XmlDocument, {System.Xml.XmlDocument, 4.3.0}"
+ ],
+ "targetFramework": "net6.0",
+ "typeReuseMode": "All"
+ }
+}
\ No newline at end of file
diff --git a/Scenarios/Authentication/Service-to-service-JWT/Client/Connected Services/ServiceReference1/Reference.cs b/Scenarios/Authentication/Service-to-service-JWT/Client/Connected Services/ServiceReference1/Reference.cs
new file mode 100644
index 0000000..3eaa3ba
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Client/Connected Services/ServiceReference1/Reference.cs
@@ -0,0 +1,123 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace ServiceReference1
+{
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")]
+ [System.ServiceModel.ServiceContractAttribute(ConfigurationName="ServiceReference1.ISecuredService")]
+ public interface ISecuredService
+ {
+
+ [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ISecuredService/Echo", ReplyAction="http://tempuri.org/ISecuredService/EchoResponse")]
+ System.Threading.Tasks.Task EchoAsync(string value);
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")]
+ public interface ISecuredServiceChannel : ServiceReference1.ISecuredService, System.ServiceModel.IClientChannel
+ {
+ }
+
+ [System.Diagnostics.DebuggerStepThroughAttribute()]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")]
+ public partial class SecuredServiceClient : System.ServiceModel.ClientBase, ServiceReference1.ISecuredService
+ {
+
+ ///
+ /// Implement this partial method to configure the service endpoint.
+ ///
+ /// The endpoint to configure
+ /// The client credentials
+ static partial void ConfigureEndpoint(System.ServiceModel.Description.ServiceEndpoint serviceEndpoint, System.ServiceModel.Description.ClientCredentials clientCredentials);
+
+ public SecuredServiceClient() :
+ base(SecuredServiceClient.GetDefaultBinding(), SecuredServiceClient.GetDefaultEndpointAddress())
+ {
+ this.Endpoint.Name = EndpointConfiguration.BasicHttpBinding_ISecuredService.ToString();
+ ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
+ }
+
+ public SecuredServiceClient(EndpointConfiguration endpointConfiguration) :
+ base(SecuredServiceClient.GetBindingForEndpoint(endpointConfiguration), SecuredServiceClient.GetEndpointAddress(endpointConfiguration))
+ {
+ this.Endpoint.Name = endpointConfiguration.ToString();
+ ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
+ }
+
+ public SecuredServiceClient(EndpointConfiguration endpointConfiguration, string remoteAddress) :
+ base(SecuredServiceClient.GetBindingForEndpoint(endpointConfiguration), new System.ServiceModel.EndpointAddress(remoteAddress))
+ {
+ this.Endpoint.Name = endpointConfiguration.ToString();
+ ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
+ }
+
+ public SecuredServiceClient(EndpointConfiguration endpointConfiguration, System.ServiceModel.EndpointAddress remoteAddress) :
+ base(SecuredServiceClient.GetBindingForEndpoint(endpointConfiguration), remoteAddress)
+ {
+ this.Endpoint.Name = endpointConfiguration.ToString();
+ ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
+ }
+
+ public SecuredServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
+ base(binding, remoteAddress)
+ {
+ }
+
+ public System.Threading.Tasks.Task EchoAsync(string value)
+ {
+ return base.Channel.EchoAsync(value);
+ }
+
+ public virtual System.Threading.Tasks.Task OpenAsync()
+ {
+ return System.Threading.Tasks.Task.Factory.FromAsync(((System.ServiceModel.ICommunicationObject)(this)).BeginOpen(null, null), new System.Action(((System.ServiceModel.ICommunicationObject)(this)).EndOpen));
+ }
+
+ private static System.ServiceModel.Channels.Binding GetBindingForEndpoint(EndpointConfiguration endpointConfiguration)
+ {
+ if ((endpointConfiguration == EndpointConfiguration.BasicHttpBinding_ISecuredService))
+ {
+ System.ServiceModel.BasicHttpBinding result = new System.ServiceModel.BasicHttpBinding();
+ result.MaxBufferSize = int.MaxValue;
+ result.ReaderQuotas = System.Xml.XmlDictionaryReaderQuotas.Max;
+ result.MaxReceivedMessageSize = int.MaxValue;
+ result.AllowCookies = true;
+ result.Security.Mode = System.ServiceModel.BasicHttpSecurityMode.Transport;
+ return result;
+ }
+ throw new System.InvalidOperationException(string.Format("Could not find endpoint with name \'{0}\'.", endpointConfiguration));
+ }
+
+ private static System.ServiceModel.EndpointAddress GetEndpointAddress(EndpointConfiguration endpointConfiguration)
+ {
+ if ((endpointConfiguration == EndpointConfiguration.BasicHttpBinding_ISecuredService))
+ {
+ return new System.ServiceModel.EndpointAddress("https://localhost:7173/Service.svc");
+ }
+ throw new System.InvalidOperationException(string.Format("Could not find endpoint with name \'{0}\'.", endpointConfiguration));
+ }
+
+ private static System.ServiceModel.Channels.Binding GetDefaultBinding()
+ {
+ return SecuredServiceClient.GetBindingForEndpoint(EndpointConfiguration.BasicHttpBinding_ISecuredService);
+ }
+
+ private static System.ServiceModel.EndpointAddress GetDefaultEndpointAddress()
+ {
+ return SecuredServiceClient.GetEndpointAddress(EndpointConfiguration.BasicHttpBinding_ISecuredService);
+ }
+
+ public enum EndpointConfiguration
+ {
+
+ BasicHttpBinding_ISecuredService,
+ }
+ }
+}
diff --git a/Scenarios/Authentication/Service-to-service-JWT/Client/Program.cs b/Scenarios/Authentication/Service-to-service-JWT/Client/Program.cs
new file mode 100644
index 0000000..e222f2b
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Client/Program.cs
@@ -0,0 +1,32 @@
+// See https://aka.ms/new-console-template for more information
+
+using System.Net;
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+using IdentityModel.Client;
+using ServiceReference1;
+
+using HttpClient httpClient = new HttpClient();
+var discoveryDocumentResponse = await httpClient.GetDiscoveryDocumentAsync("https://demo.duendesoftware.com/.well-known/openid-configuration");
+var tokenResponse = await httpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
+{
+ Address = discoveryDocumentResponse.TokenEndpoint,
+ ClientId = "m2m",
+ ClientSecret = "secret",
+ Scope = "api"
+});
+
+var channelFactory = new ChannelFactory(new BasicHttpBinding(BasicHttpSecurityMode.Transport),
+ new EndpointAddress("https://localhost:7173/Service.svc"));
+var channel = channelFactory.CreateChannel();
+
+var httpRequestProperty = new HttpRequestMessageProperty();
+httpRequestProperty.Headers[HttpRequestHeader.Authorization] = $"Bearer {tokenResponse.AccessToken}";
+var context = new OperationContext(channel);
+using var operationContextScope = new OperationContextScope(context);
+context.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
+var response = channel.EchoAsync("Hello world");
+
+Console.WriteLine(response);
+Console.ReadKey();
+
diff --git a/Scenarios/Authentication/Service-to-service-JWT/README.md b/Scenarios/Authentication/Service-to-service-JWT/README.md
new file mode 100644
index 0000000..8bebe48
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/README.md
@@ -0,0 +1,14 @@
+## Service-toService-JWT
+
+This sample shows a minimal machine to machine authentication setup using JWT. This authentication is known as [OAuth2.0 client_credentials](https://www.rfc-editor.org/rfc/rfc6749#section-1.3.4) flow. The identity provider is [the demo instance of Duende IdentityServer](https://demo.duendesoftware.com) which provides configured OAuth2.0 clients.
+
+### Service
+
+`Service` is configured to accept requests authenticated with a valid bearer `access_token` issued by the https://demo.duendesoftware.com identity provider with audience and scope valued to 'api'. The authentication is performed by the standard JwtBearer AuthenticationHandler shipped with ASP.NET Core in `Microsoft.AspNetCore.Authentication.JwtBearer` nuget package.
+
+### Client
+
+`Client` requests an `access_token` with the scope 'api' to the identity provider using its `client_id` and `client_secret`, then it calls the `Service` [passing its access_token in http headers](https://www.rfc-editor.org/rfc/rfc6749#section-7.1).
+```
+Authorization: Bearer
+```
\ No newline at end of file
diff --git a/Scenarios/Authentication/Service-to-service-JWT/Service-to-service-JWT.sln b/Scenarios/Authentication/Service-to-service-JWT/Service-to-service-JWT.sln
new file mode 100644
index 0000000..a9fc6f0
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Service-to-service-JWT.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service.csproj", "{15310845-1437-45AB-BCEE-4FAA9A3D3A08}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{4BFBEC43-A1B1-4858-9AB1-4A039ABA3098}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {15310845-1437-45AB-BCEE-4FAA9A3D3A08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {15310845-1437-45AB-BCEE-4FAA9A3D3A08}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {15310845-1437-45AB-BCEE-4FAA9A3D3A08}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {15310845-1437-45AB-BCEE-4FAA9A3D3A08}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4BFBEC43-A1B1-4858-9AB1-4A039ABA3098}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4BFBEC43-A1B1-4858-9AB1-4A039ABA3098}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4BFBEC43-A1B1-4858-9AB1-4A039ABA3098}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4BFBEC43-A1B1-4858-9AB1-4A039ABA3098}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/Scenarios/Authentication/Service-to-service-JWT/Service/ISecuredService.cs b/Scenarios/Authentication/Service-to-service-JWT/Service/ISecuredService.cs
new file mode 100644
index 0000000..16f92ac
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Service/ISecuredService.cs
@@ -0,0 +1,9 @@
+namespace Service;
+
+[ServiceContract]
+public interface ISecuredService
+{
+ [OperationContract]
+ string Echo(string value);
+
+}
\ No newline at end of file
diff --git a/Scenarios/Authentication/Service-to-service-JWT/Service/Program.cs b/Scenarios/Authentication/Service-to-service-JWT/Service/Program.cs
new file mode 100644
index 0000000..aa0b20a
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Service/Program.cs
@@ -0,0 +1,45 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Authorization;
+
+var builder = WebApplication.CreateBuilder();
+
+builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
+ {
+ options.Authority = "https://demo.duendesoftware.com";
+ options.Audience = "api";
+ });
+builder.Services.AddAuthorization(options =>
+{
+ options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
+ .RequireAuthenticatedUser()
+ .RequireClaim("scope", "api")
+ .Build();
+});
+builder.Services.AddTransient();
+builder.Services.AddHttpContextAccessor();
+builder.Services.AddServiceModelServices();
+builder.Services.AddServiceModelMetadata();
+builder.Services.AddSingleton();
+
+var app = builder.Build();
+
+app.UseServiceModel(serviceBuilder =>
+{
+ serviceBuilder.AddService();
+ serviceBuilder.AddServiceEndpoint(new BasicHttpBinding
+ {
+ Security = new BasicHttpSecurity
+ {
+ Mode = BasicHttpSecurityMode.Transport,
+ Transport = new HttpTransportSecurity
+ {
+ ClientCredentialType = HttpClientCredentialType.InheritedFromHost
+ }
+ }
+ }, "/Service.svc");
+ var serviceMetadataBehavior = app.Services.GetRequiredService();
+ serviceMetadataBehavior.HttpsGetEnabled = true;
+});
+
+app.Run();
diff --git a/Scenarios/Authentication/Service-to-service-JWT/Service/Properties/launchSettings.json b/Scenarios/Authentication/Service-to-service-JWT/Service/Properties/launchSettings.json
new file mode 100644
index 0000000..3c59c8d
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Service/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "profiles": {
+ "CoreWCFService": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:7173;http://localhost:5283"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scenarios/Authentication/Service-to-service-JWT/Service/SecuredService.cs b/Scenarios/Authentication/Service-to-service-JWT/Service/SecuredService.cs
new file mode 100644
index 0000000..ee5912e
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Service/SecuredService.cs
@@ -0,0 +1,20 @@
+using CoreWCF;
+using System;
+using System.Runtime.Serialization;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Service
+{
+ [Authorize]
+ public partial class SecuredService: ISecuredService
+ {
+ public string Echo(string value, [FromServices] IHttpContextAccessor httpContextAccessor, [FromServices] ILogger logger)
+ {
+ var principal = httpContextAccessor.HttpContext!.User;
+ logger.LogInformation("Principal has claims: {claims}", string.Join(", ", principal.Claims.Select(x => $"'{x.Type}'='{x.Value}'")));
+ return value;
+ }
+ }
+}
diff --git a/Scenarios/Authentication/Service-to-service-JWT/Service/Service.csproj b/Scenarios/Authentication/Service-to-service-JWT/Service/Service.csproj
new file mode 100644
index 0000000..3b80976
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Service/Service.csproj
@@ -0,0 +1,23 @@
+
+
+ net6.0
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Scenarios/Authentication/Service-to-service-JWT/Service/appsettings.Development.json b/Scenarios/Authentication/Service-to-service-JWT/Service/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Service/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Scenarios/Authentication/Service-to-service-JWT/Service/appsettings.json b/Scenarios/Authentication/Service-to-service-JWT/Service/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/Scenarios/Authentication/Service-to-service-JWT/Service/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}