Skip to content

Commit

Permalink
Adding the capability of calling a Web API to the webapi2 template (#226
Browse files Browse the repository at this point in the history
)

* Updating the outstanding project templates

* Fixing template.json

* Update

* Update

* Fix indentation

* Fixing the templates

* Fix the mvc2 and webapp2 templates which were generating the "call an API" even without the flags to enable it (--called-api-url  and --called-api-scopes)

Fixing the AuthorizeForScopes attribute in the Razor pages (which was added even when the webapp2 was not calling an API

Fixing the identity of the Web API 2.

Updating the test-templates.bat file to:
- add test for webapp2 and mvc2 calling Web APIs
- replace msbuild by dotnet build

* Adding "calls a web api" to the Web API2 template
  • Loading branch information
jmprieur authored Jun 19, 2020
1 parent 57184a3 commit 17f48c6
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@
"NoHttps": {
"longName": "no-https",
"shortName": ""
},
"CalledApiUrl": {
"longName": "called-api-url",
"shortName": ""
},
"CalledApiScopes": {
"longName": "called-api-scopes",
"shortName": ""
}
},
"usageExamples": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
"type": "parameter",
"datatype": "bool",
"defaultValue": "false",
"description": "Whether to exclude launchSettings.json in the generated template."
"description": "Whether to exclude launchSettings.json from the generated template."
},
"HttpPort": {
"type": "parameter",
Expand Down Expand Up @@ -211,6 +211,10 @@
{
"choice": "netcoreapp5.0",
"description": "Target netcoreapp5.0"
},
{
"choice": "netcoreapp3.1",
"description": "Target netcoreapp3.1"
}
],
"replaces": "netcoreapp5.0",
Expand All @@ -229,6 +233,22 @@
"datatype": "bool",
"description": "If specified, skips the automatic restore of the project on create.",
"defaultValue": "false"
},
"CalledApiUrl": {
"type": "parameter",
"datatype": "string",
"replaces": "https://graph.microsoft.com/v1.0/me",
"description": "URL of the API to call from the web app."
},
"CalledApiScopes": {
"type": "parameter",
"datatype": "string",
"replaces" : "user.read",
"description": "Scopes to request to call the API from the web app."
},
"GenerateApi": {
"type": "computed",
"value": "(CalledApiUrl != \"https://graph.microsoft.com/v1.0/me\" && CalledApiScopes != \"user.read\")"
}
},
"primaryOutputs": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@
#if (!NoAuth)
using Microsoft.AspNetCore.Authorization;
#endif
#if (GenerateApi)
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Web;
using System.Net;
using System.Net.Http;
#endif
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web.Resource;
using Microsoft.Extensions.Logging;

namespace Company.WebApplication1.Controllers
{
#if (GenerateApi)
using Services;

#endif
#if (!NoAuth)
[Authorize]
#endif
Expand All @@ -28,6 +38,36 @@ public class WeatherForecastController : ControllerBase
// The Web API will only accept tokens 1) for users, and 2) having the access_as_user scope for this API
static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };

#if (GenerateApi)
private readonly IDownstreamWebApi _downstreamWebApi;

public WeatherForecastController(ILogger<WeatherForecastController> logger,
IDownstreamWebApi downstreamWebApi)
{
_logger = logger;
_downstreamWebApi = downstreamWebApi;
}

[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get()
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

string downstreamApiResult = await _downstreamWebApi.CallWebApi();
// You can also specify the relative endpoint and the scopes
// downstreamApiResult = await _downstreamWebApi.CallWebApi("me", new string[] {"user.read"});

var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}

#else
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
Expand All @@ -47,5 +87,6 @@ public IEnumerable<WeatherForecast> Get()
})
.ToArray();
}
#endif
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Web;

namespace Company.WebApplication1.Services
{
public interface IDownstreamWebApi
{
Task<string> CallWebApi(string relativeEndpoint = "", string[] requiredScopes = null);
}

public static class DownstreamWebApiExtensions
{
public static void AddDownstreamWebApiService(this IServiceCollection services, IConfiguration configuration)
{
// https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
services.AddHttpClient<IDownstreamWebApi, DownstreamWebApi>();
}
}

public class DownstreamWebApi : IDownstreamWebApi
{
private readonly ITokenAcquisition _tokenAcquisition;

private readonly IConfiguration _configuration;

private readonly HttpClient _httpClient;

public DownstreamWebApi(
ITokenAcquisition tokenAcquisition,
IConfiguration configuration,
HttpClient httpClient)
{
_tokenAcquisition = tokenAcquisition;
_configuration = configuration;
_httpClient = httpClient;
}

/// <summary>
/// Calls the Web API with the required scopes
/// </summary>
/// <param name="requireScopes">[Optional] Scopes required to call the Web API. If
/// not specified, uses scopes from the configuration</param>
/// <param name="relativeEndpoint">Endpoint relative to the CalledApiUrl configuration</param>
/// <returns>A Json string representing the result of calling the Web API</returns>
public async Task<string> CallWebApi(string relativeEndpoint = "", string[] requiredScopes = null)
{
string[] scopes = requiredScopes ?? _configuration["CalledApi:CalledApiScopes"]?.Split(' ');
string apiUrl = (_configuration["CalledApi:CalledApiUrl"] as string)?.TrimEnd('/') + $"/{relativeEndpoint}";

string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
_httpClient.DefaultRequestHeaders.Add("Authorization", $"bearer {accessToken}");

string apiResult;
var response = await _httpClient.GetAsync($"{apiUrl}");
if (response.StatusCode == HttpStatusCode.OK)
{
apiResult = await response.Content.ReadAsStringAsync();
}
else
{
apiResult = $"Error calling the API '{apiUrl}'";
}

return apiResult;
}
}
}
24 changes: 18 additions & 6 deletions ProjectTemplates/templates/WebApi-CSharp/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@

namespace Company.WebApplication1
{
#if (GenerateApi)
using Services;

#endif
public class Startup
{
public Startup(IConfiguration configuration)
Expand All @@ -43,15 +47,23 @@ public void ConfigureServices(IServiceCollection services)
// Adds Microsoft Identity platform (AAD v2.0) support to protect this Api
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddProtectedWebApi(Configuration, "AzureAd");
// Uncomment the following lines if you want your Web API to call a downstream API
// services.AddProtectedWebApiCallsProtectedWebApi(Configuration, "AzureAd")
// .AddInMemoryTokenCaches();
#if (GenerateApi)
services.AddWebAppCallsProtectedWebApi(Configuration,
"AzureAd")
.AddInMemoryTokenCaches();

services.AddDownstreamWebApiService(Configuration);
#endif
#elif (IndividualB2CAuth)
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddProtectedWebApi(Configuration, "AzureAdB2C");
// Uncomment the following lines if you want your Web API to call a downstream API
// services.AddProtectedWebApiCallsProtectedWebApi(Configuration, "AzureAdB2C")
// .AddInMemoryTokenCaches();
#if (GenerateApi)
services.AddWebAppCallsProtectedWebApi(Configuration,
"AzureAdB2C")
.AddInMemoryTokenCaches();

services.AddDownstreamWebApiService(Configuration);
#endif
#endif

services.AddControllers();
Expand Down
22 changes: 21 additions & 1 deletion ProjectTemplates/templates/WebApi-CSharp/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,27 @@
// "Domain": "qualified.domain.name",
// "TenantId": "22222222-2222-2222-2222-222222222222",
//#endif
// "ClientId": "11111111-1111-1111-11111111111111111"
// "ClientId": "11111111-1111-1111-11111111111111111",
////#if (GenerateApi)
// "ClientSecret": "secret-from-app-registration",
// "ClientCertificates" : [
// ]
////#endif
// "CallbackPath": "/signin-oidc"
// },
////#if (GenerateApi)
// "CalledApi": {
// /*
// 'CalledApiScope' is the scope of the Web API you want to call. This can be:
// - a scope for a V2 application (for instance api://b3682cc7-8b30-4bd2-aaba-080c6bf0fd31/access_as_user)
// - a scope corresponding to a V1 application (for instance <App ID URI>/.default, where <App ID URI> is the
// App ID URI of a legacy v1 Web application
// Applications are registered in the https://portal.azure.com portal.
// */
// "CalledApiScopes": "user.read",
// "CalledApiUrl": "https://graph.microsoft.com/v1.0"
// },
////#endif
// },
//#endif
"Logging": {
Expand Down
15 changes: 15 additions & 0 deletions ProjectTemplates/test-templates.bat
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,21 @@ dotnet new webapi2 --auth IndividualB2C
dotnet build
cd ..

echo " Test Web API calling Web API (Microsoft identity platform, SingleOrg)"
mkdir webapi-api
cd webapi-api
dotnet new webapi2 --auth SingleOrg --called-api-url "https://graph.microsoft.com/beta" --called-api-scopes "user.read"
dotnet build
cd ..

echo " Test Web API calling Web API (AzureAD B2C)"
mkdir webapi-b2c-api
cd webapi-b2c-api
dotnet new webapi2 --auth IndividualB2C --called-api-url "https://localhost:44332" --called-api-scopes "https://fabrikamb2c.onmicrosoft.com/tasks/read"
dotnet build
cd ..


echo "Uninstall templates"
cd ..
dotnet new -u Microsoft.Identity.Web.ProjectTemplates
Expand Down

0 comments on commit 17f48c6

Please sign in to comment.