From 7e5ca05a6bd40592cd0d3c25b84299658aec0206 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Thu, 30 May 2019 11:26:26 -0700 Subject: [PATCH] create ConfigurationManager for instance, use internal refresh (#35) create TokenHandler for instance remove unused code use current code styling --- NuGet.Config | 6 ++ TodoListService-ManualJwt/Global.asax.cs | 129 +++++++++-------------- 2 files changed, 53 insertions(+), 82 deletions(-) create mode 100644 NuGet.Config diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..38d5dfb --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + diff --git a/TodoListService-ManualJwt/Global.asax.cs b/TodoListService-ManualJwt/Global.asax.cs index d652dd5..0b11a30 100644 --- a/TodoListService-ManualJwt/Global.asax.cs +++ b/TodoListService-ManualJwt/Global.asax.cs @@ -14,17 +14,11 @@ // limitations under the License. //---------------------------------------------------------------------------------------------- -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.Net; - -// The following using statements were added for this sample. using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; @@ -35,6 +29,9 @@ using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; namespace TodoListService_ManualJwt { @@ -57,126 +54,94 @@ 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 the service expects to see in tokens that are addressed to it. + // 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 static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"]; - private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"]; - private static string audience = ConfigurationManager.AppSettings["ida:Audience"]; - private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"]; - private string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant); - - private static string _issuer = string.Empty; - private static ICollection _signingKeys = null; - private static DateTime _stsMetadataRetrievalTime = DateTime.MinValue; - private static string scopeClaimType = "http://schemas.microsoft.com/identity/claims/scope"; - - // - // SendAsync checks that incoming requests have a valid access token, and sets the current user identity using that access token. - // + private string _audience; + private string _authority; + private string _clientId; + private ConfigurationManager _configManager; + private const string _scopeClaimType = "http://schemas.microsoft.com/identity/claims/scope"; + private ISecurityTokenValidator _tokenValidator; + + public TokenValidationHandler() + { + _audience = ConfigurationManager.AppSettings["ida:Audience"]; + _clientId = ConfigurationManager.AppSettings["ida:ClientId"]; + var aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"]; + var tenant = ConfigurationManager.AppSettings["ida:Tenant"]; + _authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); + _configManager = new ConfigurationManager($"{_authority}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever()); + _tokenValidator = new JwtSecurityTokenHandler(); + } + + /// + /// Checks that incoming requests have a valid access token, and sets the current user identity using that access token. + /// + /// the current . + /// a set by application. + /// A . protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - // Get the jwt bearer token from the authorization header - string jwtToken = null; - AuthenticationHeaderValue authHeader = request.Headers.Authorization; - if (authHeader != null) - { - jwtToken = authHeader.Parameter; - } - - if (jwtToken == null) - { - HttpResponseMessage response = this.BuildResponseErrorMessage(HttpStatusCode.Unauthorized); - return response; - } - - string issuer; - ICollection signingKeys; + // check 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); + OpenIdConnectConfiguration config = null; try { - // The issuer and signingKeys are cached for 24 hours. They are updated if any of the conditions in the if condition is true. - if (DateTime.UtcNow.Subtract(_stsMetadataRetrievalTime).TotalHours > 24 - || string.IsNullOrEmpty(_issuer) - || _signingKeys == null) - { - // Get tenant information that's used to validate incoming jwt tokens - string stsDiscoveryEndpoint = $"{this.authority}/.well-known/openid-configuration"; - var configManager = new ConfigurationManager(stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever()); - var config = await configManager.GetConfigurationAsync(cancellationToken); - _issuer = config.Issuer; - _signingKeys = config.SigningKeys; - - _stsMetadataRetrievalTime = DateTime.UtcNow; - } - - issuer = _issuer; - signingKeys = _signingKeys; + config = await _configManager.GetConfigurationAsync(cancellationToken).ConfigureAwait(false); } catch (Exception) { return new HttpResponseMessage(HttpStatusCode.InternalServerError); } - JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); - TokenValidationParameters validationParameters = new TokenValidationParameters { - // We accept both the App Id URI and the AppId of this service application - ValidAudiences = new[] { audience, clientId }, - - // Supports both the Azure AD V1 and V2 endpoint - ValidIssuers = new[] { issuer, $"{issuer}/v2.0" }, - IssuerSigningKeys = signingKeys + // 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 = new[] { config.Issuer, $"{config.Issuer}/v2.0" }, + IssuerSigningKeys = config.SigningKeys }; try - { + { // Validate token. - SecurityToken validatedToken = new JwtSecurityToken(); - ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken, validationParameters, out validatedToken); + var claimsPrincipal = _tokenValidator.ValidateToken(request.Headers.Authorization.Parameter, validationParameters, out SecurityToken _); // 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. - if (ClaimsPrincipal.Current.FindFirst(scopeClaimType) != null && ClaimsPrincipal.Current.FindFirst(scopeClaimType).Value != "user_impersonation") - { - HttpResponseMessage response = this.BuildResponseErrorMessage(HttpStatusCode.Forbidden); - return response; - } + if (ClaimsPrincipal.Current.FindFirst(_scopeClaimType) != null && ClaimsPrincipal.Current.FindFirst(_scopeClaimType).Value != "user_impersonation") + return BuildResponseErrorMessage(HttpStatusCode.Forbidden); return await base.SendAsync(request, cancellationToken); } catch (SecurityTokenValidationException) { - HttpResponseMessage response = this.BuildResponseErrorMessage(HttpStatusCode.Unauthorized); - return response; + return BuildResponseErrorMessage(HttpStatusCode.Unauthorized); } catch (Exception) { return new HttpResponseMessage(HttpStatusCode.InternalServerError); } - } + } private HttpResponseMessage BuildResponseErrorMessage(HttpStatusCode statusCode) { - HttpResponseMessage response = new HttpResponseMessage(statusCode); + 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. - // - AuthenticationHeaderValue authenticateHeader = new AuthenticationHeaderValue("Bearer", "authorization_uri=\"" + this.authority + "\"" + "," + "resource_id=" + audience); - + var authenticateHeader = new AuthenticationHeaderValue("Bearer", "authorization_uri=\"" + _authority + "\"" + "," + "resource_id=" + _audience); response.Headers.WwwAuthenticate.Add(authenticateHeader); - return response; } } -} \ No newline at end of file +}