Skip to content

Commit 85063f1

Browse files
author
Kalyan Krishna
committed
CustomHandler added and described
1 parent 9d2203f commit 85063f1

File tree

8 files changed

+165
-30
lines changed

8 files changed

+165
-30
lines changed

README.md

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,22 @@ endpoint: AAD v2.0
2727
- [Validating the claims](#validating-the-claims)
2828
- [Prerequisites](#prerequisites)
2929
- [Setup](#setup)
30+
- [Step 1: Clone or download this repository](#step-1--clone-or-download-this-repository)
31+
- [Register the sample application(s) with your Azure Active Directory tenant](#register-the-sample-applications-with-your-azure-active-directory-tenant)
32+
- [Choose the Azure AD tenant where you want to create your applications](#choose-the-azure-ad-tenant-where-you-want-to-create-your-applications)
33+
- [Register the service app (TodoListService-ManualJwt)](#register-the-service-app-todolistservice-manualjwt)
34+
- [Register the client app (TodoListClient-ManualJwt)](#register-the-client-app-todolistclient-manualjwt)
35+
- [Running the sample](#running-the-sample)
3036
- [Explore the sample](#explore-the-sample)
3137
- [About The Code](#about-the-code)
38+
- [Providing your own Custom token validation handler](#providing-your-own-custom-token-validation-handler)
3239
- [How To Recreate This Sample](#how-to-recreate-this-sample)
3340
- [Creating the TodoListService-ManualJwt Project](#creating-the-todolistservice-manualjwt-project)
3441
- [Creating the TodoListClient Project](#creating-the-todolistclient-project)
3542
- [How to deploy this sample to Azure](#how-to-deploy-this-sample-to-azure)
43+
- [Create and publish the `TodoListService-ManualJwt` to an Azure Web Site](#create-and-publish-the-todolistservice-manualjwt-to-an-azure-web-site)
44+
- [Update the Active Directory tenant application registration for `TodoListService-ManualJwt`](#update-the-active-directory-tenant-application-registration-for-todolistservice-manualjwt)
45+
- [Update the `TodoListClient-ManualJwt` to call the `TodoListService-ManualJwt` Running in Azure Web Sites](#update-the-todolistclient-manualjwt-to-call-the-todolistservice-manualjwt-running-in-azure-web-sites)
3646
- [Azure Government Deviations](#azure-government-deviations)
3747
- [Troubleshooting](#troubleshooting)
3848
- [Community Help and Support](#community-help-and-support)
@@ -257,24 +267,79 @@ Explore the sample by signing in, adding items to the To Do list, removing the u
257267
258268
## About The Code
259269

260-
The manual JWT validation occurs in the [TokenValidationHandler](https://github.com/Azure-Samples/active-directory-dotnet-webapi-manual-jwt-validation/blob/master/TodoListService-ManualJwt/Global.asax.cs#L58) implementation in the `Global.aspx.cs` file in the TodoListService-ManualJwt project. Each time a call is made to the web API, the [TokenValidationHandler.SendAsync()](https://github.com/Azure-Samples/active-directory-dotnet-webapi-manual-jwt-validation/blob/4b80657c5506c8cb30af67b9f61bb6aa68dfca58/TodoListService-ManualJwt/Global.asax.cs#L80) handler is executed:
270+
The manual JWT validation occurs in the [TokenValidationHandler](https://github.com/Azure-Samples/active-directory-dotnet-webapi-manual-jwt-validation/blob/master/TodoListService-ManualJwt/Global.asax.cs#L58) implementation in the `Global.aspx.cs` file in the TodoListService-ManualJwt project.
271+
Each time a call is made to the web API, the [TokenValidationHandler.SendAsync()](https://github.com/Azure-Samples/active-directory-dotnet-webapi-manual-jwt-validation/blob/4b80657c5506c8cb30af67b9f61bb6aa68dfca58/TodoListService-ManualJwt/Global.asax.cs#L80) handler is executed:
261272

262273
This method:
263274

264-
1. gets the token from the Authorization headers
265-
1. gets the open ID configuration from the Azure AD discovery endpoint
266-
1. ensures that the web API is consented to and provisioned in the Azure AD tenant from where the access token originated
267-
1. verifies that the token has not expired
268-
1. sets the parameters to validate:
275+
1. Gets the token from the Authorization headers
276+
1. Gets the open ID configuration, including keys from the Azure AD discovery endpoint
277+
1. Sets the parameters to validate in `GetTokenValidationParameters()`
269278
- the audience - the application accepts both its App ID URI and its AppID/clientID
270279
- the valid issuers - the application accepts both Azure AD V1 and Azure AD V2
271-
272-
1. then it delegates to the `JwtSecurityTokenHandler` class (provided by the `Microsoft.IdentityModel.Tokens` library)
280+
1. Then the token is validated
281+
1. An asp.net claims principal is created after a successful validation
282+
1. ensures that the web API is consented to and provisioned in the Azure AD tenant from where the access token originated
283+
1. Finally, a check for scopes that the web API expects from the caller is carried out
273284

274285
The `TokenValidationHandler` class is registered with ASP.NET in the `TodoListService-ManualJwt/Global.asx.cs` file, in the [Application_Start()](https://github.com/Azure-Samples/active-directory-dotnet-webapi-manual-jwt-validation/blob/4b80657c5506c8cb30af67b9f61bb6aa68dfca58/TodoListService-ManualJwt/Global.asax.cs#L54) method.
275286

276287
For more validation options, please refer to [TokenValidationParameters.cs](https://docs.microsoft.com/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters?view=azure-dotnet)
277288

289+
### Providing your own Custom token validation handler
290+
291+
If you do not wish to control the token validation from its very beginning to the end as laid out in the `Global.asax.cs`, but only limit yourself to validate business logic based on claims in the presented token, you can craft a Custom token handler as provided in the example below.
292+
The provided example, validates to allow callers from a list of whitelisted tenants only.
293+
294+
1. Create a custom handler implementation
295+
296+
```CSharp
297+
298+
public class CustomTokenHandler : JwtSecurityTokenHandler
299+
{
300+
public override ClaimsPrincipal ValidateToken(
301+
string token, TokenValidationParameters validationParameters,
302+
out SecurityToken validatedToken)
303+
{
304+
try
305+
{
306+
var claimsPrincipal = base.ValidateToken(token, validationParameters, out validatedToken);
307+
308+
string[] allowedTenants = { "14c2f153-90a7-4689-9db7-9543bf084dad", "af8cc1a0-d2aa-4ca7-b829-00d361edb652", "979f4440-75dc-4664-b2e1-2cafa0ac67d1" };
309+
string tenantId = claimsPrincipal.Claims.FirstOrDefault(x => x.Type == "tid" || x.Type == "http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
310+
311+
if (!allowedTenants.Contains(tenantId))
312+
{
313+
throw new Exception("This tenant is not authorized");
314+
}
315+
316+
return claimsPrincipal;
317+
}
318+
catch (Exception e)
319+
{
320+
throw;
321+
}
322+
}
323+
}
324+
325+
```
326+
327+
1. Assign the custom handler in `Startup.Auth.cs`
328+
329+
```CSharp
330+
331+
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
332+
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
333+
{
334+
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
335+
TokenValidationParameters = new TokenValidationParameters
336+
{
337+
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
338+
},
339+
TokenHandler = new CustomTokenHandler()
340+
});
341+
```
342+
278343
## How To Recreate This Sample
279344

280345
First, in Visual Studio 2017 create an empty solution to host the projects. Then, follow these steps to create each project.
@@ -386,7 +451,7 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope
386451

387452
## More information
388453

389-
- [Microsoft identity platform (Azure Active Directory for developers)](https://docs.microsoft.com/en-us/azure/active-directory/develop/)
454+
- [Microsoft identity platform (Azure Active Directory for developers)](https://docs.microsoft.com/azure/active-directory/develop/)
390455
- [MSAL.NET's conceptual documentation](https://aka.ms/msal-net)
391456
- [Quickstart: Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app)
392457
- [Quickstart: Configure a client application to access web APIs](https://docs.microsoft.com/azure/active-directory/develop/quickstart-configure-app-access-web-apis)

TodoListClient/TodoListClient-ManualJwt.csproj

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@
6363
<Generator>MSBuild:Compile</Generator>
6464
<SubType>Designer</SubType>
6565
</ApplicationDefinition>
66+
<Compile Include="..\TodoListService-ManualJwt\Models\TodoItem.cs">
67+
<Link>TodoItem.cs</Link>
68+
</Compile>
6669
<Compile Include="FileCache.cs" />
6770
<Page Include="MainWindow.xaml">
6871
<Generator>MSBuild:Compile</Generator>
@@ -107,12 +110,6 @@
107110
<SubType>Designer</SubType>
108111
</None>
109112
</ItemGroup>
110-
<ItemGroup>
111-
<ProjectReference Include="..\Shared\Shared.csproj">
112-
<Project>{f4e8b876-762e-4eec-99bc-7ef12b01799d}</Project>
113-
<Name>Shared</Name>
114-
</ProjectReference>
115-
</ItemGroup>
116113
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
117114
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
118115
Other similar extension points exist, see Microsoft.Common.targets.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Web;
5+
6+
namespace TodoListService_ManualJwt.App_Start
7+
{
8+
public class Startup
9+
{
10+
}
11+
}

TodoListService-ManualJwt/Global.asax.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ internal class TokenValidationHandler : DelegatingHandler
7272
private string _audience = ConfigurationManager.AppSettings["ida:Audience"];
7373
private string _clientId = ConfigurationManager.AppSettings["ida:ClientId"];
7474
private string _tenant = ConfigurationManager.AppSettings["ida:TenantId"];
75-
private ISecurityTokenValidator _tokenValidator;
7675
private string _authority;
7776
private ConfigurationManager<OpenIdConnectConfiguration> _configManager;
7877

@@ -81,8 +80,6 @@ public TokenValidationHandler()
8180
_authority = string.Format(CultureInfo.InvariantCulture, ConfigurationManager.AppSettings["ida:AADInstance"], _tenant);
8281
// The ConfigurationManager class holds properties to control the metadata refresh interval. For more details, https://docs.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.protocols.configurationmanager-1?view=azure-dotnet
8382
_configManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{_authority}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
84-
85-
_tokenValidator = new JwtSecurityTokenHandler();
8683
}
8784

8885
/// <summary>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015 Microsoft Corporation
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
*/
24+
25+
namespace Shared.Models
26+
{
27+
public class TodoItem
28+
{
29+
public string Title { get; set; }
30+
public string Owner { get; set; }
31+
}
32+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Microsoft.IdentityModel.Tokens;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IdentityModel.Tokens.Jwt;
5+
using System.Linq;
6+
using System.Security.Claims;
7+
using System.Web;
8+
9+
namespace TodoListService_ManualJwt.Services
10+
{
11+
/// <summary>
12+
/// Custom token handler to apply custom logic to token validation
13+
/// </summary>
14+
/// <seealso cref="JwtSecurityTokenHandler" />
15+
public class CustomTokenHandler : JwtSecurityTokenHandler
16+
{
17+
public override ClaimsPrincipal ValidateToken(
18+
string token, TokenValidationParameters validationParameters,
19+
out SecurityToken validatedToken)
20+
{
21+
try
22+
{
23+
var claimsPrincipal = base.ValidateToken(token, validationParameters, out validatedToken);
24+
25+
// Custom token validation to allow callers from a list of whitelisted tenants
26+
string[] allowedTenants = { "14c2f153-90a7-4689-9db7-9543bf084dad", "af8cc1a0-d2aa-4ca7-b829-00d361edb652", "979f4440-75dc-4664-b2e1-2cafa0ac67d1", "4d39e77c-b0f3-4253-ae0b-7068ddd47949", "556b80b7-c9fc-41fd-92da-c3635f7918e5" };
27+
string tenantId = claimsPrincipal.Claims.FirstOrDefault(x => x.Type == "tid" || x.Type == "http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
28+
29+
if (!allowedTenants.Contains(tenantId))
30+
{
31+
throw new Exception("This tenant is not authorized to this web api");
32+
}
33+
34+
return claimsPrincipal;
35+
}
36+
catch (Exception e)
37+
{
38+
39+
throw;
40+
}
41+
}
42+
}
43+
}

TodoListService-ManualJwt/TodoListService-ManualJwt.csproj

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@
168168
<Compile Include="Global.asax.cs">
169169
<DependentUpon>Global.asax</DependentUpon>
170170
</Compile>
171+
<Compile Include="Models\TodoItem.cs" />
171172
<Compile Include="Properties\AssemblyInfo.cs" />
173+
<Compile Include="Services\CustomTokenHandler.cs" />
172174
<Compile Include="Utils\ClaimConstants.cs" />
173175
</ItemGroup>
174176
<ItemGroup>
@@ -239,12 +241,6 @@
239241
<Content Include="Views\Shared\_Layout.cshtml" />
240242
<Content Include="Scripts\jquery-2.1.1.min.map" />
241243
</ItemGroup>
242-
<ItemGroup>
243-
<ProjectReference Include="..\Shared\Shared.csproj">
244-
<Project>{F4E8B876-762E-4EEC-99BC-7EF12B01799D}</Project>
245-
<Name>Shared</Name>
246-
</ProjectReference>
247-
</ItemGroup>
248244
<ItemGroup>
249245
<None Include="Project_Readme.html" />
250246
</ItemGroup>

WebAPI-ManuallyValidateJwt-DotNet.sln

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoListClient-ManualJwt",
77
EndProject
88
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoListService-ManualJwt", "TodoListService-ManualJwt\TodoListService-ManualJwt.csproj", "{67104329-E1DA-4A37-A556-834684DBC409}"
99
EndProject
10-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared.csproj", "{F4E8B876-762E-4EEC-99BC-7EF12B01799D}"
11-
EndProject
1210
Global
1311
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1412
Debug|Any CPU = Debug|Any CPU
@@ -23,10 +21,6 @@ Global
2321
{67104329-E1DA-4A37-A556-834684DBC409}.Debug|Any CPU.Build.0 = Debug|Any CPU
2422
{67104329-E1DA-4A37-A556-834684DBC409}.Release|Any CPU.ActiveCfg = Release|Any CPU
2523
{67104329-E1DA-4A37-A556-834684DBC409}.Release|Any CPU.Build.0 = Release|Any CPU
26-
{F4E8B876-762E-4EEC-99BC-7EF12B01799D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27-
{F4E8B876-762E-4EEC-99BC-7EF12B01799D}.Debug|Any CPU.Build.0 = Debug|Any CPU
28-
{F4E8B876-762E-4EEC-99BC-7EF12B01799D}.Release|Any CPU.ActiveCfg = Release|Any CPU
29-
{F4E8B876-762E-4EEC-99BC-7EF12B01799D}.Release|Any CPU.Build.0 = Release|Any CPU
3024
EndGlobalSection
3125
GlobalSection(SolutionProperties) = preSolution
3226
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)