Skip to content

Commit 9d2203f

Browse files
author
Kalyan Krishna
committed
Metadata resiliency updates and minor edits
1 parent 31de333 commit 9d2203f

File tree

3 files changed

+93
-31
lines changed

3 files changed

+93
-31
lines changed

README.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,50 @@
11
---
2+
page_type: sample
3+
languages:
4+
- csharp
5+
products:
6+
- aspnet-core
7+
- microsoft-identity-web
8+
- azure-active-directory
9+
name: How to manually validate a JWT access token using the Microsoft identity platform
10+
urlFragment: active-directory-dotnet-webapi-manual-jwt-validation
211
services: active-directory
3-
platforms: dotnet
12+
platforms: dotnetcore
413
author: kalyankrishna1
514
level: 300
615
client: .NET Desktop App (WPF)
716
service: ASP.NET Web API
817
endpoint: AAD v2.0
918
---
1019

11-
# How to manually validate a JWT access token using the Microsoft identity platform (formerly Azure Active Directory for developers)
20+
# How to manually validate a JWT access token using the Microsoft identity platform
21+
22+
- [Overview](#overview)
23+
- [About this sample](#about-this-sample)
24+
- [Scenario: protecting a Web API - acquiring a token for the protected Web API](#scenario-protecting-a-web-api---acquiring-a-token-for-the-protected-web-api)
25+
- [Token Validation](#token-validation)
26+
- [What to validate?](#what-to-validate)
27+
- [Validating the claims](#validating-the-claims)
28+
- [Prerequisites](#prerequisites)
29+
- [Setup](#setup)
30+
- [Explore the sample](#explore-the-sample)
31+
- [About The Code](#about-the-code)
32+
- [How To Recreate This Sample](#how-to-recreate-this-sample)
33+
- [Creating the TodoListService-ManualJwt Project](#creating-the-todolistservice-manualjwt-project)
34+
- [Creating the TodoListClient Project](#creating-the-todolistclient-project)
35+
- [How to deploy this sample to Azure](#how-to-deploy-this-sample-to-azure)
36+
- [Azure Government Deviations](#azure-government-deviations)
37+
- [Troubleshooting](#troubleshooting)
38+
- [Community Help and Support](#community-help-and-support)
39+
- [Contributing](#contributing)
40+
- [More information](#more-information)
1241

1342
![Build badge](https://identitydivision.visualstudio.com/_apis/public/build/definitions/a7934fdd-dcde-4492-a406-7fad6ac00e17/18/badge)
1443

44+
## Overview
45+
46+
This sample demonstrates how to manually validate an access token issued to a web API protected by the Microsoft Identity Platform. Here a .NET Desktop App (WPF) calls a protected ASP.NET Web API that is secured using Azure AD.
47+
1548
## About this sample
1649

1750
A Web API that accepts bearer token as a proof of authentication is secured by [validating the token](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens#validating-tokens) they receive from the callers. When a developer generates a skeleton Web API code using [Visual Studio](https://aka.ms/vsdownload), token validation libraries and code to carry out basic token validation is automatically generated for the project. An example of the generated code using the [asp.net security middleware](https://github.com/aspnet/Security) and [Microsoft Identity Model Extension for .NET](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) to validate tokens is provided below.
@@ -205,7 +238,7 @@ Open the project in your IDE (like Visual Studio or Visual Studio Code) to confi
205238
1. Open the `TodoListClient\App.Config` file.
206239
1. Find the key `ida:Tenant` and replace the existing value with your Azure AD tenant name.
207240
1. Find the key `ida:ClientId` and replace the existing value with the application ID (clientId) of `TodoListClient-ManualJwt` app copied from the Azure portal.
208-
1. Find the key `todo:TodoListResourceId` and replace the value with the App ID URI you registered earlier, when exposing an API. For instance use `api://<application_id>`.
241+
1. Find the key `todo:TodoListResourceId` and replace the existing value with the App ID URI you registered earlier, when exposing an API. For instance use `api://<application_id>`.
209242
1. Find the key `todo:TodoListBaseAddress` and replace the existing value with the base address of `TodoListService-ManualJwt` (by default `https://localhost:44324`).
210243

211244
## Running the sample
@@ -214,9 +247,11 @@ Open the project in your IDE (like Visual Studio or Visual Studio Code) to confi
214247
>
215248
> Clean the solution, rebuild the solution, and run it. You might want to go into the solution properties and set both projects as startup projects, with the service project starting first.
216249
250+
## Explore the sample
251+
217252
Explore the sample by signing in, adding items to the To Do list, removing the user account, and starting again. Notice that if you stop the application without removing the user account, the next time you run the application you won't be prompted to sign in again - that is the sample implements a [persistent cache for MSAL](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/token-cache-serialization), and remembers the tokens from the previous run.
218253

219-
> Did the sample not work for you as expected? Did you encounter issues trying this sample? Then please reach out to us using the [GitHub Issues](../../issues) page.
254+
> :information_source: Did the sample not work for you as expected? Did you encounter issues trying this sample? Then please reach out to us using the [GitHub Issues](../../issues) page.
220255
221256
> [Consider taking a moment to share your experience with us.](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR73pcsbpbxNJuZCMKN0lURpUMjFRQjA0RElFUFNPV0dCUVBGQzk0QkhKTiQlQCN0PWcu)
222257

TodoListService-ManualJwt/Controllers/TodoListController.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public IEnumerable<TodoItem> Get()
4545
CheckExpectedClaim();
4646

4747
// A user's To Do list is keyed off of the NameIdentifier claim, which contains an immutable, unique identifier for the user.
48-
Claim subject = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier);
48+
Claim subject = ClaimsPrincipal.Current.FindFirst(ClaimConstants.Name);
4949

5050
return from todo in todoBag
5151
where todo.Owner == subject.Value
@@ -59,7 +59,7 @@ public void Post(TodoItem todo)
5959

6060
if (null != todo && !string.IsNullOrWhiteSpace(todo.Title))
6161
{
62-
todoBag.Add(new TodoItem { Title = todo.Title, Owner = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value });
62+
todoBag.Add(new TodoItem { Title = todo.Title, Owner = ClaimsPrincipal.Current.FindFirst(ClaimConstants.Name).Value });
6363
}
6464
}
6565

@@ -74,7 +74,7 @@ private void CheckExpectedClaim()
7474
// The Scope claim tells you what permissions the client application has in the service.
7575
// In this case we look for a scope value of access_as_user, or full access to the service as the user.
7676

77-
if (!ClaimsPrincipal.Current.HasClaim(ClaimConstants.ScopeClaimType, ClaimConstants.ScopeClaimValue))
77+
if (!ClaimsPrincipal.Current.HasClaim(ClaimConstants.ScpClaimType, ClaimConstants.ScopeClaimValue))
7878
{
7979
throw new HttpResponseException(
8080
new HttpResponseMessage {

TodoListService-ManualJwt/Global.asax.cs

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
SOFTWARE.
2323
*/
2424

25+
using Microsoft.IdentityModel.JsonWebTokens;
2526
using Microsoft.IdentityModel.Protocols;
2627
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
2728
using Microsoft.IdentityModel.Tokens;
@@ -68,15 +69,19 @@ internal class TokenValidationHandler : DelegatingHandler
6869
// The Authority is the sign-in URL of the tenant.
6970
// The Audience is the value of one of the 'aud' claims the service expects to find in token to assure the token is addressed to it.
7071

71-
private string _audience = ConfigurationManager.AppSettings["ida:Audience"];
72+
private string _audience = ConfigurationManager.AppSettings["ida:Audience"];
7273
private string _clientId = ConfigurationManager.AppSettings["ida:ClientId"];
7374
private string _tenant = ConfigurationManager.AppSettings["ida:TenantId"];
7475
private ISecurityTokenValidator _tokenValidator;
7576
private string _authority;
77+
private ConfigurationManager<OpenIdConnectConfiguration> _configManager;
7678

7779
public TokenValidationHandler()
7880
{
7981
_authority = string.Format(CultureInfo.InvariantCulture, ConfigurationManager.AppSettings["ida:AADInstance"], _tenant);
82+
// 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
83+
_configManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{_authority}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
84+
8085
_tokenValidator = new JwtSecurityTokenHandler();
8186
}
8287

@@ -100,7 +105,7 @@ protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage
100105
OpenIdConnectConfiguration config = null;
101106
try
102107
{
103-
config = await GetConfigurationManager().GetConfigurationAsync(cancellationToken).ConfigureAwait(false);
108+
config = await _configManager.GetConfigurationAsync(cancellationToken).ConfigureAwait(false);
104109
}
105110
catch (Exception ex)
106111
{
@@ -124,23 +129,37 @@ protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage
124129
};
125130

126131
// Initialize the token validation parameters
127-
TokenValidationParameters validationParameters = new TokenValidationParameters
132+
TokenValidationParameters validationParameters = GetTokenValidationParameters(config, validissuers);
133+
134+
try
128135
{
129-
// App Id URI and AppId of this service application are both valid audiences.
130-
ValidAudiences = new[] { _audience, _clientId },
136+
if (string.IsNullOrWhiteSpace(request.Headers.Authorization.Parameter))
137+
{
138+
#if DEBUG
139+
return BuildResponseErrorMessage(HttpStatusCode.Unauthorized, "No token provided in the 'Authorization' header");
140+
#else
141+
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
142+
#endif
143+
}
131144

132-
// Support Azure AD V1 and V2 endpoints.
133-
ValidIssuers = validissuers,
134-
IssuerSigningKeys = config.SigningKeys
145+
string jwtToken = request.Headers.Authorization.Parameter;
146+
JsonWebTokenHandler tokenHandler = new JsonWebTokenHandler();
147+
TokenValidationResult result = tokenHandler.ValidateToken(jwtToken, validationParameters);
135148

136-
// Please inspect TokenValidationParameters class for a lot more validation parameters.
137-
};
149+
// Refresh the metadata (cached keys) if the metadata refresh has invalidated the cache (https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/wiki/Resilience-on-metadata-refresh)
150+
if (result.Exception != null && result.Exception is SecurityTokenSignatureKeyNotFoundException)
151+
{
152+
_configManager.RequestRefresh();
153+
config = await _configManager.GetConfigurationAsync().ConfigureAwait(false);
154+
validationParameters = GetTokenValidationParameters(config, validissuers);
138155

139-
try
140-
{
141-
// Validate token.
142-
SecurityToken securityToken;
143-
var claimsPrincipal = _tokenValidator.ValidateToken(request.Headers.Authorization.Parameter, validationParameters, out securityToken);
156+
// attempt to validate token again after refresh
157+
result = tokenHandler.ValidateToken(jwtToken, validationParameters);
158+
}
159+
160+
161+
// Create a claims principal
162+
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(result.ClaimsIdentity);
144163

145164
#pragma warning disable 1998
146165
// This check is required to ensure that the Web API only accepts tokens from tenants where it has been consented to and provisioned.
@@ -165,7 +184,7 @@ protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage
165184

166185
// If the token is scoped, verify that required permission is set in the scope claim.
167186
// This could be done later at the controller level as well
168-
return ClaimsPrincipal.Current.FindFirst(ClaimConstants.ScopeClaimType).Value != ClaimConstants.ScopeClaimValue
187+
return ClaimsPrincipal.Current.FindFirst(ClaimConstants.ScpClaimType).Value != ClaimConstants.ScopeClaimValue
169188
? BuildResponseErrorMessage(HttpStatusCode.Forbidden)
170189
: await base.SendAsync(request, cancellationToken);
171190
}
@@ -187,6 +206,22 @@ protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage
187206
}
188207
}
189208

209+
private TokenValidationParameters GetTokenValidationParameters(OpenIdConnectConfiguration config, IList<string> validissuers)
210+
{
211+
TokenValidationParameters validationParameters = new TokenValidationParameters
212+
{
213+
// App Id URI and AppId of this service application are both valid audiences.
214+
ValidAudiences = new[] { _audience, _clientId },
215+
216+
// Support Azure AD V1 and V2 endpoints.
217+
ValidIssuers = validissuers,
218+
IssuerSigningKeys = config.SigningKeys
219+
220+
// Please inspect TokenValidationParameters class for a lot more validation parameters.
221+
};
222+
return validationParameters;
223+
}
224+
190225
private HttpResponseMessage BuildResponseErrorMessage(HttpStatusCode statusCode, string error_description = "")
191226
{
192227
var response = new HttpResponseMessage(statusCode);
@@ -196,13 +231,5 @@ private HttpResponseMessage BuildResponseErrorMessage(HttpStatusCode statusCode,
196231
new AuthenticationHeaderValue("Bearer", "authorization_uri=\"" + _authority + "\"" + "," + "resource_id=" + _audience + $",error_description={error_description}"));
197232
return response;
198233
}
199-
200-
/// <summary>
201-
/// The ConfigurationManager class holds properties to control the metadata refresh interval.
202-
/// For more details, https://docs.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.protocols.configurationmanager-1?view=azure-dotnet
203-
/// </summary>
204-
/// <returns></returns>
205-
private ConfigurationManager<OpenIdConnectConfiguration> GetConfigurationManager() =>
206-
new ConfigurationManager<OpenIdConnectConfiguration>($"{_authority}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
207234
}
208235
}

0 commit comments

Comments
 (0)