Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Service to Service Auth using JWT #21

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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. 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

[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.

## 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 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, 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

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 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 `Authorization` header. These samples add that header and validate it as part of the service call.
Copy link
Collaborator

Choose a reason for hiding this comment

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

http header is: Authorization: Bearer <access_token>


## Azure AD configuration
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
- Grant the API Permission of `AccessTileBag` for the Tile Service API

## 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:
- 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:

``` 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)
{
...
}
```
- Configuring the parameters for the Azure SDK to validate the JWT 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 (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 `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 for 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,128 @@
using CoreWCF;
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Identity.Web.Resource;
using System.Web.Services.Description;

namespace TileService
{
[ServiceContract]
public interface ITileService
{
[OperationContract]
IList<GameTile> DrawTiles(int count);
}

public partial class TileBag : ITileService
{
private IList<GameTile> _gameTiles;
private int _tileCount;
private Random _rnd;
private ILogger<TileBag> _logger;
private bool _isDev = false;

// Parameterized constructor will be called by Dependency Injection
public TileBag(ILogger<TileBag> logger, IWebHostEnvironment env)
{
_logger = logger;
if (env.IsDevelopment()) _isDev = true;
_rnd = new Random();
_gameTiles = initTileCollection();
}


[AuthorizeRole("AccessTileBag")]

public IList<GameTile> DrawTiles(int count, [Injected] HttpContext ctx)
{
if (_isDev)
{
foreach (var claim in ctx.User.Claims)
{
_logger.LogDebug("Claims {claim}", claim.ToString());
}
foreach (var h in ctx.Request.Headers)
{
_logger.LogDebug("Request Header {name}={value}", h.Key, h.Value);
}
}
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 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,41 @@
using Microsoft.Identity.Web;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Web.Services.Description;

var builder = WebApplication.CreateBuilder();

// CoreWCF Services
builder.Services.AddServiceModelServices();
builder.Services.AddServiceModelMetadata();
builder.Services.AddSingleton<IServiceBehavior, UseRequestHeadersForMetadataAddressBehavior>();

//Register the `TileBag` class with Dependency Injection, based on it being a single instance
builder.Services.AddSingleton<TileBag>();

// ASP.NET Core Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration);

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;
samsp-msft marked this conversation as resolved.
Show resolved Hide resolved
}

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