From a797f038345b5aad12eafa75bbdcecf5a45e77ca Mon Sep 17 00:00:00 2001 From: g7ed6e Date: Mon, 16 Jan 2023 08:01:55 +0100 Subject: [PATCH] Add Mixed-auth-AllowAnonymous-workaround --- .github/workflows/dotnet.yml | 6 + .../Client/Client.csproj | 20 ++ .../ServiceReference1/ConnectedService.json | 16 ++ .../ServiceReference1/Reference.cs | 241 ++++++++++++++++++ .../Client/Program.cs | 61 +++++ .../Idp/Idp.csproj | 11 + .../Idp/Program.cs | 78 ++++++ .../Idp/Properties/launchSettings.json | 37 +++ .../Idp/appsettings.Development.json | 8 + .../Idp/appsettings.json | 9 + .../Mixed-auth-allow-anonymous-workaround.sln | 37 +++ .../README.md | 18 ++ .../Service/IAnonymousService.cs | 11 + .../Service/ISecuredService.cs | 11 + .../Service/MixedAuthService.cs | 15 ++ .../Service/Program.cs | 67 +++++ .../Service/Properties/launchSettings.json | 11 + .../Service/Service.csproj | 23 ++ .../Service/appsettings.Development.json | 8 + .../Service/appsettings.json | 9 + 20 files changed, 697 insertions(+) create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Client.csproj create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Connected Services/ServiceReference1/ConnectedService.json create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Connected Services/ServiceReference1/Reference.cs create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Client/Program.cs create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Idp.csproj create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Program.cs create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/Properties/launchSettings.json create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/appsettings.Development.json create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Idp/appsettings.json create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Mixed-auth-allow-anonymous-workaround.sln create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/README.md create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/IAnonymousService.cs create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/ISecuredService.cs create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/MixedAuthService.cs create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Program.cs create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Properties/launchSettings.json create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/Service.csproj create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/appsettings.Development.json create mode 100644 Scenarios/Authentication/Mixed-auth-allow-anonymous-workaround/Service/appsettings.json 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": "*" +}