diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index f6f1d68..0e96027 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -20,10 +20,16 @@ jobs:
uses: actions/setup-dotnet@v2
with:
dotnet-version: 3.1.x
+
- name: Setup .NET 6
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
+
+ - name: Setup .NET 7
+ uses: actions/setup-dotnet@v2
+ with:
+ dotnet-version: 7.0.x
- name: Restore dependencies
run: dotnet restore
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Client.csproj b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Client.csproj
new file mode 100644
index 0000000..4c79382
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Client.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Connected Services/ServiceReference1/ConnectedService.json b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Connected Services/ServiceReference1/ConnectedService.json
new file mode 100644
index 0000000..c20716e
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Connected Services/ServiceReference1/ConnectedService.json
@@ -0,0 +1,16 @@
+{
+ "ExtendedData": {
+ "inputs": [
+ "https://localhost:7233/MixedAuthService.svc"
+ ],
+ "collectionTypes": [
+ "System.Array",
+ "System.Collections.Generic.Dictionary`2"
+ ],
+ "namespaceMappings": [
+ "*, ServiceReference1"
+ ],
+ "targetFramework": "net7.0",
+ "typeReuseMode": "All"
+ }
+}
\ No newline at end of file
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Connected Services/ServiceReference1/Reference.cs b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Connected Services/ServiceReference1/Reference.cs
new file mode 100644
index 0000000..b26129c
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Connected Services/ServiceReference1/Reference.cs
@@ -0,0 +1,241 @@
+//------------------------------------------------------------------------------
+//
+// 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.IAnonymousService")]
+ public interface IAnonymousService
+ {
+
+ [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IAnonymousService/EchoAnonymous", ReplyAction="http://tempuri.org/IAnonymousService/EchoAnonymousResponse")]
+ System.Threading.Tasks.Task EchoAnonymousAsync(string value);
+ }
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")]
+ public interface IAnonymousServiceChannel : ServiceReference1.IAnonymousService, System.ServiceModel.IClientChannel
+ {
+ }
+
+ [System.Diagnostics.DebuggerStepThroughAttribute()]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")]
+ public partial class AnonymousServiceClient : System.ServiceModel.ClientBase, ServiceReference1.IAnonymousService
+ {
+
+ ///
+ /// 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 AnonymousServiceClient() :
+ base(AnonymousServiceClient.GetDefaultBinding(), AnonymousServiceClient.GetDefaultEndpointAddress())
+ {
+ this.Endpoint.Name = EndpointConfiguration.BasicHttpBinding_IAnonymousService.ToString();
+ ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
+ }
+
+ public AnonymousServiceClient(EndpointConfiguration endpointConfiguration) :
+ base(AnonymousServiceClient.GetBindingForEndpoint(endpointConfiguration), AnonymousServiceClient.GetEndpointAddress(endpointConfiguration))
+ {
+ this.Endpoint.Name = endpointConfiguration.ToString();
+ ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
+ }
+
+ public AnonymousServiceClient(EndpointConfiguration endpointConfiguration, string remoteAddress) :
+ base(AnonymousServiceClient.GetBindingForEndpoint(endpointConfiguration), new System.ServiceModel.EndpointAddress(remoteAddress))
+ {
+ this.Endpoint.Name = endpointConfiguration.ToString();
+ ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
+ }
+
+ public AnonymousServiceClient(EndpointConfiguration endpointConfiguration, System.ServiceModel.EndpointAddress remoteAddress) :
+ base(AnonymousServiceClient.GetBindingForEndpoint(endpointConfiguration), remoteAddress)
+ {
+ this.Endpoint.Name = endpointConfiguration.ToString();
+ ConfigureEndpoint(this.Endpoint, this.ClientCredentials);
+ }
+
+ public AnonymousServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
+ base(binding, remoteAddress)
+ {
+ }
+
+ public System.Threading.Tasks.Task EchoAnonymousAsync(string value)
+ {
+ return base.Channel.EchoAnonymousAsync(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_IAnonymousService))
+ {
+ 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_IAnonymousService))
+ {
+ return new System.ServiceModel.EndpointAddress("https://localhost:7233/MixedAuthService.svc/anonymous");
+ }
+ throw new System.InvalidOperationException(string.Format("Could not find endpoint with name \'{0}\'.", endpointConfiguration));
+ }
+
+ private static System.ServiceModel.Channels.Binding GetDefaultBinding()
+ {
+ return AnonymousServiceClient.GetBindingForEndpoint(EndpointConfiguration.BasicHttpBinding_IAnonymousService);
+ }
+
+ private static System.ServiceModel.EndpointAddress GetDefaultEndpointAddress()
+ {
+ return AnonymousServiceClient.GetEndpointAddress(EndpointConfiguration.BasicHttpBinding_IAnonymousService);
+ }
+
+ public enum EndpointConfiguration
+ {
+
+ BasicHttpBinding_IAnonymousService,
+ }
+ }
+
+ [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/IAnonymousService/EchoAnonymous", ReplyAction="http://tempuri.org/IAnonymousService/EchoAnonymousResponse")]
+ System.Threading.Tasks.Task EchoAnonymousAsync(string value);
+
+ [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 EchoAnonymousAsync(string value)
+ {
+ return base.Channel.EchoAnonymousAsync(value);
+ }
+
+ 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:7233/MixedAuthService.svc/authenticated");
+ }
+ 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/Mixed-auth-allow-anonymous-workaround/Client/Program.cs b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Program.cs
new file mode 100644
index 0000000..45b359a
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Program.cs
@@ -0,0 +1,61 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net;
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+using Microsoft.Extensions.DependencyInjection;
+using OpenIddict.Client;
+using ServiceReference1;
+
+// Request an access_token
+var services = new ServiceCollection();
+services.AddOpenIddict()
+ .AddClient(options =>
+ {
+ options.AllowClientCredentialsFlow();
+ options.DisableTokenStorage();
+ options.UseSystemNetHttp();
+ options.AddRegistration(new OpenIddictClientRegistration
+ {
+ Issuer = new Uri("https://localhost:7222/", UriKind.Absolute),
+ ClientId = "console_app",
+ ClientSecret = "secret",
+ Scopes = { "api" }
+ }
+ );
+ });
+var provider = services.BuildServiceProvider();
+var service = provider.GetRequiredService();
+var (tokenResponse, _) = await service.AuthenticateWithClientCredentialsAsync(new Uri("https://localhost:7222/", UriKind.Absolute), new [] { "api" });
+
+Console.WriteLine($"Retrieved access_token {tokenResponse.AccessToken}");
+
+// Create an authenticated client and call 'Echo' and 'EchoAnonymous'
+var channelFactory = new ChannelFactory(new BasicHttpBinding(BasicHttpSecurityMode.Transport),
+ new EndpointAddress("https://localhost:7233/MixedAuthService.svc/authenticated"));
+var channel = channelFactory.CreateChannel();
+
+var context = new OperationContext(channel);
+using (var _ = new OperationContextScope(context))
+{
+ var httpRequestProperty = new HttpRequestMessageProperty();
+ httpRequestProperty.Headers[HttpRequestHeader.Authorization] = $"Bearer {tokenResponse.AccessToken}";
+ context.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
+ var response = await channel.EchoAsync("Hello world");
+ Console.WriteLine(response);
+ var response2 = await channel.EchoAnonymousAsync("Hey");
+ Console.WriteLine(response2);
+}
+
+// Create an anonymous client and call 'EchoAnonymous'
+var anonymousChannelFactory = new ChannelFactory(new BasicHttpBinding(BasicHttpSecurityMode.Transport),
+ new EndpointAddress("https://localhost:7233/MixedAuthService.svc/anonymous"));
+
+var anonymousChannel = anonymousChannelFactory.CreateChannel();
+
+var response3 = await anonymousChannel.EchoAnonymousAsync("Bonjour");
+Console.WriteLine(response3);
+
+Console.ReadLine();
+
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Idp.csproj b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Idp.csproj
new file mode 100644
index 0000000..7e0d85e
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Idp.csproj
@@ -0,0 +1,11 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Program.cs b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Program.cs
new file mode 100644
index 0000000..6508ffe
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Program.cs
@@ -0,0 +1,78 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Security.Claims;
+using Microsoft.IdentityModel.Tokens;
+using OpenIddict.Abstractions;
+using OpenIddict.Server;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddOpenIddict()
+ // The OpenIddict configuration below provides a minimal OpenId Provider
+ // which only supports the client_credentials flow for demo purpose.
+ .AddServer(options =>
+ {
+ options.AddDevelopmentEncryptionCertificate();
+ options.AddDevelopmentSigningCertificate();
+ options.AllowClientCredentialsFlow();
+ options.SetTokenEndpointUris("token");
+ options.EnableDegradedMode();
+ options.UseAspNetCore();
+ options.RegisterScopes("api");
+ options.DisableAccessTokenEncryption();
+ options.AddEventHandler(options =>
+ options.UseInlineHandler(context =>
+ {
+ if (!context.Request.IsClientCredentialsGrantType())
+ {
+ context.Reject(error: OpenIddictConstants.Errors.InvalidGrant);
+ return default;
+ }
+
+ if (!string.Equals(context.ClientId, "console_app", StringComparison.Ordinal))
+ {
+ context.Reject(error: OpenIddictConstants.Errors.InvalidClient);
+ return default;
+ }
+
+ // In real world the client_secret would be a time constant derivation of the client's secret
+ // to mitigate statistical attacks.
+ // Here we used the "secret" value for readability.
+ if (!string.Equals(context.ClientSecret, "secret", StringComparison.Ordinal))
+ {
+ context.Reject(error: OpenIddictConstants.Errors.InvalidClient);
+ return default;
+ }
+
+ if (string.IsNullOrEmpty(context.Request.Scope))
+ {
+ context.Reject(error: OpenIddictConstants.Errors.InvalidScope);
+ }
+
+ return default;
+ }));
+ options.AddEventHandler(options =>
+ {
+ options.UseInlineHandler(context =>
+ {
+ var identity = new ClaimsIdentity(
+ authenticationType: TokenValidationParameters.DefaultAuthenticationType,
+ nameType: OpenIddictConstants.Claims.Name,
+ roleType: OpenIddictConstants.Claims.Role);
+
+ identity.SetScopes("api");
+ identity.SetAudiences("api");
+ identity.SetClaim(OpenIddictConstants.Claims.Subject, context.ClientId);
+
+ context.Principal = new ClaimsPrincipal(identity);
+ return default;
+ });
+ });
+ });
+
+var app = builder.Build();
+
+app.MapGet("/", () => Results.Ok());
+
+app.Run();
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Properties/launchSettings.json b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Properties/launchSettings.json
new file mode 100644
index 0000000..919ee97
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Properties/launchSettings.json
@@ -0,0 +1,37 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:33372",
+ "sslPort": 44369
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5059",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7222;http://localhost:5059",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/appsettings.Development.json b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/appsettings.json b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Mixed-auth-allow-anonymous-workaround.sln b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Mixed-auth-allow-anonymous-workaround.sln
new file mode 100644
index 0000000..1f6147f
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Mixed-auth-allow-anonymous-workaround.sln
@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33205.214
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{F8D3FD74-1948-4948-A382-BD2D6ED752C8}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Idp", "Idp\Idp.csproj", "{695BD62A-30ED-4ED5-87F1-78BAF7ED3138}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Service", "Service\Service.csproj", "{2B7CDDE9-0AFD-4AD2-8DFC-0EE537C6C7B8}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {F8D3FD74-1948-4948-A382-BD2D6ED752C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F8D3FD74-1948-4948-A382-BD2D6ED752C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F8D3FD74-1948-4948-A382-BD2D6ED752C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F8D3FD74-1948-4948-A382-BD2D6ED752C8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {695BD62A-30ED-4ED5-87F1-78BAF7ED3138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {695BD62A-30ED-4ED5-87F1-78BAF7ED3138}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {695BD62A-30ED-4ED5-87F1-78BAF7ED3138}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {695BD62A-30ED-4ED5-87F1-78BAF7ED3138}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2B7CDDE9-0AFD-4AD2-8DFC-0EE537C6C7B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B7CDDE9-0AFD-4AD2-8DFC-0EE537C6C7B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2B7CDDE9-0AFD-4AD2-8DFC-0EE537C6C7B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2B7CDDE9-0AFD-4AD2-8DFC-0EE537C6C7B8}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {7B4EA383-98B2-44EC-B868-5030B9430698}
+ EndGlobalSection
+EndGlobal
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/README.md b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/README.md
new file mode 100644
index 0000000..d6fbba8
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/README.md
@@ -0,0 +1,18 @@
+## Mixed-auth-allow-anonymous-workaround
+
+This sample shows a mixed authentication service which uses two `ServiceContract` to require or not authenticated requests using two separate endpoints.
+The pattern implemented can be used as a workaround to the lack of support of the `AllowAnonymous` attribute.
+The authentication flow is known as [OAuth2.0 client_credentials](https://www.rfc-editor.org/rfc/rfc6749#section-1.3.4) flow.
+
+### Idp
+
+`Idp` is a security token service which implements an OAuth client_credentials flow using [OpenIddict](https://github.com/openiddict).
+
+### Service
+
+The `/authenticated` endpoint of service `Service` is configured to accept requests authenticated with a valid bearer `access_token` issued by `Idp` with audience and scope valued to 'api'. The authentication is performed by the standard JwtBearer AuthenticationHandler shipped with ASP.NET Core in the `Microsoft.AspNetCore.Authentication.JwtBearer` nuget package.
+The `/anonymous` endpoint of `Service` does not require authentication.
+
+### 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 `/authenticated` `Service` and the `/anonymous` endpoints of `Service`.
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/IAnonymousService.cs b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/IAnonymousService.cs
new file mode 100644
index 0000000..5149fc3
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/IAnonymousService.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Service;
+
+[ServiceContract]
+public interface IAnonymousService
+{
+ [OperationContract]
+ string EchoAnonymous(string value);
+}
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/ISecuredService.cs b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/ISecuredService.cs
new file mode 100644
index 0000000..7f9f909
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/ISecuredService.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Service;
+
+[ServiceContract]
+public interface ISecuredService : IAnonymousService
+{
+ [OperationContract]
+ string Echo(string value);
+}
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/MixedAuthService.cs b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/MixedAuthService.cs
new file mode 100644
index 0000000..41ceb08
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/MixedAuthService.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Authorization;
+
+namespace Service
+{
+ [Authorize]
+ public class MixedAuthService : ISecuredService
+ {
+ public string Echo(string value) => value;
+
+ public string EchoAnonymous(string value) => value;
+ }
+}
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Program.cs b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Program.cs
new file mode 100644
index 0000000..131d5a5
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Program.cs
@@ -0,0 +1,67 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+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://localhost:7222/";
+ options.Audience = "api";
+ });
+builder.Services.AddAuthorization(options =>
+{
+ options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
+ .RequireAuthenticatedUser()
+ .RequireAssertion(context =>
+ {
+ string[] scopes = context.User.FindFirst("scope")?.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries)
+ ?? Array.Empty();
+ return scopes.Any(x => string.Equals(x, "api", StringComparison.Ordinal));
+ })
+ .Build();
+});
+builder.Services.AddTransient();
+
+builder.Services.AddServiceModelServices();
+builder.Services.AddServiceModelMetadata();
+builder.Services.AddSingleton();
+
+var app = builder.Build();
+
+app.UseServiceModel(serviceBuilder =>
+{
+ serviceBuilder.AddService(options =>
+ {
+ options.BaseAddresses.Add(new Uri("https://localhost:7233/MixedAuthService.svc"));
+ });
+ serviceBuilder.AddServiceEndpoint(new BasicHttpBinding
+ {
+ Security = new BasicHttpSecurity
+ {
+ Mode = BasicHttpSecurityMode.Transport,
+ Transport = new HttpTransportSecurity
+ {
+ ClientCredentialType = HttpClientCredentialType.None
+ }
+ }
+ }, "/anonymous");
+ serviceBuilder.AddServiceEndpoint(new BasicHttpBinding
+ {
+ Security = new BasicHttpSecurity
+ {
+ Mode = BasicHttpSecurityMode.Transport,
+ Transport = new HttpTransportSecurity
+ {
+ ClientCredentialType = HttpClientCredentialType.InheritedFromHost
+ }
+ }
+ }, "/authenticated");
+ var serviceMetadataBehavior = app.Services.GetRequiredService();
+ serviceMetadataBehavior.HttpsGetEnabled = true;
+});
+
+app.Run();
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Properties/launchSettings.json b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Properties/launchSettings.json
new file mode 100644
index 0000000..49475b5
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "profiles": {
+ "Service": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:7233;http://localhost:5064"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Service.csproj b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Service.csproj
new file mode 100644
index 0000000..96298a0
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Service.csproj
@@ -0,0 +1,23 @@
+
+
+ net6.0
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/appsettings.Development.json b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/appsettings.json b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}