Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: include links to docs for things like AuthorizeRole


``` c#
[AuthorizeRole("AccessTileBag")]
public IList<GameTile> 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.
Original file line number Diff line number Diff line change
@@ -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\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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -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<GameTile> DrawTiles(int count);
}

public class TileBag : ITileService
{
private static IList<GameTile> _gameTiles = initTileCollection();
private static int TileCount;
private static Random rnd = new Random();

// [AuthorizeRole("AccessTileBag")]
public IList<GameTile> 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<GameTile>();
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<GameTile> initTileCollection()
{
var tiles = new List<GameTile>();
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<IServiceBehavior, UseRequestHeadersForMetadataAddressBehavior>();

// 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<TileBag>();
serviceBuilder.AddServiceEndpoint<TileBag, ITileService>(new BasicHttpBinding(BasicHttpSecurityMode.Transport), "https://host/TileService");
var serviceMetadataBehavior = app.Services.GetRequiredService<ServiceMetadataBehavior>();
serviceMetadataBehavior.HttpsGetEnabled = true;
});

app.MapGet("/", () => "Error incorrect path: Use /TileService to access the service");

app.Run();
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading