Skip to content

Commit

Permalink
Add simple OAuth2.0 Client Credentials flow sample
Browse files Browse the repository at this point in the history
  • Loading branch information
g7ed6e committed Jan 2, 2023
1 parent 2cfd3ef commit ef58483
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="IdentityModel" Version="6.0.0" />
<PackageReference Include="System.ServiceModel.Duplex" Version="4.10.*" />
<PackageReference Include="System.ServiceModel.Federation" Version="4.10.*" />
<PackageReference Include="System.ServiceModel.Http" Version="4.10.*" />
<PackageReference Include="System.ServiceModel.NetTcp" Version="4.10.*" />
<PackageReference Include="System.ServiceModel.Security" Version="4.10.*" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

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<string> 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>, ServiceReference1.ISecuredService
{

/// <summary>
/// Implement this partial method to configure the service endpoint.
/// </summary>
/// <param name="serviceEndpoint">The endpoint to configure</param>
/// <param name="clientCredentials">The client credentials</param>
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<string> 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.IAsyncResult>(((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,
}
}
}
32 changes: 32 additions & 0 deletions Scenarios/Authentication/Service-to-service-JWT/Client/Program.cs
Original file line number Diff line number Diff line change
@@ -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<ISecuredServiceChannel>(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();

14 changes: 14 additions & 0 deletions Scenarios/Authentication/Service-to-service-JWT/README.md
Original file line number Diff line number Diff line change
@@ -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 <access_token>
```
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Service;

[ServiceContract]
public interface ISecuredService
{
[OperationContract]
string Echo(string value);

}
45 changes: 45 additions & 0 deletions Scenarios/Authentication/Service-to-service-JWT/Service/Program.cs
Original file line number Diff line number Diff line change
@@ -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<SecuredService>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddServiceModelServices();
builder.Services.AddServiceModelMetadata();
builder.Services.AddSingleton<IServiceBehavior, UseRequestHeadersForMetadataAddressBehavior>();

var app = builder.Build();

app.UseServiceModel(serviceBuilder =>
{
serviceBuilder.AddService<SecuredService>();
serviceBuilder.AddServiceEndpoint<SecuredService, ISecuredService>(new BasicHttpBinding
{
Security = new BasicHttpSecurity
{
Mode = BasicHttpSecurityMode.Transport,
Transport = new HttpTransportSecurity
{
ClientCredentialType = HttpClientCredentialType.InheritedFromHost
}
}
}, "/Service.svc");
var serviceMetadataBehavior = app.Services.GetRequiredService<ServiceMetadataBehavior>();
serviceMetadataBehavior.HttpsGetEnabled = true;
});

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"profiles": {
"CoreWCFService": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:7173;http://localhost:5283"
}
}
}
Original file line number Diff line number Diff line change
@@ -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<SecuredService> logger)
{
var principal = httpContextAccessor.HttpContext!.User;
logger.LogInformation("Principal has claims: {claims}", string.Join(", ", principal.Claims.Select(x => $"'{x.Type}'='{x.Value}'")));
return value;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Using Include="CoreWCF" />
<Using Include="CoreWCF.Configuration" />
<Using Include="CoreWCF.Channels" />
<Using Include="CoreWCF.Description" />
<Using Include="System.Runtime.Serialization " />
<Using Include="Service" />
<Using Include="Microsoft.Extensions.DependencyInjection.Extensions" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CoreWCF.Primitives" Version="1.3.1" />
<PackageReference Include="CoreWCF.Http" Version="1.3.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.12" />
<!-- https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1792#issuecomment-993393946 -->
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="6.25.1" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

0 comments on commit ef58483

Please sign in to comment.