Skip to content

Commit 1393b1a

Browse files
committed
Add simple OAuth2.0 Client Credentials flow sample
1 parent 2cfd3ef commit 1393b1a

File tree

13 files changed

+382
-0
lines changed

13 files changed

+382
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net6.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="IdentityModel" Version="6.0.0" />
12+
<PackageReference Include="System.ServiceModel.Duplex" Version="4.10.*" />
13+
<PackageReference Include="System.ServiceModel.Federation" Version="4.10.*" />
14+
<PackageReference Include="System.ServiceModel.Http" Version="4.10.*" />
15+
<PackageReference Include="System.ServiceModel.NetTcp" Version="4.10.*" />
16+
<PackageReference Include="System.ServiceModel.Security" Version="4.10.*" />
17+
</ItemGroup>
18+
19+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"ExtendedData": {
3+
"inputs": [
4+
"https://localhost:7173/Service.svc?wsdl"
5+
],
6+
"collectionTypes": [
7+
"System.Array",
8+
"System.Collections.Generic.Dictionary`2"
9+
],
10+
"namespaceMappings": [
11+
"*, ServiceReference1"
12+
],
13+
"references": [
14+
"IdentityModel, {IdentityModel, 6.0.0}",
15+
"Microsoft.Bcl.AsyncInterfaces, {Microsoft.Bcl.AsyncInterfaces, 5.0.0}",
16+
"Microsoft.Extensions.ObjectPool, {Microsoft.Extensions.ObjectPool, 5.0.10}",
17+
"Microsoft.IdentityModel.Logging, {Microsoft.IdentityModel.Logging, 6.8.0}",
18+
"Microsoft.IdentityModel.Protocols.WsTrust, {Microsoft.IdentityModel.Protocols.WsTrust, 6.8.0}",
19+
"Microsoft.IdentityModel.Tokens, {Microsoft.IdentityModel.Tokens, 6.8.0}",
20+
"Microsoft.IdentityModel.Tokens.Saml, {Microsoft.IdentityModel.Tokens.Saml, 6.8.0}",
21+
"Microsoft.IdentityModel.Xml, {Microsoft.IdentityModel.Xml, 6.8.0}",
22+
"System.Drawing.Common, {System.Drawing.Common, 5.0.0}",
23+
"System.IO, {System.IO, 4.3.0}",
24+
"System.Reflection.DispatchProxy, {System.Reflection.DispatchProxy, 4.7.1}",
25+
"System.Runtime, {System.Runtime, 4.3.0}",
26+
"System.Security.AccessControl, {System.Security.AccessControl, 5.0.0}",
27+
"System.Security.Cryptography.Cng, {System.Security.Cryptography.Cng, 5.0.0}",
28+
"System.Security.Cryptography.Xml, {System.Security.Cryptography.Xml, 5.0.0}",
29+
"System.Security.Permissions, {System.Security.Permissions, 5.0.0}",
30+
"System.Security.Principal.Windows, {System.Security.Principal.Windows, 5.0.0}",
31+
"System.ServiceModel, {System.ServiceModel.Primitives, 4.10.0}",
32+
"System.ServiceModel.Duplex, {System.ServiceModel.Duplex, 4.10.0}",
33+
"System.ServiceModel.Federation, {System.ServiceModel.Federation, 4.10.0}",
34+
"System.ServiceModel.Http, {System.ServiceModel.Http, 4.10.0}",
35+
"System.ServiceModel.NetTcp, {System.ServiceModel.NetTcp, 4.10.0}",
36+
"System.ServiceModel.Primitives, {System.ServiceModel.Primitives, 4.10.0}",
37+
"System.ServiceModel.Security, {System.ServiceModel.Security, 4.10.0}",
38+
"System.Text.Encoding, {System.Text.Encoding, 4.3.0}",
39+
"System.Threading.Tasks, {System.Threading.Tasks, 4.3.0}",
40+
"System.Windows.Extensions, {System.Windows.Extensions, 5.0.0}",
41+
"System.Xml.ReaderWriter, {System.Xml.ReaderWriter, 4.3.0}",
42+
"System.Xml.XmlDocument, {System.Xml.XmlDocument, 4.3.0}"
43+
],
44+
"targetFramework": "net6.0",
45+
"typeReuseMode": "All"
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by a tool.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
namespace ServiceReference1
11+
{
12+
13+
14+
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")]
15+
[System.ServiceModel.ServiceContractAttribute(ConfigurationName="ServiceReference1.ISecuredService")]
16+
public interface ISecuredService
17+
{
18+
19+
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ISecuredService/Echo", ReplyAction="http://tempuri.org/ISecuredService/EchoResponse")]
20+
System.Threading.Tasks.Task<string> EchoAsync(string value);
21+
}
22+
23+
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")]
24+
public interface ISecuredServiceChannel : ServiceReference1.ISecuredService, System.ServiceModel.IClientChannel
25+
{
26+
}
27+
28+
[System.Diagnostics.DebuggerStepThroughAttribute()]
29+
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")]
30+
public partial class SecuredServiceClient : System.ServiceModel.ClientBase<ServiceReference1.ISecuredService>, ServiceReference1.ISecuredService
31+
{
32+
33+
/// <summary>
34+
/// Implement this partial method to configure the service endpoint.
35+
/// </summary>
36+
/// <param name="serviceEndpoint">The endpoint to configure</param>
37+
/// <param name="clientCredentials">The client credentials</param>
38+
static partial void ConfigureEndpoint(System.ServiceModel.Description.ServiceEndpoint serviceEndpoint, System.ServiceModel.Description.ClientCredentials clientCredentials);
39+
40+
public SecuredServiceClient() :
41+
base(SecuredServiceClient.GetDefaultBinding(), SecuredServiceClient.GetDefaultEndpointAddress())
42+
{
43+
this.Endpoint.Name = EndpointConfiguration.BasicHttpBinding_ISecuredService.ToString();
44+
ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
45+
}
46+
47+
public SecuredServiceClient(EndpointConfiguration endpointConfiguration) :
48+
base(SecuredServiceClient.GetBindingForEndpoint(endpointConfiguration), SecuredServiceClient.GetEndpointAddress(endpointConfiguration))
49+
{
50+
this.Endpoint.Name = endpointConfiguration.ToString();
51+
ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
52+
}
53+
54+
public SecuredServiceClient(EndpointConfiguration endpointConfiguration, string remoteAddress) :
55+
base(SecuredServiceClient.GetBindingForEndpoint(endpointConfiguration), new System.ServiceModel.EndpointAddress(remoteAddress))
56+
{
57+
this.Endpoint.Name = endpointConfiguration.ToString();
58+
ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
59+
}
60+
61+
public SecuredServiceClient(EndpointConfiguration endpointConfiguration, System.ServiceModel.EndpointAddress remoteAddress) :
62+
base(SecuredServiceClient.GetBindingForEndpoint(endpointConfiguration), remoteAddress)
63+
{
64+
this.Endpoint.Name = endpointConfiguration.ToString();
65+
ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
66+
}
67+
68+
public SecuredServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
69+
base(binding, remoteAddress)
70+
{
71+
}
72+
73+
public System.Threading.Tasks.Task<string> EchoAsync(string value)
74+
{
75+
return base.Channel.EchoAsync(value);
76+
}
77+
78+
public virtual System.Threading.Tasks.Task OpenAsync()
79+
{
80+
return System.Threading.Tasks.Task.Factory.FromAsync(((System.ServiceModel.ICommunicationObject)(this)).BeginOpen(null, null), new System.Action<System.IAsyncResult>(((System.ServiceModel.ICommunicationObject)(this)).EndOpen));
81+
}
82+
83+
private static System.ServiceModel.Channels.Binding GetBindingForEndpoint(EndpointConfiguration endpointConfiguration)
84+
{
85+
if ((endpointConfiguration == EndpointConfiguration.BasicHttpBinding_ISecuredService))
86+
{
87+
System.ServiceModel.BasicHttpBinding result = new System.ServiceModel.BasicHttpBinding();
88+
result.MaxBufferSize = int.MaxValue;
89+
result.ReaderQuotas = System.Xml.XmlDictionaryReaderQuotas.Max;
90+
result.MaxReceivedMessageSize = int.MaxValue;
91+
result.AllowCookies = true;
92+
result.Security.Mode = System.ServiceModel.BasicHttpSecurityMode.Transport;
93+
return result;
94+
}
95+
throw new System.InvalidOperationException(string.Format("Could not find endpoint with name \'{0}\'.", endpointConfiguration));
96+
}
97+
98+
private static System.ServiceModel.EndpointAddress GetEndpointAddress(EndpointConfiguration endpointConfiguration)
99+
{
100+
if ((endpointConfiguration == EndpointConfiguration.BasicHttpBinding_ISecuredService))
101+
{
102+
return new System.ServiceModel.EndpointAddress("https://localhost:7173/Service.svc");
103+
}
104+
throw new System.InvalidOperationException(string.Format("Could not find endpoint with name \'{0}\'.", endpointConfiguration));
105+
}
106+
107+
private static System.ServiceModel.Channels.Binding GetDefaultBinding()
108+
{
109+
return SecuredServiceClient.GetBindingForEndpoint(EndpointConfiguration.BasicHttpBinding_ISecuredService);
110+
}
111+
112+
private static System.ServiceModel.EndpointAddress GetDefaultEndpointAddress()
113+
{
114+
return SecuredServiceClient.GetEndpointAddress(EndpointConfiguration.BasicHttpBinding_ISecuredService);
115+
}
116+
117+
public enum EndpointConfiguration
118+
{
119+
120+
BasicHttpBinding_ISecuredService,
121+
}
122+
}
123+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// See https://aka.ms/new-console-template for more information
2+
3+
using System.Net;
4+
using System.ServiceModel;
5+
using System.ServiceModel.Channels;
6+
using IdentityModel.Client;
7+
using ServiceReference1;
8+
9+
using HttpClient httpClient = new HttpClient();
10+
var discoveryDocumentResponse = await httpClient.GetDiscoveryDocumentAsync("https://demo.duendesoftware.com/.well-known/openid-configuration");
11+
var tokenResponse = await httpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
12+
{
13+
Address = discoveryDocumentResponse.TokenEndpoint,
14+
ClientId = "m2m",
15+
ClientSecret = "secret",
16+
Scope = "api"
17+
});
18+
19+
var channelFactory = new ChannelFactory<ISecuredServiceChannel>(new BasicHttpBinding(BasicHttpSecurityMode.Transport),
20+
new EndpointAddress("https://localhost:7173/Service.svc"));
21+
var channel = channelFactory.CreateChannel();
22+
23+
var httpRequestProperty = new HttpRequestMessageProperty();
24+
httpRequestProperty.Headers[HttpRequestHeader.Authorization] = $"Bearer {tokenResponse.AccessToken}";
25+
var context = new OperationContext(channel);
26+
using var operationContextScope = new OperationContextScope(context);
27+
context.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
28+
var response = channel.EchoAsync("Hello world");
29+
30+
Console.WriteLine(response);
31+
Console.ReadKey();
32+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service.csproj", "{15310845-1437-45AB-BCEE-4FAA9A3D3A08}"
4+
EndProject
5+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{4BFBEC43-A1B1-4858-9AB1-4A039ABA3098}"
6+
EndProject
7+
Global
8+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9+
Debug|Any CPU = Debug|Any CPU
10+
Release|Any CPU = Release|Any CPU
11+
EndGlobalSection
12+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
13+
{15310845-1437-45AB-BCEE-4FAA9A3D3A08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14+
{15310845-1437-45AB-BCEE-4FAA9A3D3A08}.Debug|Any CPU.Build.0 = Debug|Any CPU
15+
{15310845-1437-45AB-BCEE-4FAA9A3D3A08}.Release|Any CPU.ActiveCfg = Release|Any CPU
16+
{15310845-1437-45AB-BCEE-4FAA9A3D3A08}.Release|Any CPU.Build.0 = Release|Any CPU
17+
{4BFBEC43-A1B1-4858-9AB1-4A039ABA3098}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18+
{4BFBEC43-A1B1-4858-9AB1-4A039ABA3098}.Debug|Any CPU.Build.0 = Debug|Any CPU
19+
{4BFBEC43-A1B1-4858-9AB1-4A039ABA3098}.Release|Any CPU.ActiveCfg = Release|Any CPU
20+
{4BFBEC43-A1B1-4858-9AB1-4A039ABA3098}.Release|Any CPU.Build.0 = Release|Any CPU
21+
EndGlobalSection
22+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## Minimal-machine-to-machine-using-JWT
2+
3+
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.
4+
5+
### Service
6+
7+
`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.
8+
9+
### Client
10+
11+
`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).
12+
```
13+
Authorization: Bearer <access_token>
14+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Service;
2+
3+
[ServiceContract]
4+
public interface ISecuredService
5+
{
6+
[OperationContract]
7+
string Echo(string value);
8+
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using Microsoft.AspNetCore.Authentication.JwtBearer;
2+
using Microsoft.AspNetCore.Authorization;
3+
4+
var builder = WebApplication.CreateBuilder();
5+
6+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
7+
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
8+
{
9+
options.Authority = "https://demo.duendesoftware.com";
10+
options.Audience = "api";
11+
});
12+
builder.Services.AddAuthorization(options =>
13+
{
14+
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
15+
.RequireAuthenticatedUser()
16+
.RequireClaim("scope", "api")
17+
.Build();
18+
});
19+
builder.Services.AddTransient<SecuredService>();
20+
builder.Services.AddHttpContextAccessor();
21+
builder.Services.AddServiceModelServices();
22+
builder.Services.AddServiceModelMetadata();
23+
builder.Services.AddSingleton<IServiceBehavior, UseRequestHeadersForMetadataAddressBehavior>();
24+
25+
var app = builder.Build();
26+
27+
app.UseServiceModel(serviceBuilder =>
28+
{
29+
serviceBuilder.AddService<SecuredService>();
30+
serviceBuilder.AddServiceEndpoint<SecuredService, ISecuredService>(new BasicHttpBinding
31+
{
32+
Security = new BasicHttpSecurity
33+
{
34+
Mode = BasicHttpSecurityMode.Transport,
35+
Transport = new HttpTransportSecurity
36+
{
37+
ClientCredentialType = HttpClientCredentialType.InheritedFromHost
38+
}
39+
}
40+
}, "/Service.svc");
41+
var serviceMetadataBehavior = app.Services.GetRequiredService<ServiceMetadataBehavior>();
42+
serviceMetadataBehavior.HttpsGetEnabled = true;
43+
});
44+
45+
app.Run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"profiles": {
3+
"CoreWCFService": {
4+
"commandName": "Project",
5+
"environmentVariables": {
6+
"ASPNETCORE_ENVIRONMENT": "Development"
7+
},
8+
"applicationUrl": "https://localhost:7173;http://localhost:5283"
9+
}
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using CoreWCF;
2+
using System;
3+
using System.Runtime.Serialization;
4+
using System.Security.Claims;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.AspNetCore.Mvc;
7+
8+
namespace Service
9+
{
10+
[Authorize]
11+
public partial class SecuredService: ISecuredService
12+
{
13+
public string Echo(string value, [FromServices] IHttpContextAccessor httpContextAccessor, [FromServices] ILogger<SecuredService> logger)
14+
{
15+
var principal = httpContextAccessor.HttpContext!.User;
16+
logger.LogInformation("Principal has claims: {claims}", string.Join(", ", principal.Claims.Select(x => $"'{x.Type}'='{x.Value}'")));
17+
return value;
18+
}
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
<PropertyGroup>
3+
<TargetFramework>net6.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<ImplicitUsings>true</ImplicitUsings>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<Using Include="CoreWCF" />
9+
<Using Include="CoreWCF.Configuration" />
10+
<Using Include="CoreWCF.Channels" />
11+
<Using Include="CoreWCF.Description" />
12+
<Using Include="System.Runtime.Serialization " />
13+
<Using Include="Service" />
14+
<Using Include="Microsoft.Extensions.DependencyInjection.Extensions" />
15+
</ItemGroup>
16+
<ItemGroup>
17+
<PackageReference Include="CoreWCF.Primitives" Version="1.3.1" />
18+
<PackageReference Include="CoreWCF.Http" Version="1.3.1" />
19+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.12" />
20+
<!-- https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1792#issuecomment-993393946 -->
21+
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="6.25.1" />
22+
</ItemGroup>
23+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*"
9+
}

0 commit comments

Comments
 (0)