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": "*" +}