-
Notifications
You must be signed in to change notification settings - Fork 65
/
Global.asax.cs
232 lines (202 loc) · 11.4 KB
/
Global.asax.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
/*
The MIT License (MIT)
Copyright (c) 2018 Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
namespace TodoListService_ManualJwt
{
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());
}
}
/// <summary>This Message handler inspects incoming access tokens and validates them.</summary>
/// <seealso cref="System.Net.Http.DelegatingHandler" />
internal class TokenValidationHandler : DelegatingHandler
{
//
// The AAD Instance is the instance of Azure, for example public Azure or Azure China.
// The Tenant is the name of the tenant in which this application is registered.
// The Authority is the sign-in URL of the tenant.
// 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.
private string _audience = ConfigurationManager.AppSettings["ida:Audience"];
private string _clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private string _tenant = ConfigurationManager.AppSettings["ida:TenantId"];
private string _authority;
private ConfigurationManager<OpenIdConnectConfiguration> _configManager;
public TokenValidationHandler()
{
_authority = string.Format(CultureInfo.InvariantCulture, ConfigurationManager.AppSettings["ida:AADInstance"], _tenant);
// 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
_configManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{_authority}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
}
/// <summary>
/// Checks that incoming requests have a valid access token, and sets the current user identity using that access token.
/// </summary>
/// <param name="request">the current <see cref="HttpRequestMessage"/>.</param>
/// <param name="cancellationToken">a <see cref="CancellationToken"/> set by application.</param>
/// <returns>A <see cref="HttpResponseMessage"/>.</returns>
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// For debugging/development purposes, one can enable additional detail in exceptions by setting IdentityModelEventSource.ShowPII to true.
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
// check if there is a jwt in the authorization header, return 'Unauthorized' error if the token is null.
if (request.Headers.Authorization == null || request.Headers.Authorization.Parameter == null)
return BuildResponseErrorMessage(HttpStatusCode.Unauthorized);
// Pull OIDC discovery document from Azure AD. For example, the tenant-independent version of the document is located
// at https://login.microsoftonline.com/common/.well-known/openid-configuration.
OpenIdConnectConfiguration config = null;
try
{
config = await _configManager.GetConfigurationAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
#if DEBUG
return BuildResponseErrorMessage(HttpStatusCode.InternalServerError, ex.Message);
#else
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
#endif
}
// You can get a list of issuers for the various Azure AD deployments (global & sovereign) from the following endpoint
//https://login.microsoftonline.com/common/discovery/instance?authorization_endpoint=https://login.microsoftonline.com/common/oauth2/v2.0/authorize&api-version=1.1;
IList<string> validissuers = new List<string>()
{
$"https://login.microsoftonline.com/{_tenant}/",
$"https://login.microsoftonline.com/{_tenant}/v2.0",
$"https://login.windows.net/{_tenant}/",
$"https://login.microsoft.com/{_tenant}/",
$"https://sts.windows.net/{_tenant}/"
};
// Initialize the token validation parameters
TokenValidationParameters validationParameters = GetTokenValidationParameters(config, validissuers);
try
{
if (string.IsNullOrWhiteSpace(request.Headers.Authorization.Parameter))
{
#if DEBUG
return BuildResponseErrorMessage(HttpStatusCode.Unauthorized, "No token provided in the 'Authorization' header");
#else
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
#endif
}
string jwtToken = request.Headers.Authorization.Parameter;
JsonWebTokenHandler tokenHandler = new JsonWebTokenHandler();
TokenValidationResult result = tokenHandler.ValidateToken(jwtToken, validationParameters);
// 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)
if (result.Exception != null && result.Exception is SecurityTokenSignatureKeyNotFoundException)
{
_configManager.RequestRefresh();
config = await _configManager.GetConfigurationAsync().ConfigureAwait(false);
validationParameters = GetTokenValidationParameters(config, validissuers);
// attempt to validate token again after refresh
result = tokenHandler.ValidateToken(jwtToken, validationParameters);
}
// Create a claims principal
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(result.ClaimsIdentity);
#pragma warning disable 1998
// This check is required to ensure that the Web API only accepts tokens from tenants where it has been consented to and provisioned.
if (!claimsPrincipal.Claims.Any(x => x.Type == ClaimConstants.ScopeClaimType)
&& !claimsPrincipal.Claims.Any(y => y.Type == ClaimConstants.ScpClaimType)
&& !claimsPrincipal.Claims.Any(y => y.Type == ClaimConstants.RolesClaimType))
{
#if DEBUG
return BuildResponseErrorMessage(HttpStatusCode.Forbidden, "Neither 'scope' or 'roles' claim was found in the bearer token.");
#else
return BuildResponseErrorMessage(HttpStatusCode.Forbidden);
#endif
}
#pragma warning restore 1998
// Set the ClaimsPrincipal on the current thread.
Thread.CurrentPrincipal = claimsPrincipal;
// Set the ClaimsPrincipal on HttpContext.Current if the app is running in web hosted environment.
if (HttpContext.Current != null)
HttpContext.Current.User = claimsPrincipal;
// If the token is scoped, verify that required permission is set in the scope claim.
// This could be done later at the controller level as well
return ClaimsPrincipal.Current.FindFirst(ClaimConstants.ScpClaimType).Value != ClaimConstants.ScopeClaimValue
? BuildResponseErrorMessage(HttpStatusCode.Forbidden)
: await base.SendAsync(request, cancellationToken);
}
catch (SecurityTokenValidationException stex)
{
#if DEBUG
return BuildResponseErrorMessage(HttpStatusCode.Unauthorized, stex.Message);
#else
return BuildResponseErrorMessage(HttpStatusCode.Unauthorized);
#endif
}
catch (Exception ex)
{
#if DEBUG
return BuildResponseErrorMessage(HttpStatusCode.InternalServerError, ex.Message);
#else
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
#endif
}
}
private TokenValidationParameters GetTokenValidationParameters(OpenIdConnectConfiguration config, IList<string> validissuers)
{
TokenValidationParameters validationParameters = new TokenValidationParameters
{
// App Id URI and AppId of this service application are both valid audiences.
ValidAudiences = new[] { _audience, _clientId },
// Support Azure AD V1 and V2 endpoints.
ValidIssuers = validissuers,
IssuerSigningKeys = config.SigningKeys
// Please inspect TokenValidationParameters class for a lot more validation parameters.
};
return validationParameters;
}
private HttpResponseMessage BuildResponseErrorMessage(HttpStatusCode statusCode, string error_description = "")
{
var response = new HttpResponseMessage(statusCode);
// The Scheme should be "Bearer", authorization_uri should point to the tenant url and resource_id should point to the audience.
response.Headers.WwwAuthenticate.Add(
new AuthenticationHeaderValue("Bearer", "authorization_uri=\"" + _authority + "\"" + "," + "resource_id=" + _audience + $",error_description={error_description}"));
return response;
}
}
}