Skip to content
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

[Feature Request] Downstream Web APi services from the templates to the library #403

Closed
jmprieur opened this issue Aug 5, 2020 · 3 comments
Assignees
Labels
Milestone

Comments

@jmprieur
Copy link
Collaborator

jmprieur commented Aug 5, 2020

Issue

Today the template introduce files to easy calling MIcrosoft Graph that would benefit from being productized in the library

Spec

See: #431

We'd want to be able to set which Microsoft graph endpoint to use (v1.0, Beta, but also national clouds) from the configuration, or programmatically. For instance in the appsettings.json

{
    "AzureAd": {
        "Instance": "https://login.microsoftonline.com/",
        "Domain": "qualified.domain.name",
        "TenantId": "22222222-2222-2222-2222-222222222222",
        "ClientId": "11111111-1111-1111-11111111111111111",
        "ClientSecret": "[Copy the client secret added to the app from the Azure portal]",
        "CallbackPath": "/signin-oidc"
    },

    "GraphBeta": {
        "CalledApiUrl": "https://graph.microsoft.com/beta",
        "CalledApiScopes": "user.read"
    },
    "Api2": {
        "CalledApiUrl": "https://myapi.mydomain.com",
        "CalledApiScopes": "api://someGuid/access_as_user"
    },

}

And be able to reference them in a CallsDownstreamWebApi method that would be called under CallsWebApi() or under AddMicrosoftIdentityWebApp or AddMicrosoftIdentityWebApi (to be decided)

  services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
               .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
                 .CallsWebApi()
                   .CallsDownstreamWebApi(Configuration.GetSection("Api1"))
                   .CallsDownstreamWebApi(Configuration.GetSection("Api2"))
                   .AddInMemoryTokenCaches(); 

We'll also provide an override that offers a delegate:

  services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
              .AddMicrosoftIdentityPlatformWebApp(Configuration.GetSection("AzureAd"))
                .CallsWebApi(initialScopes:new string[] {"user.read" })
                .CallsDownstreamWebApi("Api1", 
                                                   options => 
                   { 
                       options.CalledApiBaseUrl = "https://myapi.mydomain.com";
                       options.CalledApiScopes= "api://someGuid/access_as_user"
                   })
                .AddInMemoryTokenCaches(); 

In the controller or a Razor page, or a Blazor page, the GraphServiceClient will be injected by dependency injection and simply used.

using Microsoft.Identity.Web;

namespace WebAppCallsMicrosoftGraph.Pages
{
    [AuthorizeForScopes(Scopes = new[] { "user.read" })]
    public class IndexModel : PageModel
    {
        private readonly IDownstreamWebApi _downstreamWebApi;

        public IndexModel(IDownstreamWebApi downstreamWebApi)
        {
            _downstreamWebApi= downstreamWebApi;
        }

        public async Task OnGet()
        {
         
            try
            {
                var response = await _downstreamWebApi.CallWebApiForUserAsync("GraphBeta", "me");
                ViewData["name"] = await response.GetStringAsync();

                var response = await _downstreamWebApi.CallWebApiForUserAsync("Api2");
            }
            catch (Exception)
            {
                ViewData["name"] = null;
            }
        }
    }
}
@jmprieur jmprieur added enhancement New feature or request P1 labels Aug 5, 2020
@jmprieur jmprieur changed the title [Feature Request] Add GraphService and Web APi part of the library [Feature Request] Add GraphService and Downstream Web APi services from the templates to the library Aug 5, 2020
@jennyf19 jennyf19 added this to the 0.3.0-preview milestone Aug 7, 2020
@jmprieur
Copy link
Collaborator Author

jmprieur commented Aug 9, 2020

Proposed design:

Add a DownstreamApiSupport folder

Add an option class to use in the services to configure the APIs to be called

using System.Net.Http;

namespace Microsoft.Identity.Web
{
    /// <summary>
    /// Options passed-in to call web APIs (Microsoft Graph or another API).
    /// </summary>
    public class CalledApiOptions
    {
        /// <summary>
        /// Base URL for the called Web API. For instance <c>"https://graph.microsoft.com/v1.0/".</c>.
        /// </summary>
        public string CalledApiBaseUrl { get; set; } = "https://graph.microsoft.com/v1.0/";

        /// <summary>
        /// Space separated scopes used to call the Web API.
        /// For instance "user.read mail.read".
        /// </summary>
        public string? CalledApiScopes { get; set; } = null;

        /// <summary>
        /// Http method used to call this API (by default Get).
        /// </summary>
        public HttpMethod HttpMethod { get; set; } = HttpMethod.Get;
    }
}

Add an IDownstreamWebApi interface.

using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Microsoft.Identity.Web
{
    /// <summary>
    /// Interface used to call a downstream web API, for instance from controllers.
    /// </summary>
    public interface IDownstreamWebApi
    {
        /// <summary>
        /// Calls the Web API for the user, based on a description of the
        /// web API in the configuration.
        /// </summary>
        /// <param name="optionsInstanceName">Name of the options instance describing the API. There can
        /// be several configuration namedsections mapped to a <see cref="CalledApiOptions"/>,
        /// each for one API.</param>
        /// <param name="relativeEndpoint">[Optional] relative path with respect
        /// to <see cref="CalledApiOptions.CalledApiOptions"/>. It can be:
        /// <list type="bullet">
        /// <item>- empty if you want to use the enpoint URL proposed in the configuration</item>
        /// <item>- a relative path to the <see cref="CalledApiOptions.CalledApiBaseUrl"/> configuration.</item>
        /// </list>
        /// </param>
        /// <param name="requiredScopes">[Optional] Scopes required to call the Web API. If
        /// not specified, uses <see cref="CalledApiOptions.CalledApiScopes"/> from the configuration 
        /// If provided overrides them.</param>
        /// <param name="tenantId">[Optional] tenant ID. This is used for specific scenarios where
        /// the application needs to call a Web API on behalf of a user in several tenants.</param>
        /// <param name="userFlow">[Optional]. User flow (in the case of a B2C Web API). If not
        /// specified, the B2C web API will be called with the default user flow from the configuration.</param>
        /// <param name="user">[Optional] claims representing a user..</param>
        /// <returns>An <see cref="HttpResponseMessage"/> that the application will process.</returns>
        public Task<HttpResponseMessage> CallWebApiForUserAsync(
            string optionsInstanceName,
            string relativeEndpoint = "",
            string[]? requiredScopes = null,
            string? tenantId = null,
            string? userFlow = null,
            ClaimsPrincipal? user = null);

        /// <summary>
        /// Calls the Web API for the user, specifying the url endpoint and required scopes.
        /// </summary>
        /// <param name="endpointUrl">Abolute endpoint URL of the web API.</param>
        /// <param name="requiredScopes">Scopes required to call the Web API.</param>
        /// <param name="tenantId">[Optional] tenant ID. This is used for specific scenarios where
        /// the application needs to call a Web API on behalf of a user in several tenants.</param>
        /// <param name="userFlow">[Optional]. User flow (in the case of a B2C Web API). If not
        /// specified, the B2C web API will be called with the default user flow from the configuration.</param>
        /// <param name="user">[Optional] claims representing a user..</param>
        /// <returns>An <see cref="HttpResponseMessage"/> that the application will process.</returns>
        public Task<HttpResponseMessage> CallWebApiForUserAsync(
            string endpointUrl,
            string[] requiredScopes,
            string? tenantId = null,
            string? userFlow = null,
            ClaimsPrincipal? user = null);

        /// <summary>
        /// Calls the Web API for the app, with the required scopes.
        /// </summary>
        /// <param name="optionsInstanceName">Name of the options instance / the API.</param>
        /// <param name="endpointUrl">Endpoint relative to the CalledApiUrl configuration. Can be empty.</param>
        /// /// <param name="requiredScope">[Optional] Scopes required to call the Web API. If
        /// not specified, uses scopes from the configuration.</param>
        /// <param name="tenantId">Optional tenant ID.</param>
        /// <returns>An <see cref="HttpResponseMessage"/> that the application will process.</returns>
        public Task<HttpResponseMessage> CallWebApiForAppAsync(
            string optionsInstanceName,
            string endpointUrl = "",
            string? requiredScope = null,
            string? tenantId = null);

        /// <summary>
        /// Calls the Web API for the user, based on a description of the
        /// web API in the configuration.
        /// </summary>
        /// <param name="endpointUrl">Absolute endpoint URL.</param>
        /// /// <param name="requiredScope">Scopes required to call the Web API.</param>
        /// <param name="tenantId">Optional tenant ID.</param>
        /// <returns>An <see cref="HttpResponseMessage"/> that the application will process.</returns>
        public Task<HttpResponseMessage> CallWebApiForAppAsync(
            string endpointUrl,
            string? requiredScope = null,
            string? tenantId = null);
    }
}

This interface will be injected by dependency injection in controllers. Or, in the case where the developer wants to call several APIs, some interface derived from this one would be injected

@jmprieur jmprieur changed the title [Feature Request] Add GraphService and Downstream Web APi services from the templates to the library [Feature Request] Downstream Web APi services from the templates to the library Aug 10, 2020
@jmprieur jmprieur self-assigned this Aug 10, 2020
@jmprieur
Copy link
Collaborator Author

assigning to me as I'm spec-ing it

jmprieur added a commit that referenced this issue Aug 13, 2020
…rvice (#447)

*ust for #403
I've taken into account the PR review comments of the bigger branch 

Proposal for Downstream web API support
--------------------------------------------
- **Downstream API support** folder contains options, service, extensions
- Downstream API options
- Updated the Blazor sample to use both the Graph service and the Downstream API service (BlazorServer calls graph)

**Todo list sample**
- changed the namespace of the model (common to client and service). Ideally we'd put in a shared project. We might do that later.
- Updated the `TodoListService` class to leverage the `DownstreamWebApi` service. The code is way simpler!!!
- updated the `AppSettings.json` and the `Startup.cs` to leverage these services

Co-authored-by: Jean-Marc Prieur <jmprieur@microsoft.com>
Co-authored-by: pmaytak <34331512+pmaytak@users.noreply.github.com>
Co-authored-by: Jenny Ferries <jeferrie@microsoft.com>
@jennyf19
Copy link
Collaborator

Included in 0.3.0-preview release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants