From 7328056d6b55fd36c185fd4537fe9b4b23d94a9f Mon Sep 17 00:00:00 2001 From: Sam Spencer Date: Tue, 23 Aug 2022 12:21:50 -0700 Subject: [PATCH 1/5] Sample for JWT auth --- .../TileService/.dockerignore | 25 +++ .../TileService/Dockerfile | 22 +++ .../TileService/ITileService.cs | 123 ++++++++++++ .../TileService/Program.cs | 39 ++++ .../Properties/launchSettings.json | 17 ++ .../TileService/TileService.csproj | 24 +++ .../TileService/TileService.sln | 31 +++ .../TileService/appsettings.Development.json | 8 + .../TileService/appsettings.json | 17 ++ .../ServiceReference1/ConnectedService.json | 16 ++ .../ServiceReference1/Reference.cs | 181 ++++++++++++++++++ .../WordGame/Program.cs | 86 +++++++++ .../WordGame/Properties/launchSettings.json | 28 +++ .../WordGame/WordGame.csproj | 19 ++ .../WordGame/appsettings.Development.json | 8 + .../WordGame/appsettings.json | 20 ++ 16 files changed, 664 insertions(+) create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/.dockerignore create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Dockerfile create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/ITileService.cs create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Program.cs create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Properties/launchSettings.json create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/TileService.csproj create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/TileService.sln create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/appsettings.Development.json create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/appsettings.json create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Connected Services/ServiceReference1/ConnectedService.json create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Connected Services/ServiceReference1/Reference.cs create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Properties/launchSettings.json create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/WordGame.csproj create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/appsettings.Development.json create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/appsettings.json diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/.dockerignore b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/.dockerignore new file mode 100644 index 0000000..3729ff0 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Dockerfile b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Dockerfile new file mode 100644 index 0000000..23ad846 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Dockerfile @@ -0,0 +1,22 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["TileService.csproj", "."] +RUN dotnet restore "./TileService.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "TileService.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "TileService.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "TileService.dll"] \ No newline at end of file diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/ITileService.cs b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/ITileService.cs new file mode 100644 index 0000000..7b2ac48 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/ITileService.cs @@ -0,0 +1,123 @@ +using CoreWCF; +using System; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Identity.Web.Resource; + +namespace TileService +{ + [ServiceContract] + public interface ITileService + { + [OperationContract] + IList DrawTiles(int count); + } + + public class TileBag : ITileService + { + private static IList _gameTiles = initTileCollection(); + private static int TileCount; + private static Random rnd = new Random(); + + // [AuthorizeRole("AccessTileBag")] + public IList DrawTiles(int count) + { + + // TODO: Remove this block + var ctxa = new HttpContextAccessor(); + var ctx = ctxa.HttpContext; + if (ctx == null) { Console.WriteLine("no http context"); } + else + { + foreach (var claim in ctx.User.Claims) + { + Console.WriteLine("***** BEGIN CLAIM *****"); + Console.WriteLine(claim.ToString()); + Console.WriteLine("***** END CLAIM *****"); + } + Console.WriteLine("***** BEGIN HEADERS *****"); + foreach (var h in ctx.Request.Headers) + { + Console.WriteLine(h.Key + ":" + h.Value); + } + Console.WriteLine("***** END HEADERS *****"); + } + if (count > 0) + { + var tiles = new List(); + for (var i = 0; i < count; i++) + { + var index = rnd.Next(TileCount); + var t_offset = 0; + foreach (var t in _gameTiles) + { + if (t_offset + t.Weight > index) + { + tiles.Add(t); + break; + } + t_offset += t.Weight; + } + } + return tiles; + } + throw new ArgumentException("DrawTiles needs to be given a positive number of tiles to return"); + } + + + private static IList initTileCollection() + { + var tiles = new List(); + tiles.Add(new GameTile('A', 1, 9)); + tiles.Add(new GameTile('B', 3, 2)); + tiles.Add(new GameTile('C', 3, 2)); + tiles.Add(new GameTile('D', 2, 4)); + tiles.Add(new GameTile('E', 1, 12)); + tiles.Add(new GameTile('F', 4, 2)); + tiles.Add(new GameTile('G', 2, 3)); + tiles.Add(new GameTile('H', 4, 2)); + tiles.Add(new GameTile('I', 1, 9)); + tiles.Add(new GameTile('J', 7, 1)); + tiles.Add(new GameTile('K', 5, 1)); + tiles.Add(new GameTile('L', 1, 4)); + tiles.Add(new GameTile('M', 3, 2)); + tiles.Add(new GameTile('N', 1, 6)); + tiles.Add(new GameTile('O', 1, 8)); + tiles.Add(new GameTile('P', 3, 2)); + tiles.Add(new GameTile('Q', 10, 1)); + tiles.Add(new GameTile('R', 1, 9)); + tiles.Add(new GameTile('S', 1, 4)); + tiles.Add(new GameTile('T', 1, 6)); + tiles.Add(new GameTile('U', 1, 4)); + tiles.Add(new GameTile('V', 4, 2)); + tiles.Add(new GameTile('W', 1, 9)); + tiles.Add(new GameTile('X', 8, 1)); + tiles.Add(new GameTile('Y', 4, 2)); + tiles.Add(new GameTile('Z', 10, 1)); + tiles.Add(new GameTile('\0', 0, 2)); + + TileCount = (from t in tiles + select t.Weight).Sum(); + return tiles; + } + } + + [DataContract] + public class GameTile + { + [DataMember] + public char Letter { get; init; } + [DataMember] + public int Score { get; init; } + [DataMember] + public int Weight { get; init; } + + public GameTile(char letter, int score, int weight) + { + Letter = letter; + Score = score; + Weight = weight; + } + } +} diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Program.cs b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Program.cs new file mode 100644 index 0000000..5cb80de --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Program.cs @@ -0,0 +1,39 @@ +using Microsoft.Identity.Web; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.AspNetCore.Authentication.JwtBearer; + +var builder = WebApplication.CreateBuilder(); + +// CoreWCF Services +builder.Services.AddServiceModelServices(); +builder.Services.AddServiceModelMetadata(); +builder.Services.AddSingleton(); + +// Asp.NET Core Authentication +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration); + +//TODO: Remove +builder.Services.AddHttpContextAccessor(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; +} + +app.UseAuthentication(); +app.UseAuthorization(); + +app.UseServiceModel(serviceBuilder => +{ + serviceBuilder.AddService(); + serviceBuilder.AddServiceEndpoint(new BasicHttpBinding(BasicHttpSecurityMode.Transport), "https://host/TileService"); + var serviceMetadataBehavior = app.Services.GetRequiredService(); + serviceMetadataBehavior.HttpsGetEnabled = true; +}); + +app.MapGet("/", () => "Error incorrect path: Use /TileService to access the service"); + +app.Run(); diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Properties/launchSettings.json b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Properties/launchSettings.json new file mode 100644 index 0000000..49d4e97 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Properties/launchSettings.json @@ -0,0 +1,17 @@ +{ + "profiles": { + "TileService": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:7121;http://localhost:5035" + }, + "Docker": { + "commandName": "Docker", + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "publishAllPorts": true, + "useSSL": true + } + } +} \ No newline at end of file diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/TileService.csproj b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/TileService.csproj new file mode 100644 index 0000000..7d200c7 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/TileService.csproj @@ -0,0 +1,24 @@ + + + net6.0 + enable + true + dfcd5381-edd3-4aa9-ac84-877a2a131db4 + Linux + . + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/TileService.sln b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/TileService.sln new file mode 100644 index 0000000..b8e1465 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/TileService.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.32728.343 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TileService", "TileService.csproj", "{8B29DBB7-9AEF-4809-ADB7-A376B16A2C84}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WordGame", "..\WordGame\WordGame.csproj", "{D9DFC022-7E02-4595-908C-BB0A5683CDF5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8B29DBB7-9AEF-4809-ADB7-A376B16A2C84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B29DBB7-9AEF-4809-ADB7-A376B16A2C84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B29DBB7-9AEF-4809-ADB7-A376B16A2C84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B29DBB7-9AEF-4809-ADB7-A376B16A2C84}.Release|Any CPU.Build.0 = Release|Any CPU + {D9DFC022-7E02-4595-908C-BB0A5683CDF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9DFC022-7E02-4595-908C-BB0A5683CDF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9DFC022-7E02-4595-908C-BB0A5683CDF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9DFC022-7E02-4595-908C-BB0A5683CDF5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B915AA32-40C3-40DD-B7BC-A30DD28CA42B} + EndGlobalSection +EndGlobal diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/appsettings.Development.json b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/appsettings.json b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/appsettings.json new file mode 100644 index 0000000..99026e4 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug" + /*, + "Default": "Information", + "Microsoft.AspNetCore": "Warning" */ + } + }, + "AllowedHosts": "*", + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "ClientId": "[Enter the Client Id of the service (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", + "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]", + "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]" + } +} diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Connected Services/ServiceReference1/ConnectedService.json b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Connected Services/ServiceReference1/ConnectedService.json new file mode 100644 index 0000000..0c98a1c --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Connected Services/ServiceReference1/ConnectedService.json @@ -0,0 +1,16 @@ +{ + "ExtendedData": { + "inputs": [ + "https://localhost:7121/TileService" + ], + "collectionTypes": [ + "System.Array", + "System.Collections.Generic.Dictionary`2" + ], + "namespaceMappings": [ + "*, ServiceReference1" + ], + "targetFramework": "net6.0", + "typeReuseMode": "All" + } +} \ No newline at end of file diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Connected Services/ServiceReference1/Reference.cs b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Connected Services/ServiceReference1/Reference.cs new file mode 100644 index 0000000..ea7aad8 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Connected Services/ServiceReference1/Reference.cs @@ -0,0 +1,181 @@ +//------------------------------------------------------------------------------ +// +// 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 +{ + using System.Runtime.Serialization; + + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.0.3")] + [System.Runtime.Serialization.DataContractAttribute(Name="GameTile", Namespace="http://schemas.datacontract.org/2004/07/TileService")] + public partial class GameTile : object + { + + private char LetterField; + + private int ScoreField; + + private int WeightField; + + [System.Runtime.Serialization.DataMemberAttribute()] + public char Letter + { + get + { + return this.LetterField; + } + set + { + this.LetterField = value; + } + } + + [System.Runtime.Serialization.DataMemberAttribute()] + public int Score + { + get + { + return this.ScoreField; + } + set + { + this.ScoreField = value; + } + } + + [System.Runtime.Serialization.DataMemberAttribute()] + public int Weight + { + get + { + return this.WeightField; + } + set + { + this.WeightField = value; + } + } + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.0.3")] + [System.ServiceModel.ServiceContractAttribute(ConfigurationName="ServiceReference1.ITileService")] + public interface ITileService + { + + [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ITileService/DrawTiles", ReplyAction="http://tempuri.org/ITileService/DrawTilesResponse")] + System.Threading.Tasks.Task DrawTilesAsync(int count); + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.0.3")] + public interface ITileServiceChannel : ServiceReference1.ITileService, System.ServiceModel.IClientChannel + { + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.0.3")] + public partial class TileServiceClient : System.ServiceModel.ClientBase, ServiceReference1.ITileService + { + + /// + /// 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 TileServiceClient() : + base(TileServiceClient.GetDefaultBinding(), TileServiceClient.GetDefaultEndpointAddress()) + { + this.Endpoint.Name = EndpointConfiguration.BasicHttpBinding_ITileService.ToString(); + ConfigureEndpoint(this.Endpoint, this.ClientCredentials); + } + + public TileServiceClient(EndpointConfiguration endpointConfiguration) : + base(TileServiceClient.GetBindingForEndpoint(endpointConfiguration), TileServiceClient.GetEndpointAddress(endpointConfiguration)) + { + this.Endpoint.Name = endpointConfiguration.ToString(); + ConfigureEndpoint(this.Endpoint, this.ClientCredentials); + } + + public TileServiceClient(EndpointConfiguration endpointConfiguration, string remoteAddress) : + base(TileServiceClient.GetBindingForEndpoint(endpointConfiguration), new System.ServiceModel.EndpointAddress(remoteAddress)) + { + this.Endpoint.Name = endpointConfiguration.ToString(); + ConfigureEndpoint(this.Endpoint, this.ClientCredentials); + } + + public TileServiceClient(EndpointConfiguration endpointConfiguration, System.ServiceModel.EndpointAddress remoteAddress) : + base(TileServiceClient.GetBindingForEndpoint(endpointConfiguration), remoteAddress) + { + this.Endpoint.Name = endpointConfiguration.ToString(); + ConfigureEndpoint(this.Endpoint, this.ClientCredentials); + } + + public TileServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : + base(binding, remoteAddress) + { + } + + public System.Threading.Tasks.Task DrawTilesAsync(int count) + { + return base.Channel.DrawTilesAsync(count); + } + + 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)); + } + + public virtual System.Threading.Tasks.Task CloseAsync() + { + return System.Threading.Tasks.Task.Factory.FromAsync(((System.ServiceModel.ICommunicationObject)(this)).BeginClose(null, null), new System.Action(((System.ServiceModel.ICommunicationObject)(this)).EndClose)); + } + + private static System.ServiceModel.Channels.Binding GetBindingForEndpoint(EndpointConfiguration endpointConfiguration) + { + if ((endpointConfiguration == EndpointConfiguration.BasicHttpBinding_ITileService)) + { + 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_ITileService)) + { + return new System.ServiceModel.EndpointAddress("https://localhost:7121/TileService"); + } + throw new System.InvalidOperationException(string.Format("Could not find endpoint with name \'{0}\'.", endpointConfiguration)); + } + + private static System.ServiceModel.Channels.Binding GetDefaultBinding() + { + return TileServiceClient.GetBindingForEndpoint(EndpointConfiguration.BasicHttpBinding_ITileService); + } + + private static System.ServiceModel.EndpointAddress GetDefaultEndpointAddress() + { + return TileServiceClient.GetEndpointAddress(EndpointConfiguration.BasicHttpBinding_ITileService); + } + + public enum EndpointConfiguration + { + + BasicHttpBinding_ITileService, + } + } +} diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs new file mode 100644 index 0000000..51ec52c --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs @@ -0,0 +1,86 @@ +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; +using ServiceReference1; +using System.ServiceModel.Channels; +using System.ServiceModel; + +var builder = WebApplication.CreateBuilder(args); + +var tileServiceUrl = builder.Configuration["TileServiceUrl"]; +var tileService = TileServiceFactory(tileServiceUrl); + +//builder.Services.AddSingleton(TileServiceFactory); +var jwt = await getAzAdJwtBlob(builder.Configuration); +var app = builder.Build(); + +// Use Minimal WebApi Endpoint for the demo functionality +app.MapGet("/", (int count) => getTiles(tileService, count, jwt)); + +app.Run(); + +// Factory for TileService +TileServiceClient TileServiceFactory(string tileServiceUrl) +{ + return new TileServiceClient(TileServiceClient.EndpointConfiguration.BasicHttpBinding_ITileService, tileServiceUrl); +} + +//TileServiceClient TileServiceFactory(IServiceProvider provider) +//{ +// var config = provider.GetRequiredService(IConfiguration); +// var tileServiceUrl = config["TileServiceUrl"]; +// return new TileServiceClient(TileServiceClient.EndpointConfiguration.BasicHttpBinding_ITileService, tileServiceUrl); +//} + +async Task> getTiles(TileServiceClient tileService, int count, string jwt) +{ + // Create a scope for this call + using (new OperationContextScope((tileService).InnerChannel)) + { + // Add an http Authorization header with the JWT + HttpRequestMessageProperty requestMessage = new HttpRequestMessageProperty(); + requestMessage.Headers["Authorization"] = $"Bearer {jwt}"; + OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = requestMessage; + return await tileService.DrawTilesAsync(count); + } + +} + + +// Azure Active Directoy specific code to authenticate a daemon app with a service +// Based on https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Call-OwnApi-Pop +async Task getAzAdJwtBlob(ConfigurationManager config) +{ + // Fetch the auth details from configuration or the environment for secrets + var adTenant = config["AzureAdTenant"]; + var clientId = config["ClientId"]; + var clientSecret = config["ClientSecret"]; + var tileServiceScope = config["TileServiceScope"]; + + var authority = new Uri($"https://login.microsoftonline.com/{adTenant}"); + var scopes = new string[] { tileServiceScope }; + + var app = ConfidentialClientApplicationBuilder.Create(clientId) + .WithClientSecret(clientSecret) + .WithAuthority(authority) + .Build(); + + app.AddInMemoryTokenCache(); + + AuthenticationResult result; + try + { + result = await app.AcquireTokenForClient(scopes) + .ExecuteAsync(); + } + catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011")) + { + // Invalid scope. The scope has to be of the form "https://resourceurl/.default" + // Mitigation: change the scope to be as expected + + // Rethrow so we can see it in the application logs + throw ex; + } + + return result.AccessToken; +} + diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Properties/launchSettings.json b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Properties/launchSettings.json new file mode 100644 index 0000000..cd242e4 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:38364", + "sslPort": 44366 + } + }, + "profiles": { + "WordGame": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7054;http://localhost:5122", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/WordGame.csproj b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/WordGame.csproj new file mode 100644 index 0000000..2d21bc3 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/WordGame.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + 6101badd-1e58-43da-a6a9-427625862069 + + + + + + + + + + + + diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/appsettings.Development.json b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/appsettings.json b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/appsettings.json new file mode 100644 index 0000000..09fd05c --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "TileServiceUrl": "https://localhost:49161/TileService", + + /* Azure AD properties for JWT Auth */ + "ClientId": "[Enter the Client Id of the service (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", + "AzureAdTenant": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]", + "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", + "TileServiceScope": "api://[Enter_client_ID_Of_TodoListService-v2_from_Azure_Portal,_e.g._2ec40e65-ba09-4853-bcde-bcb60029e596]/.default", + + /* DO NOT SPECIFY THIS HERE, See https://docs.microsoft.com/aspnet/core/security/app-secrets */ + "ClientSecret": "Specify this property in a user secrets file instead" + +} From 1fe49616046d3b5b1d28803562842b9aee24ef18 Mon Sep 17 00:00:00 2001 From: Sam Spencer Date: Tue, 23 Aug 2022 15:39:57 -0700 Subject: [PATCH 2/5] Added ReadMe Misc cleanup --- .../Readme.md | 107 ++++++++++++++++++ ...vice.sln => ServiceToServiceJWTSample.sln} | 4 +- .../WordGame/Program.cs | 3 +- 3 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md rename Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/{TileService/TileService.sln => ServiceToServiceJWTSample.sln} (90%) diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md new file mode 100644 index 0000000..54ad881 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md @@ -0,0 +1,107 @@ +# Service to Service authentication using a JWT + +## Introduction + +This sample shows how a client can authenticate with a CoreWCF service using JWT-based authentication. In this specific sample, it is using Azure Active Directory as the identity provider, and the calling app is another service. The same pattern will apply to other JWT based authentication providers or to end-user rather than service to service authentication. + +## What is a JWT + +[Jason Web Token](https://en.wikipedia.org/wiki/JSON_Web_Token) is a way to support federated authentication and authorization of http requests. The client makes a call to an authentication server passing credentials and identifying information about the resource they wish to access. If authentication succeeds and the resource is authorized, then they are handed back a signed token, which includes information about the resource and rights. They then pass that token to the service, which can verify its integrity, and then trust the claims it specifies. + +JWT is an open standard, so is supported by multiple server stacks, clients, code languages and identity providers. + +## Azure Active Directory + +This particular sample is designed for use with Azure Active Directory (AAD) as the identity provider. However the code that is specific to AAD is limited and can be easily replaced with a different identity provider: +- In the client the code & settings to authenticate with AAD and retrieve the token +- In the server, the code & settings to use AAD as the JWT provider for ASP.NET Core + +If you wish to run the samples as-is then you will need to register the applications with an existing Azure AD Tenent, or create a new one. This sample is based off one of the [Azure AD samples](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Call-OwnApi-Pop). Instructions on how to register apps are included in it's readme. + +## Projects + +The sample consists of 2 projects around the concept of a word game: +- TileService is a WCF Core server app that exposes an http endpoint that will return a set of letter tiles that could be used in a word game. +- WordGame is an ASP.NET Core Web API endpoint that will call the TileService and return the results as a JSON blob. + +These projects were chosen so that there is minimal sample code that is not related to the role of the authentication flow. + +## Managing Secrets + +In any kind of scenario involving service to service authentication, you will invariably need to deal with some form of shared secret, be it a string or a client certificate. These should not be included in code, and at runtime come from some form of secure storage, and definetely should not included in source control for the world to see! .NET makes this easier to manage through the configuration API and built-in overlays: +- At design time, the [User Secrets](https://docs.microsoft.com/aspnet/core/security/app-secrets) feature will create a configuration overlay file that can be used to store secrets. In Visual Studio, right click on the project and choose *Manage User Secrets* to create and open the overlay file. Client Secrets should be stored in this file during development. +- At runtime, Environment Variables will overlay over config properties. Connection strings and secrets can be placed there and then accessed via configuration. Hosting environments have support for safely storing secrets and supplying them at runtime. Eg [App Service](https://docs.microsoft.com/azure/app-service/configure-common?tabs=portal#configure-app-settings) or [Azure Container Apps](https://docs.microsoft.com/azure/container-apps/manage-secrets?tabs=azure-cli) + +The appsettings.json files in the projects include the names of the config parameters that are required for the AAD authentication. They can be stored in User Secrets for additional security during development. + +# Using JWT with WCF Services + +The WS-* specifications which define the SOAP protocol and form the basis for WCF were developed long before JWT came onto the scene as the preferred form of web authentication. For this reason the WCF client APIs don't include direct support for JWT-based authentication or Authorization. However, JWT is implemented over http by supplying the token as a base64 encoded string as the authenticate header. So these samples add that header and validate it as part of the service call. + +## Azure AD configuration +Within the Azure AD Tenent used for the authentication, the following need to be configured. See this [Readme](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Call-OwnApi-Pop) for instructions. +- App registration for the Tile Service + - With an App role of `AccessTileBag` +- App registration of the Word Game app + - With a client secret to identify the app + - Granted the API Permission of `AccessTileBag` for the Tile Service API + +## Tile Service (Server app) +TileService is a CoreWCF Server app. It supports one API defined in *ITileService.cs* for `DrawTiles`. + +The app uses the Azure AD integration with ASP.NET Core to enable and perform JWT Authentication. This is done through: +- Nuget references for `Microsoft.Identity.Web` and `Microsoft.VisualStudio.Azure.Containers.Tools.Targets` + +- Including the ASP.NET Core and Azure AD SDK support for JWT with + +``` c# +// Asp.NET Core Authentication +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration); +``` + +- Adding Authentication and Authorization to the ASP.NET Core pipeline with + +``` c# +app.UseAuthentication(); +app.UseAuthorization(); +``` + +- Attributing the service call with the claims that need to be present + +``` c# +[AuthorizeRole("AccessTileBag")] +public IList DrawTiles(int count) +{ + ... +} +``` +- The parameters for the Azure SDK to validate the JWT are provided through configuration in appsettings.json + +``` json + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "ClientId": "[Enter the Client Id of the service (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", + "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]", + "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]" + } +``` + +## WordGame (Service Client) + +The service client is included in a WebAPI app that exposes a simple HTTP GET endpoint to fetch the tiles. + +To make the WCF service calls, the app uses a Service Reference client wrapper, generated from the WSDL from the Tile Service. + +The key parts of the application are: + +- At startup, it calls `getAzAdJwtBlob`, which + - Reads AAD properties from configuration (appsetting.json) and User Secrets + - Calls AAD to get a JWT for this specific service +- Exposes a WebAPI at `/getTiles` that will make the WCF service call and return the response as JSON + - Exposes a WebAPI at `/` which redirects to /getTiles with a count parameter +- The `getTiles` function which + - Uses an `OperationContextScope` to add an http `Authorize` header with the JWT as the value + - Calls the Tile Service API using the generated wrapper class + +The only AAD specific code in this sample is included in the getAzAdJWTBlob function. The same pattern can be followed to retrieve a JWT from other authentication providers, or performing user authentication etc. \ No newline at end of file diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/TileService.sln b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/ServiceToServiceJWTSample.sln similarity index 90% rename from Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/TileService.sln rename to Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/ServiceToServiceJWTSample.sln index b8e1465..c53b781 100644 --- a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/TileService.sln +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/ServiceToServiceJWTSample.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.32728.343 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TileService", "TileService.csproj", "{8B29DBB7-9AEF-4809-ADB7-A376B16A2C84}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TileService", "TileService\TileService.csproj", "{8B29DBB7-9AEF-4809-ADB7-A376B16A2C84}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WordGame", "..\WordGame\WordGame.csproj", "{D9DFC022-7E02-4595-908C-BB0A5683CDF5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WordGame", "WordGame\WordGame.csproj", "{D9DFC022-7E02-4595-908C-BB0A5683CDF5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs index 51ec52c..bde207f 100644 --- a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs @@ -14,7 +14,8 @@ var app = builder.Build(); // Use Minimal WebApi Endpoint for the demo functionality -app.MapGet("/", (int count) => getTiles(tileService, count, jwt)); +app.Map("/", () => Results.Redirect("/getTiles?Count=5")); +app.MapGet("/getTiles", (int count) => getTiles(tileService, count, jwt)); app.Run(); From d5017d83cef5aba1b12a6246e4b35fd36d7b1217 Mon Sep 17 00:00:00 2001 From: Sam Spencer Date: Wed, 24 Aug 2022 13:39:28 -0700 Subject: [PATCH 3/5] Fixed based on @adityamandaleeka's feedback --- .../Readme.md | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md index 54ad881..4e17fa3 100644 --- a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md @@ -1,22 +1,22 @@ -# Service to Service authentication using a JWT +# Service-to-Service Authentication Using a JWT ## Introduction -This sample shows how a client can authenticate with a CoreWCF service using JWT-based authentication. In this specific sample, it is using Azure Active Directory as the identity provider, and the calling app is another service. The same pattern will apply to other JWT based authentication providers or to end-user rather than service to service authentication. +This sample shows how a client can authenticate with a CoreWCF service using JWT-based authentication. It includes two services; one that uses Azure Active Directory as the identity provider, and another that makes authenticated requests to the first service. The same pattern will apply to other JWT-based authentication providers or to end-user rather than service-to-service authentication. ## What is a JWT -[Jason Web Token](https://en.wikipedia.org/wiki/JSON_Web_Token) is a way to support federated authentication and authorization of http requests. The client makes a call to an authentication server passing credentials and identifying information about the resource they wish to access. If authentication succeeds and the resource is authorized, then they are handed back a signed token, which includes information about the resource and rights. They then pass that token to the service, which can verify its integrity, and then trust the claims it specifies. +[JSON Web Token](https://en.wikipedia.org/wiki/JSON_Web_Token) is a way to support federated authentication and authorization of http requests. The client makes a call to an authentication server passing credentials and identifying information about the resource they wish to access. If authentication succeeds and the resource is authorized, they are handed back a signed token which includes information about the resource and rights. The client then passes that token to the service, which can then verify its integrity and trust the claims it specifies. -JWT is an open standard, so is supported by multiple server stacks, clients, code languages and identity providers. +JWT is an open standard supported by multiple server stacks, clients, code languages and identity providers. ## Azure Active Directory -This particular sample is designed for use with Azure Active Directory (AAD) as the identity provider. However the code that is specific to AAD is limited and can be easily replaced with a different identity provider: -- In the client the code & settings to authenticate with AAD and retrieve the token -- In the server, the code & settings to use AAD as the JWT provider for ASP.NET Core +This particular sample is designed for use with Azure Active Directory (AAD) as the identity provider. However, the code that is specific to AAD is limited and another identity provider can easily be handled by replacing: +- In the client, the code and settings to authenticate with AAD and retrieve the token +- In the server, the code and settings to use AAD as the JWT provider for ASP.NET Core -If you wish to run the samples as-is then you will need to register the applications with an existing Azure AD Tenent, or create a new one. This sample is based off one of the [Azure AD samples](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Call-OwnApi-Pop). Instructions on how to register apps are included in it's readme. +If you wish to run the samples as-is, you will need to register the applications with an existing Azure AD tenant or create a new one. This sample is based on one of the [Azure AD samples](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Call-OwnApi-Pop). Instructions on how to register apps are included in its README. ## Projects @@ -28,34 +28,34 @@ These projects were chosen so that there is minimal sample code that is not rela ## Managing Secrets -In any kind of scenario involving service to service authentication, you will invariably need to deal with some form of shared secret, be it a string or a client certificate. These should not be included in code, and at runtime come from some form of secure storage, and definetely should not included in source control for the world to see! .NET makes this easier to manage through the configuration API and built-in overlays: -- At design time, the [User Secrets](https://docs.microsoft.com/aspnet/core/security/app-secrets) feature will create a configuration overlay file that can be used to store secrets. In Visual Studio, right click on the project and choose *Manage User Secrets* to create and open the overlay file. Client Secrets should be stored in this file during development. -- At runtime, Environment Variables will overlay over config properties. Connection strings and secrets can be placed there and then accessed via configuration. Hosting environments have support for safely storing secrets and supplying them at runtime. Eg [App Service](https://docs.microsoft.com/azure/app-service/configure-common?tabs=portal#configure-app-settings) or [Azure Container Apps](https://docs.microsoft.com/azure/container-apps/manage-secrets?tabs=azure-cli) +In any kind of scenario involving service-to-service authentication, you will invariably need to deal with some form of shared secret, be it a string or a client certificate. These should **NOT** be included in code or be added to source control. At runtime, the secrets should come from some form of secure storage. .NET makes this easier to manage through the configuration API and built-in overlays: +- At design time, the [User Secrets](https://docs.microsoft.com/aspnet/core/security/app-secrets) feature will create a configuration overlay file that can be used to store secrets. In Visual Studio, right-click on the project and choose *Manage User Secrets* to create and open the overlay file. Client Secrets should be stored in this file during development. +- At runtime, environment variables will overlay config properties. Connection strings and secrets can be placed there and then accessed via configuration. Hosting environments have support for safely storing secrets and supplying them at runtime. For instance, here are instructions for doing this in [Azure App Service](https://docs.microsoft.com/azure/app-service/configure-common?tabs=portal#configure-app-settings) or [Azure Container Apps](https://docs.microsoft.com/azure/container-apps/manage-secrets?tabs=azure-cli) The appsettings.json files in the projects include the names of the config parameters that are required for the AAD authentication. They can be stored in User Secrets for additional security during development. # Using JWT with WCF Services -The WS-* specifications which define the SOAP protocol and form the basis for WCF were developed long before JWT came onto the scene as the preferred form of web authentication. For this reason the WCF client APIs don't include direct support for JWT-based authentication or Authorization. However, JWT is implemented over http by supplying the token as a base64 encoded string as the authenticate header. So these samples add that header and validate it as part of the service call. +The WS-* specifications which define the SOAP protocol and form the basis for WCF were developed long before JWT came onto the scene as the preferred form of web authentication. For this reason the WCF client APIs don't include direct support for JWT-based authentication or authorization. However, JWT is implemented over http by supplying the token as a base64-encoded string as the `Authorization` header. These samples add that header and validate it as part of the service call. ## Azure AD configuration -Within the Azure AD Tenent used for the authentication, the following need to be configured. See this [Readme](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Call-OwnApi-Pop) for instructions. +Within the Azure AD tenant used for authentication, the following need to be configured. See this [README](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Call-OwnApi-Pop) for instructions. - App registration for the Tile Service - With an App role of `AccessTileBag` - App registration of the Word Game app - With a client secret to identify the app - - Granted the API Permission of `AccessTileBag` for the Tile Service API + - Grant the API Permission of `AccessTileBag` for the Tile Service API -## Tile Service (Server app) +## TileService (Server app) TileService is a CoreWCF Server app. It supports one API defined in *ITileService.cs* for `DrawTiles`. The app uses the Azure AD integration with ASP.NET Core to enable and perform JWT Authentication. This is done through: -- Nuget references for `Microsoft.Identity.Web` and `Microsoft.VisualStudio.Azure.Containers.Tools.Targets` +- Adding Nuget references for `Microsoft.Identity.Web` and `Microsoft.VisualStudio.Azure.Containers.Tools.Targets` -- Including the ASP.NET Core and Azure AD SDK support for JWT with +- Including the ASP.NET Core and Azure AD SDK support for JWT with: ``` c# -// Asp.NET Core Authentication +// ASP.NET Core Authentication builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(builder.Configuration); ``` @@ -76,7 +76,7 @@ public IList DrawTiles(int count) ... } ``` -- The parameters for the Azure SDK to validate the JWT are provided through configuration in appsettings.json +- Configuring the parameters for the Azure SDK to validate the JWT in appsettings.json ``` json "AzureAd": { @@ -96,12 +96,12 @@ To make the WCF service calls, the app uses a Service Reference client wrapper, The key parts of the application are: - At startup, it calls `getAzAdJwtBlob`, which - - Reads AAD properties from configuration (appsetting.json) and User Secrets + - Reads AAD properties from configuration (appsettings.json) and User Secrets - Calls AAD to get a JWT for this specific service - Exposes a WebAPI at `/getTiles` that will make the WCF service call and return the response as JSON - Exposes a WebAPI at `/` which redirects to /getTiles with a count parameter - The `getTiles` function which - - Uses an `OperationContextScope` to add an http `Authorize` header with the JWT as the value + - Uses an `OperationContextScope` to add an http `Authorization` header with the JWT as the value - Calls the Tile Service API using the generated wrapper class -The only AAD specific code in this sample is included in the getAzAdJWTBlob function. The same pattern can be followed to retrieve a JWT from other authentication providers, or performing user authentication etc. \ No newline at end of file +The only AAD specific code in this sample is included in the getAzAdJWTBlob function. The same pattern can be followed to retrieve a JWT from other authentication providers, or for performing user authentication, etc. \ No newline at end of file From 68b722f8e6d2b2b8564141463343ebbb12462e33 Mon Sep 17 00:00:00 2001 From: Sam Spencer Date: Wed, 24 Aug 2022 17:15:30 -0700 Subject: [PATCH 4/5] Replaced per-message header code with an inspector helper class Other cleanup --- .../Readme.md | 2 +- .../TileService/ITileService.cs | 45 ++++---- .../TileService/Program.cs | 10 +- .../WordGame/HttpHeadersBehavior.cs | 108 ++++++++++++++++++ .../WordGame/Program.cs | 37 ++---- 5 files changed, 152 insertions(+), 50 deletions(-) create mode 100644 Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/HttpHeadersBehavior.cs diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md index 4e17fa3..b1a33fc 100644 --- a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/Readme.md @@ -6,7 +6,7 @@ This sample shows how a client can authenticate with a CoreWCF service using JWT ## What is a JWT -[JSON Web Token](https://en.wikipedia.org/wiki/JSON_Web_Token) is a way to support federated authentication and authorization of http requests. The client makes a call to an authentication server passing credentials and identifying information about the resource they wish to access. If authentication succeeds and the resource is authorized, they are handed back a signed token which includes information about the resource and rights. The client then passes that token to the service, which can then verify its integrity and trust the claims it specifies. +[JSON Web Token](https://en.wikipedia.org/wiki/JSON_Web_Token) ([rfc 7519](https://www.rfc-editor.org/rfc/rfc7519)) is a way to support federated authentication and authorization of http requests. The client makes a call to an authentication server passing credentials and identifying information about the resource they wish to access. If authentication succeeds and the resource is authorized, they are handed back a signed token which includes information about the resource and rights. The client then passes that token to the service, which can then verify its integrity and trust the claims it specifies. JWT is an open standard supported by multiple server stacks, clients, code languages and identity providers. diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/ITileService.cs b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/ITileService.cs index 7b2ac48..6f8ac12 100644 --- a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/ITileService.cs +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/ITileService.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Authorization; using Microsoft.Identity.Web.Resource; +using System.Web.Services.Description; namespace TileService { @@ -14,41 +15,45 @@ public interface ITileService IList DrawTiles(int count); } - public class TileBag : ITileService + public partial class TileBag : ITileService { - private static IList _gameTiles = initTileCollection(); - private static int TileCount; - private static Random rnd = new Random(); + private IList _gameTiles; + private int _tileCount; + private Random _rnd; + private ILogger _logger; + private bool _isDev = false; - // [AuthorizeRole("AccessTileBag")] - public IList DrawTiles(int count) + // Parameterized constructor will be called by Dependency Injection + public TileBag(ILogger logger, IWebHostEnvironment env) { + _logger = logger; + if (env.IsDevelopment()) _isDev = true; + _rnd = new Random(); + _gameTiles = initTileCollection(); + } + - // TODO: Remove this block - var ctxa = new HttpContextAccessor(); - var ctx = ctxa.HttpContext; - if (ctx == null) { Console.WriteLine("no http context"); } - else + [AuthorizeRole("AccessTileBag")] + + public IList DrawTiles(int count, [Injected] HttpContext ctx) + { + if (_isDev) { foreach (var claim in ctx.User.Claims) { - Console.WriteLine("***** BEGIN CLAIM *****"); - Console.WriteLine(claim.ToString()); - Console.WriteLine("***** END CLAIM *****"); + _logger.LogDebug("Claims {claim}", claim.ToString()); } - Console.WriteLine("***** BEGIN HEADERS *****"); foreach (var h in ctx.Request.Headers) { - Console.WriteLine(h.Key + ":" + h.Value); + _logger.LogDebug("Request Header {name}={value}", h.Key, h.Value); } - Console.WriteLine("***** END HEADERS *****"); } if (count > 0) { var tiles = new List(); for (var i = 0; i < count; i++) { - var index = rnd.Next(TileCount); + var index = _rnd.Next(_tileCount); var t_offset = 0; foreach (var t in _gameTiles) { @@ -66,7 +71,7 @@ public IList DrawTiles(int count) } - private static IList initTileCollection() + private IList initTileCollection() { var tiles = new List(); tiles.Add(new GameTile('A', 1, 9)); @@ -97,7 +102,7 @@ private static IList initTileCollection() tiles.Add(new GameTile('Z', 10, 1)); tiles.Add(new GameTile('\0', 0, 2)); - TileCount = (from t in tiles + _tileCount = (from t in tiles select t.Weight).Sum(); return tiles; } diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Program.cs b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Program.cs index 5cb80de..4cdfcfe 100644 --- a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Program.cs +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/TileService/Program.cs @@ -1,6 +1,7 @@ using Microsoft.Identity.Web; using System.IdentityModel.Tokens.Jwt; using Microsoft.AspNetCore.Authentication.JwtBearer; +using System.Web.Services.Description; var builder = WebApplication.CreateBuilder(); @@ -9,17 +10,18 @@ builder.Services.AddServiceModelMetadata(); builder.Services.AddSingleton(); -// Asp.NET Core Authentication +//Register the `TileBag` class with Dependency Injection, based on it being a single instance +builder.Services.AddSingleton(); + +// ASP.NET Core Authentication builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(builder.Configuration); -//TODO: Remove -builder.Services.AddHttpContextAccessor(); - var app = builder.Build(); if (app.Environment.IsDevelopment()) { + // Should be turned off for production, but will show user info as part of logging Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; } diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/HttpHeadersBehavior.cs b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/HttpHeadersBehavior.cs new file mode 100644 index 0000000..4168603 --- /dev/null +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/HttpHeadersBehavior.cs @@ -0,0 +1,108 @@ +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Description; +using System.ServiceModel.Dispatcher; + +namespace CoreWCF.Helpers +{ + public class HttpHeadersBehavior : IEndpointBehavior + { + private Dictionary _headers = new Dictionary(); + + public IDictionary Headers => _headers; + + + // IEndpointBehavior Members + public void AddBindingParameters(ServiceEndpoint serviceEndpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) + { + return; + } + + public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint, ClientRuntime behavior) + { + behavior.ClientMessageInspectors.Add(new HttpHeaderInjectionMessageInspector(_headers)); + } + + public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint, EndpointDispatcher endpointDispatcher) + { + return; + //endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new EndpointBehaviorMessageInspector()); + } + + public void Validate(ServiceEndpoint serviceEndpoint) + { + return; + } + + public class HttpHeaderInjectionMessageInspector : IClientMessageInspector + { + private IDictionary _headers; + + public HttpHeaderInjectionMessageInspector(IDictionary headers) + { + _headers = headers; + } + + public void AfterReceiveReply(ref Message reply, object correlationState) + { + } + + public object? BeforeSendRequest(ref Message request, IClientChannel channel) + { + HttpRequestMessageProperty? requestMessage; + if (request.Properties.ContainsKey(HttpRequestMessageProperty.Name)) + { + requestMessage = request.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty; + } + else + { + requestMessage = new HttpRequestMessageProperty(); + request.Properties[HttpRequestMessageProperty.Name] = requestMessage; + } + if (requestMessage != null) + { + foreach (var prop in _headers) + { + requestMessage.Headers[prop.Key] = prop.Value; + } + } + return null; + } + } + } + + public static class HttpHeadersBehaviorHelpers + { + public static void AddRequestHeader(this ClientBase client, string HeaderName, string HeaderValue) where T : class + { + var behavior = GetHttpHeadersBehavior(client); + behavior.Headers[HeaderName] = HeaderValue; + } + + public static void AddRequestHeaders(this ClientBase client, IDictionary headers) where T : class + { + var behavior = GetHttpHeadersBehavior(client); + foreach (var header in headers) + { + behavior.Headers[header.Key] = header.Value; + } + } + + private static HttpHeadersBehavior GetHttpHeadersBehavior(ClientBase client) where T : class + { + HttpHeadersBehavior behavior; + + if (client.Endpoint.EndpointBehaviors.Contains(typeof(HttpHeadersBehavior))) + { + behavior = (HttpHeadersBehavior)client.Endpoint.EndpointBehaviors[typeof(HttpHeadersBehavior)]; + } + else + { + behavior = new HttpHeadersBehavior(); + client.Endpoint.EndpointBehaviors.Add(behavior); + } + + return behavior; + } + } +} diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs index bde207f..7af8e84 100644 --- a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs @@ -1,52 +1,39 @@ +using CoreWCF.Helpers; using Microsoft.Identity.Client; using Microsoft.Identity.Web; using ServiceReference1; -using System.ServiceModel.Channels; -using System.ServiceModel; var builder = WebApplication.CreateBuilder(args); var tileServiceUrl = builder.Configuration["TileServiceUrl"]; -var tileService = TileServiceFactory(tileServiceUrl); //builder.Services.AddSingleton(TileServiceFactory); var jwt = await getAzAdJwtBlob(builder.Configuration); +var tileService = TileServiceFactory(tileServiceUrl, jwt); var app = builder.Build(); // Use Minimal WebApi Endpoint for the demo functionality app.Map("/", () => Results.Redirect("/getTiles?Count=5")); -app.MapGet("/getTiles", (int count) => getTiles(tileService, count, jwt)); +app.MapGet("/getTiles", (int count) => getTiles(tileService, count)); app.Run(); // Factory for TileService -TileServiceClient TileServiceFactory(string tileServiceUrl) +TileServiceClient TileServiceFactory(string tileServiceUrl, string jwt) { - return new TileServiceClient(TileServiceClient.EndpointConfiguration.BasicHttpBinding_ITileService, tileServiceUrl); -} + var client = new TileServiceClient(TileServiceClient.EndpointConfiguration.BasicHttpBinding_ITileService, tileServiceUrl); -//TileServiceClient TileServiceFactory(IServiceProvider provider) -//{ -// var config = provider.GetRequiredService(IConfiguration); -// var tileServiceUrl = config["TileServiceUrl"]; -// return new TileServiceClient(TileServiceClient.EndpointConfiguration.BasicHttpBinding_ITileService, tileServiceUrl); -//} + // Use a helper from HttpRequestBehaviors.cs to add a client behavior that adds http headers + // to outbound requests + client.AddRequestHeader("Authorization", $"Bearer {jwt}"); + return client; +} -async Task> getTiles(TileServiceClient tileService, int count, string jwt) +async Task> getTiles(TileServiceClient tileService, int count) { - // Create a scope for this call - using (new OperationContextScope((tileService).InnerChannel)) - { - // Add an http Authorization header with the JWT - HttpRequestMessageProperty requestMessage = new HttpRequestMessageProperty(); - requestMessage.Headers["Authorization"] = $"Bearer {jwt}"; - OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = requestMessage; - return await tileService.DrawTilesAsync(count); - } - + return await tileService.DrawTilesAsync(count); } - // Azure Active Directoy specific code to authenticate a daemon app with a service // Based on https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Call-OwnApi-Pop async Task getAzAdJwtBlob(ConfigurationManager config) From 57678d6d2ff3d9bc8c6efbffc58fa9e6979d0362 Mon Sep 17 00:00:00 2001 From: Sam Spencer Date: Thu, 25 Aug 2022 09:45:16 -0700 Subject: [PATCH 5/5] Switch to program being a class for state Adding a refresh timer --- .../WordGame/Program.cs | 127 ++++++++++-------- 1 file changed, 72 insertions(+), 55 deletions(-) diff --git a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs index 7af8e84..1aa3d9f 100644 --- a/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs +++ b/Scenarios/Authentication/Service-to-service-JWT-using-Azure-AD/WordGame/Program.cs @@ -1,74 +1,91 @@ -using CoreWCF.Helpers; using Microsoft.Identity.Client; using Microsoft.Identity.Web; using ServiceReference1; +using System.ServiceModel.Channels; +using System.ServiceModel; +using CoreWCF.Helpers; -var builder = WebApplication.CreateBuilder(args); - -var tileServiceUrl = builder.Configuration["TileServiceUrl"]; - -//builder.Services.AddSingleton(TileServiceFactory); -var jwt = await getAzAdJwtBlob(builder.Configuration); -var tileService = TileServiceFactory(tileServiceUrl, jwt); -var app = builder.Build(); - -// Use Minimal WebApi Endpoint for the demo functionality -app.Map("/", () => Results.Redirect("/getTiles?Count=5")); -app.MapGet("/getTiles", (int count) => getTiles(tileService, count)); - -app.Run(); +await new Program().Run(args); -// Factory for TileService -TileServiceClient TileServiceFactory(string tileServiceUrl, string jwt) +partial class Program { - var client = new TileServiceClient(TileServiceClient.EndpointConfiguration.BasicHttpBinding_ITileService, tileServiceUrl); - - // Use a helper from HttpRequestBehaviors.cs to add a client behavior that adds http headers - // to outbound requests - client.AddRequestHeader("Authorization", $"Bearer {jwt}"); - return client; -} + private Timer? _jwtRefreshTimer; + private IConfiguration? _configuration; + private TileServiceClient? _tileService; -async Task> getTiles(TileServiceClient tileService, int count) -{ - return await tileService.DrawTilesAsync(count); -} + async Task Run(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + _configuration = builder.Configuration; + var app = builder.Build(); -// Azure Active Directoy specific code to authenticate a daemon app with a service -// Based on https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Call-OwnApi-Pop -async Task getAzAdJwtBlob(ConfigurationManager config) -{ - // Fetch the auth details from configuration or the environment for secrets - var adTenant = config["AzureAdTenant"]; - var clientId = config["ClientId"]; - var clientSecret = config["ClientSecret"]; - var tileServiceScope = config["TileServiceScope"]; + var tileServiceUrl = _configuration["TileServiceUrl"]; + _tileService = new TileServiceClient(TileServiceClient.EndpointConfiguration.BasicHttpBinding_ITileService, tileServiceUrl); - var authority = new Uri($"https://login.microsoftonline.com/{adTenant}"); - var scopes = new string[] { tileServiceScope }; + await SetupJWTRefresh(); - var app = ConfidentialClientApplicationBuilder.Create(clientId) - .WithClientSecret(clientSecret) - .WithAuthority(authority) - .Build(); + // Use Minimal WebApi Endpoint for the demo functionality + app.Map("/", () => Results.Redirect("/getTiles?Count=5")); + app.MapGet("/getTiles", (int count) => getTiles(count)); - app.AddInMemoryTokenCache(); + app.Run(); + } - AuthenticationResult result; - try + async Task> getTiles(int count) { - result = await app.AcquireTokenForClient(scopes) - .ExecuteAsync(); + return await _tileService.DrawTilesAsync(count); } - catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011")) + + // Fetch the jwt, configure the header, and setup a timer to refresh the token + async Task SetupJWTRefresh() { - // Invalid scope. The scope has to be of the form "https://resourceurl/.default" - // Mitigation: change the scope to be as expected + // Fetch the jwt from AAD + var _jwt = await getAzAdJwt(_configuration); + + // Set the WCF Client to include the jwt as the `Authorization` header + _tileService.AddRequestHeader("Authorization", $"Bearer {_jwt.AccessToken}"); - // Rethrow so we can see it in the application logs - throw ex; + // JWT's have a timeout. Setup a timer 5 mins earlier to fetch a new token + // AAD service-to-service jwt don't have a refresh token, so fetch again based on client secret + var timeout = (_jwt.ExpiresOn - DateTimeOffset.Now).Subtract(TimeSpan.FromMinutes(5)); + _jwtRefreshTimer = new Timer((x) => _ = SetupJWTRefresh(), null, timeout, Timeout.InfiniteTimeSpan); } - return result.AccessToken; -} + // Azure Active Directoy specific code to authenticate a daemon app with a service + // Based on https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Call-OwnApi-Pop + async Task getAzAdJwt(IConfiguration config) + { + // Fetch the auth details from configuration or the environment for secrets + var adTenant = config["AzureAdTenant"]; + var clientId = config["ClientId"]; + var clientSecret = config["ClientSecret"]; + var tileServiceScope = config["TileServiceScope"]; + + var authority = new Uri($"https://login.microsoftonline.com/{adTenant}"); + var scopes = new string[] { tileServiceScope }; + + var app = ConfidentialClientApplicationBuilder.Create(clientId) + .WithClientSecret(clientSecret) + .WithAuthority(authority) + .Build(); + app.AddInMemoryTokenCache(); + + AuthenticationResult result; + try + { + result = await app.AcquireTokenForClient(scopes) + .ExecuteAsync(); + } + catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011")) + { + // Invalid scope. The scope has to be of the form "https://resourceurl/.default" + // Mitigation: change the scope to be as expected + + // Rethrow so we can see it in the application logs + throw ex; + } + + return result; + } +}