1414// limitations under the License.
1515//----------------------------------------------------------------------------------------------
1616
17- using Microsoft . IdentityModel . Protocols ;
18- using Microsoft . IdentityModel . Protocols . OpenIdConnect ;
19- using Microsoft . IdentityModel . Tokens ;
2017using System ;
21- using System . Collections . Generic ;
2218using System . Configuration ;
2319using System . Globalization ;
2420using System . IdentityModel . Tokens . Jwt ;
2521using System . Net ;
26-
27- // The following using statements were added for this sample.
2822using System . Net . Http ;
2923using System . Net . Http . Headers ;
3024using System . Security . Claims ;
3529using System . Web . Mvc ;
3630using System . Web . Optimization ;
3731using System . Web . Routing ;
32+ using Microsoft . IdentityModel . Protocols ;
33+ using Microsoft . IdentityModel . Protocols . OpenIdConnect ;
34+ using Microsoft . IdentityModel . Tokens ;
3835
3936namespace TodoListService_ManualJwt
4037{
@@ -57,126 +54,94 @@ internal class TokenValidationHandler : DelegatingHandler
5754 // The AAD Instance is the instance of Azure, for example public Azure or Azure China.
5855 // The Tenant is the name of the tenant in which this application is registered.
5956 // The Authority is the sign-in URL of the tenant.
60- // The Audience is the value the service expects to see in tokens that are addressed to it.
57+ // 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.
6158 //
62- private static string aadInstance = ConfigurationManager . AppSettings [ "ida:AADInstance" ] ;
6359
64- private static string tenant = ConfigurationManager . AppSettings [ "ida:Tenant" ] ;
65- private static string audience = ConfigurationManager . AppSettings [ "ida:Audience" ] ;
66- private static string clientId = ConfigurationManager . AppSettings [ "ida:ClientId" ] ;
67- private string authority = String . Format ( CultureInfo . InvariantCulture , aadInstance , tenant ) ;
68-
69- private static string _issuer = string . Empty ;
70- private static ICollection < SecurityKey > _signingKeys = null ;
71- private static DateTime _stsMetadataRetrievalTime = DateTime . MinValue ;
72- private static string scopeClaimType = "http://schemas.microsoft.com/identity/claims/scope" ;
73-
74- //
75- // SendAsync checks that incoming requests have a valid access token, and sets the current user identity using that access token.
76- //
60+ private string _audience ;
61+ private string _authority ;
62+ private string _clientId ;
63+ private ConfigurationManager < OpenIdConnectConfiguration > _configManager ;
64+ private const string _scopeClaimType = "http://schemas.microsoft.com/identity/claims/scope" ;
65+ private ISecurityTokenValidator _tokenValidator ;
66+
67+ public TokenValidationHandler ( )
68+ {
69+ _audience = ConfigurationManager . AppSettings [ "ida:Audience" ] ;
70+ _clientId = ConfigurationManager . AppSettings [ "ida:ClientId" ] ;
71+ var aadInstance = ConfigurationManager . AppSettings [ "ida:AADInstance" ] ;
72+ var tenant = ConfigurationManager . AppSettings [ "ida:Tenant" ] ;
73+ _authority = string . Format ( CultureInfo . InvariantCulture , aadInstance , tenant ) ;
74+ _configManager = new ConfigurationManager < OpenIdConnectConfiguration > ( $ "{ _authority } /.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever ( ) ) ;
75+ _tokenValidator = new JwtSecurityTokenHandler ( ) ;
76+ }
77+
78+ /// <summary>
79+ /// Checks that incoming requests have a valid access token, and sets the current user identity using that access token.
80+ /// </summary>
81+ /// <param name="request">the current <see cref="HttpRequestMessage"/>.</param>
82+ /// <param name="cancellationToken">a <see cref="CancellationToken"/> set by application.</param>
83+ /// <returns>A <see cref="HttpResponseMessage"/>.</returns>
7784 protected async override Task < HttpResponseMessage > SendAsync ( HttpRequestMessage request , CancellationToken cancellationToken )
7885 {
79- // Get the jwt bearer token from the authorization header
80- string jwtToken = null ;
81- AuthenticationHeaderValue authHeader = request . Headers . Authorization ;
82- if ( authHeader != null )
83- {
84- jwtToken = authHeader . Parameter ;
85- }
86-
87- if ( jwtToken == null )
88- {
89- HttpResponseMessage response = this . BuildResponseErrorMessage ( HttpStatusCode . Unauthorized ) ;
90- return response ;
91- }
92-
93- string issuer ;
94- ICollection < SecurityKey > signingKeys ;
86+ // check there is a jwt in the authorization header, return 'Unauthorized' error if the token is null.
87+ if ( request . Headers . Authorization == null || request . Headers . Authorization . Parameter == null )
88+ return BuildResponseErrorMessage ( HttpStatusCode . Unauthorized ) ;
9589
90+ OpenIdConnectConfiguration config = null ;
9691 try
9792 {
98- // The issuer and signingKeys are cached for 24 hours. They are updated if any of the conditions in the if condition is true.
99- if ( DateTime . UtcNow . Subtract ( _stsMetadataRetrievalTime ) . TotalHours > 24
100- || string . IsNullOrEmpty ( _issuer )
101- || _signingKeys == null )
102- {
103- // Get tenant information that's used to validate incoming jwt tokens
104- string stsDiscoveryEndpoint = $ "{ this . authority } /.well-known/openid-configuration";
105- var configManager = new ConfigurationManager < OpenIdConnectConfiguration > ( stsDiscoveryEndpoint , new OpenIdConnectConfigurationRetriever ( ) ) ;
106- var config = await configManager . GetConfigurationAsync ( cancellationToken ) ;
107- _issuer = config . Issuer ;
108- _signingKeys = config . SigningKeys ;
109-
110- _stsMetadataRetrievalTime = DateTime . UtcNow ;
111- }
112-
113- issuer = _issuer ;
114- signingKeys = _signingKeys ;
93+ config = await _configManager . GetConfigurationAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
11594 }
11695 catch ( Exception )
11796 {
11897 return new HttpResponseMessage ( HttpStatusCode . InternalServerError ) ;
11998 }
12099
121- JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler ( ) ;
122-
123100 TokenValidationParameters validationParameters = new TokenValidationParameters
124101 {
125- // We accept both the App Id URI and the AppId of this service application
126- ValidAudiences = new [ ] { audience , clientId } ,
127-
128- // Supports both the Azure AD V1 and V2 endpoint
129- ValidIssuers = new [ ] { issuer , $ "{ issuer } /v2.0" } ,
130- IssuerSigningKeys = signingKeys
102+ // App Id URI and AppId of this service application are both valid audiences.
103+ ValidAudiences = new [ ] { _audience , _clientId } ,
104+ // Support Azure AD V1 and V2 endpoints.
105+ ValidIssuers = new [ ] { config . Issuer , $ "{ config . Issuer } /v2.0" } ,
106+ IssuerSigningKeys = config . SigningKeys
131107 } ;
132108
133109 try
134- {
110+ {
135111 // Validate token.
136- SecurityToken validatedToken = new JwtSecurityToken ( ) ;
137- ClaimsPrincipal claimsPrincipal = tokenHandler . ValidateToken ( jwtToken , validationParameters , out validatedToken ) ;
112+ var claimsPrincipal = _tokenValidator . ValidateToken ( request . Headers . Authorization . Parameter , validationParameters , out SecurityToken _ ) ;
138113
139114 // Set the ClaimsPrincipal on the current thread.
140115 Thread . CurrentPrincipal = claimsPrincipal ;
141116
142117 // Set the ClaimsPrincipal on HttpContext.Current if the app is running in web hosted environment.
143118 if ( HttpContext . Current != null )
144- {
145119 HttpContext . Current . User = claimsPrincipal ;
146- }
147120
148121 // If the token is scoped, verify that required permission is set in the scope claim.
149- if ( ClaimsPrincipal . Current . FindFirst ( scopeClaimType ) != null && ClaimsPrincipal . Current . FindFirst ( scopeClaimType ) . Value != "user_impersonation" )
150- {
151- HttpResponseMessage response = this . BuildResponseErrorMessage ( HttpStatusCode . Forbidden ) ;
152- return response ;
153- }
122+ if ( ClaimsPrincipal . Current . FindFirst ( _scopeClaimType ) != null && ClaimsPrincipal . Current . FindFirst ( _scopeClaimType ) . Value != "user_impersonation" )
123+ return BuildResponseErrorMessage ( HttpStatusCode . Forbidden ) ;
154124
155125 return await base . SendAsync ( request , cancellationToken ) ;
156126 }
157127 catch ( SecurityTokenValidationException )
158128 {
159- HttpResponseMessage response = this . BuildResponseErrorMessage ( HttpStatusCode . Unauthorized ) ;
160- return response ;
129+ return BuildResponseErrorMessage ( HttpStatusCode . Unauthorized ) ;
161130 }
162131 catch ( Exception )
163132 {
164133 return new HttpResponseMessage ( HttpStatusCode . InternalServerError ) ;
165134 }
166- }
135+ }
167136
168137 private HttpResponseMessage BuildResponseErrorMessage ( HttpStatusCode statusCode )
169138 {
170- HttpResponseMessage response = new HttpResponseMessage ( statusCode ) ;
139+ var response = new HttpResponseMessage ( statusCode ) ;
171140
172- //
173141 // The Scheme should be "Bearer", authorization_uri should point to the tenant url and resource_id should point to the audience.
174- //
175- AuthenticationHeaderValue authenticateHeader = new AuthenticationHeaderValue ( "Bearer" , "authorization_uri=\" " + this . authority + "\" " + "," + "resource_id=" + audience ) ;
176-
142+ var authenticateHeader = new AuthenticationHeaderValue ( "Bearer" , "authorization_uri=\" " + _authority + "\" " + "," + "resource_id=" + _audience ) ;
177143 response . Headers . WwwAuthenticate . Add ( authenticateHeader ) ;
178-
179144 return response ;
180145 }
181146 }
182- }
147+ }
0 commit comments