@@ -28,14 +28,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
28
using Microsoft . AspNetCore . Http . Extensions ;
29
29
using Microsoft . Extensions . Configuration ;
30
30
using Microsoft . Extensions . DependencyInjection ;
31
+ using Microsoft . Extensions . Primitives ;
31
32
using Microsoft . Identity . Client ;
32
33
using Microsoft . Identity . Client . AppConfig ;
33
34
using Microsoft . Identity . Web . Client . TokenCacheProviders ;
35
+ using Microsoft . Net . Http . Headers ;
34
36
using System ;
35
37
using System . Collections . Generic ;
36
38
using System . Diagnostics ;
37
39
using System . IdentityModel . Tokens . Jwt ;
38
40
using System . Linq ;
41
+ using System . Net ;
39
42
using System . Security . Claims ;
40
43
using System . Threading . Tasks ;
41
44
@@ -377,5 +380,59 @@ private void AddAccountToCacheFromJwt(IEnumerable<string> scopes, JwtSecurityTok
377
380
throw ;
378
381
}
379
382
}
383
+
384
+
385
+ /// <summary>
386
+ /// Used in Web APIs (which therefore cannot have an interaction with the user).
387
+ /// Replies to the client through the HttpReponse by sending a 403 (forbidden) and populating wwwAuthenticateHeaders so that
388
+ /// the client can trigger an iteraction with the user so that the user consents to more scopes
389
+ /// </summary>
390
+ /// <param name="httpContext">HttpContext</param>
391
+ /// <param name="scopes">Scopes to consent to</param>
392
+ /// <param name="msalSeviceException"><see cref="MsalUiRequiredException"/> triggering the challenge</param>
393
+
394
+ public void ReplyForbiddenWithWwwAuthenticateHeader ( HttpContext httpContext , IEnumerable < string > scopes , MsalUiRequiredException msalSeviceException )
395
+ {
396
+ // A user interaction is required, but we are in a Web API, and therefore, we need to report back to the client through an wwww-Authenticate header https://tools.ietf.org/html/rfc6750#section-3.1
397
+ string proposedAction = "consent" ;
398
+ if ( msalSeviceException . ErrorCode == MsalUiRequiredException . InvalidGrantError )
399
+ {
400
+ if ( AcceptedTokenVersionIsNotTheSameAsTokenVersion ( msalSeviceException ) )
401
+ {
402
+ throw msalSeviceException ;
403
+ }
404
+ }
405
+
406
+ IDictionary < string , string > parameters = new Dictionary < string , string > ( )
407
+ {
408
+ { "clientId" , azureAdOptions . ClientId } ,
409
+ { "claims" , msalSeviceException . Claims } ,
410
+ { "scopes" , string . Join ( "," , scopes ) } ,
411
+ { "proposedAction" , proposedAction }
412
+ } ;
413
+
414
+ string parameterString = string . Join ( ", " , parameters . Select ( p => $ "{ p . Key } =\" { p . Value } \" ") ) ;
415
+ string scheme = "Bearer" ;
416
+ StringValues v = new StringValues ( $ "{ scheme } { parameterString } ") ;
417
+
418
+ // StringValues v = new StringValues(new string[] { $"Bearer clientId=\"{jwtToken.Audiences.First()}\", claims=\"{ex.Claims}\", scopes=\" {string.Join(",", scopes)}\"" });
419
+ var httpResponse = httpContext . Response ;
420
+ var headers = httpResponse . Headers ;
421
+ httpResponse . StatusCode = ( int ) HttpStatusCode . Forbidden ;
422
+ if ( headers . ContainsKey ( HeaderNames . WWWAuthenticate ) )
423
+ {
424
+ headers . Remove ( HeaderNames . WWWAuthenticate ) ;
425
+ }
426
+ headers . Add ( HeaderNames . WWWAuthenticate , v ) ;
427
+ }
428
+
429
+ private static bool AcceptedTokenVersionIsNotTheSameAsTokenVersion ( MsalUiRequiredException msalSeviceException )
430
+ {
431
+ // Normally app developers should not make decisions based on the internal AAD code
432
+ // however until the STS sends sub-error codes for this error, this is the only
433
+ // way to distinguish the case.
434
+ // This is subject to change in the future
435
+ return ( msalSeviceException . Message . Contains ( "AADSTS50013" ) ) ;
436
+ }
380
437
}
381
438
}
0 commit comments