Skip to content

Commit b87a1d8

Browse files
authored
Adding suppport for Web APIs in the library (#78)
1 parent c34fac6 commit b87a1d8

File tree

4 files changed

+147
-1
lines changed

4 files changed

+147
-1
lines changed

Microsoft.Identity.Web/Client/ITokenAcquisition.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
22
using Microsoft.AspNetCore.Http;
3+
using Microsoft.Identity.Client;
34
using System.Collections.Generic;
45
using System.Threading.Tasks;
56

@@ -108,5 +109,15 @@ public interface ITokenAcquisition
108109
/// Openidconnect event</param>
109110
/// <returns></returns>
110111
Task RemoveAccount(RedirectContext context);
112+
113+
/// <summary>
114+
/// Used in Web APIs (which therefore cannot have an interaction with the user).
115+
/// Replies to the client through the HttpReponse by sending a 403 (forbidden) and populating wwwAuthenticateHeaders so that
116+
/// the client can trigger an iteraction with the user so that the user consents to more scopes
117+
/// </summary>
118+
/// <param name="httpContext">HttpContext</param>
119+
/// <param name="scopes">Scopes to consent to</param>
120+
/// <param name="msalSeviceException"><see cref="MsalUiRequiredException"/> triggering the challenge</param>
121+
void ReplyForbiddenWithWwwAuthenticateHeader(HttpContext httpContext, IEnumerable<string> scopes, MsalUiRequiredException msalSeviceException);
111122
}
112123
}

Microsoft.Identity.Web/Client/TokenAcquisition.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2828
using Microsoft.AspNetCore.Http.Extensions;
2929
using Microsoft.Extensions.Configuration;
3030
using Microsoft.Extensions.DependencyInjection;
31+
using Microsoft.Extensions.Primitives;
3132
using Microsoft.Identity.Client;
3233
using Microsoft.Identity.Client.AppConfig;
3334
using Microsoft.Identity.Web.Client.TokenCacheProviders;
35+
using Microsoft.Net.Http.Headers;
3436
using System;
3537
using System.Collections.Generic;
3638
using System.Diagnostics;
3739
using System.IdentityModel.Tokens.Jwt;
3840
using System.Linq;
41+
using System.Net;
3942
using System.Security.Claims;
4043
using System.Threading.Tasks;
4144

@@ -377,5 +380,59 @@ private void AddAccountToCacheFromJwt(IEnumerable<string> scopes, JwtSecurityTok
377380
throw;
378381
}
379382
}
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+
}
380437
}
381438
}

Microsoft.Identity.Web/Microsoft.Identity.Web.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netcoreapp2.2</TargetFramework>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using Microsoft.AspNetCore.Authentication;
2+
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
3+
using Microsoft.AspNetCore.Authentication.JwtBearer;
4+
using Microsoft.Extensions.Configuration;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Identity.Web.Client;
7+
using Microsoft.Identity.Web.Resource;
8+
using System.Collections.Generic;
9+
using System.Threading.Tasks;
10+
11+
namespace Microsoft.Identity.Web
12+
{
13+
public static class WebApiStartupHelpers
14+
{
15+
/// <summary>
16+
/// Protects the Web API with Microsoft Identity Platform v2.0 (AAD v2.0)
17+
/// This supposes that the configuration files have a section named "AzureAD"
18+
/// </summary>
19+
/// <param name="services">Service collection to which to add authentication</param>
20+
/// <param name="configuration">Configuration</param>
21+
/// <returns></returns>
22+
public static IServiceCollection AddProtectWebApiWithMicrosoftIdentityPlatformV2(this IServiceCollection services, IConfiguration configuration)
23+
{
24+
services.AddAuthentication(AzureADDefaults.JwtBearerAuthenticationScheme)
25+
.AddAzureADBearer(options => configuration.Bind("AzureAd", options));
26+
27+
services.AddSession();
28+
29+
// Added
30+
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
31+
{
32+
// This is an Azure AD v2.0 Web API
33+
options.Authority += "/v2.0";
34+
35+
// The valid audiences are both the Client ID (options.Audience) and api://{ClientID}
36+
options.TokenValidationParameters.ValidAudiences = new string[] { options.Audience, $"api://{options.Audience}" };
37+
38+
// Instead of using the default validation (validating against a single tenant, as we do in line of business apps),
39+
// we inject our own multitenant validation logic (which even accepts both V1 and V2 tokens)
40+
options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.ForAadInstance(options.Authority).ValidateAadIssuer;
41+
42+
// When an access token for our own Web API is validated, we add it to MSAL.NET's cache so that it can
43+
// be used from the controllers.
44+
options.Events = new JwtBearerEvents();
45+
46+
// If you want to debug, or just understand the JwtBearer events, uncomment the following line of code
47+
// options.Events = JwtBearerMiddlewareDiagnostics.Subscribe(options.Events);
48+
});
49+
50+
return services;
51+
}
52+
53+
/// <summary>
54+
/// Protects the Web API with Microsoft Identity Platform v2.0 (AAD v2.0)
55+
/// This supposes that the configuration files have a section named "AzureAD"
56+
/// </summary>
57+
/// <param name="services">Service collection to which to add authentication</param>
58+
/// <param name="configuration">Configuration</param>
59+
/// <returns></returns>
60+
public static IServiceCollection AddProtectedApiCallsWebApis(this IServiceCollection services, IConfiguration configuration, IEnumerable<string> scopes)
61+
{
62+
services.AddTokenAcquisition();
63+
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
64+
{
65+
options.Events.OnTokenValidated = async context =>
66+
{
67+
var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
68+
context.Success();
69+
70+
// Adds the token to the cache, and also handles the incremental consent and claim challenges
71+
tokenAcquisition.AddAccountToCacheFromJwt(context, scopes);
72+
await Task.FromResult(0);
73+
};
74+
});
75+
return services;
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)