Skip to content

Adding suppport for Web APIs in the library #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Microsoft.Identity.Web/Client/ITokenAcquisition.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.Identity.Client;
using System.Collections.Generic;
using System.Threading.Tasks;

Expand Down Expand Up @@ -108,5 +109,15 @@ public interface ITokenAcquisition
/// Openidconnect event</param>
/// <returns></returns>
Task RemoveAccount(RedirectContext context);

/// <summary>
/// Used in Web APIs (which therefore cannot have an interaction with the user).
/// Replies to the client through the HttpReponse by sending a 403 (forbidden) and populating wwwAuthenticateHeaders so that
/// the client can trigger an iteraction with the user so that the user consents to more scopes
/// </summary>
/// <param name="httpContext">HttpContext</param>
/// <param name="scopes">Scopes to consent to</param>
/// <param name="msalSeviceException"><see cref="MsalUiRequiredException"/> triggering the challenge</param>
void ReplyForbiddenWithWwwAuthenticateHeader(HttpContext httpContext, IEnumerable<string> scopes, MsalUiRequiredException msalSeviceException);
}
}
57 changes: 57 additions & 0 deletions Microsoft.Identity.Web/Client/TokenAcquisition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Web.Client.TokenCacheProviders;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;

Expand Down Expand Up @@ -377,5 +380,59 @@ private void AddAccountToCacheFromJwt(IEnumerable<string> scopes, JwtSecurityTok
throw;
}
}


/// <summary>
/// Used in Web APIs (which therefore cannot have an interaction with the user).
/// Replies to the client through the HttpReponse by sending a 403 (forbidden) and populating wwwAuthenticateHeaders so that
/// the client can trigger an iteraction with the user so that the user consents to more scopes
/// </summary>
/// <param name="httpContext">HttpContext</param>
/// <param name="scopes">Scopes to consent to</param>
/// <param name="msalSeviceException"><see cref="MsalUiRequiredException"/> triggering the challenge</param>

public void ReplyForbiddenWithWwwAuthenticateHeader(HttpContext httpContext, IEnumerable<string> scopes, MsalUiRequiredException msalSeviceException)
{
// 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
string proposedAction = "consent";
if (msalSeviceException.ErrorCode == MsalUiRequiredException.InvalidGrantError)
{
if (AcceptedTokenVersionIsNotTheSameAsTokenVersion(msalSeviceException))
{
throw msalSeviceException;
}
}

IDictionary<string, string> parameters = new Dictionary<string, string>()
{
{ "clientId", azureAdOptions.ClientId },
{ "claims", msalSeviceException.Claims },
{ "scopes", string.Join(",", scopes) },
{ "proposedAction", proposedAction }
};

string parameterString = string.Join(", ", parameters.Select(p => $"{p.Key}=\"{p.Value}\""));
string scheme = "Bearer";
StringValues v = new StringValues($"{scheme} {parameterString}");

// StringValues v = new StringValues(new string[] { $"Bearer clientId=\"{jwtToken.Audiences.First()}\", claims=\"{ex.Claims}\", scopes=\" {string.Join(",", scopes)}\"" });
var httpResponse = httpContext.Response;
var headers = httpResponse.Headers;
httpResponse.StatusCode = (int)HttpStatusCode.Forbidden;
if (headers.ContainsKey(HeaderNames.WWWAuthenticate))
{
headers.Remove(HeaderNames.WWWAuthenticate);
}
headers.Add(HeaderNames.WWWAuthenticate, v);
}

private static bool AcceptedTokenVersionIsNotTheSameAsTokenVersion(MsalUiRequiredException msalSeviceException)
{
// Normally app developers should not make decisions based on the internal AAD code
// however until the STS sends sub-error codes for this error, this is the only
// way to distinguish the case.
// This is subject to change in the future
return (msalSeviceException.Message.Contains("AADSTS50013"));
}
}
}
2 changes: 1 addition & 1 deletion Microsoft.Identity.Web/Microsoft.Identity.Web.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
Expand Down
78 changes: 78 additions & 0 deletions Microsoft.Identity.Web/WebApiStartupHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Web.Client;
using Microsoft.Identity.Web.Resource;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Microsoft.Identity.Web
{
public static class WebApiStartupHelpers
{
/// <summary>
/// Protects the Web API with Microsoft Identity Platform v2.0 (AAD v2.0)
/// This supposes that the configuration files have a section named "AzureAD"
/// </summary>
/// <param name="services">Service collection to which to add authentication</param>
/// <param name="configuration">Configuration</param>
/// <returns></returns>
public static IServiceCollection AddProtectWebApiWithMicrosoftIdentityPlatformV2(this IServiceCollection services, IConfiguration configuration)
{
services.AddAuthentication(AzureADDefaults.JwtBearerAuthenticationScheme)
.AddAzureADBearer(options => configuration.Bind("AzureAd", options));

services.AddSession();

// Added
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
// This is an Azure AD v2.0 Web API
options.Authority += "/v2.0";

// The valid audiences are both the Client ID (options.Audience) and api://{ClientID}
options.TokenValidationParameters.ValidAudiences = new string[] { options.Audience, $"api://{options.Audience}" };

// Instead of using the default validation (validating against a single tenant, as we do in line of business apps),
// we inject our own multitenant validation logic (which even accepts both V1 and V2 tokens)
options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.ForAadInstance(options.Authority).ValidateAadIssuer;

// When an access token for our own Web API is validated, we add it to MSAL.NET's cache so that it can
// be used from the controllers.
options.Events = new JwtBearerEvents();

// If you want to debug, or just understand the JwtBearer events, uncomment the following line of code
// options.Events = JwtBearerMiddlewareDiagnostics.Subscribe(options.Events);
});

return services;
}

/// <summary>
/// Protects the Web API with Microsoft Identity Platform v2.0 (AAD v2.0)
/// This supposes that the configuration files have a section named "AzureAD"
/// </summary>
/// <param name="services">Service collection to which to add authentication</param>
/// <param name="configuration">Configuration</param>
/// <returns></returns>
public static IServiceCollection AddProtectedApiCallsWebApis(this IServiceCollection services, IConfiguration configuration, IEnumerable<string> scopes)
{
services.AddTokenAcquisition();
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.Events.OnTokenValidated = async context =>
{
var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
context.Success();

// Adds the token to the cache, and also handles the incremental consent and claim challenges
tokenAcquisition.AddAccountToCacheFromJwt(context, scopes);
await Task.FromResult(0);
};
});
return services;
}
}
}