skip to Main Content

I am trying to configure WebApp (MSAL + Razor Pages) application to use two app registrations – one for frontend, another one for backend. The main reason for this is, that I am planning to migrate to SPA in a future and would like to have transition as smooth as possible (without requesting new consents/new app registrations). I have configured them in similar way as I do for SPA applications (two registrations, all required API permissions on backend, frontend registration configured in "knownClientApplications" of backend. System has frontend registration used to authenticate user with scope api://{backendClientId}/.default scope. Everything works well, consent is properly propagated to backend registration.

I am experiencing problems with OBO flow. I tried to configure downstream API to use client id of backend server, but call to graph API failed with insufficient privileges. Following is the configuration of the server side:

            services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApp(o =>
                {
                    o.Instance = "https://login.microsoftonline.com/";
                    o.Domain = "{MyDomain}";
                    o.TenantId = "common";
                    o.ClientId = "{FrontendClientId}";
                    o.ClientSecret = "{FrontendClientSecret}";
                    o.CallbackPath = "/signin-oidc";
                    o.Scope.Add("api://{BackendClientId}/.default");
                })
                .EnableTokenAcquisitionToCallDownstreamApi(o =>
                {
                    o.Instance = "https://login.microsoftonline.com/";
                    o.TenantId = "common";
                    o.ClientId = "{BackendClientId}";
                    o.ClientSecret = "{BackendClientSecret}";
                })
                .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
                .AddInMemoryTokenCaches();

What am I doing wrong? Is the setup desired by me even possible?

Thanks a lot for the help.

2

Answers


  1. Yes, it’s possible. And, from a security perspective is desirable.

    In an OBO flow you shouldn’t be using the backend ClientId or ClientSecret anywhere in your frontend. Your backend should be obtaining an OBO token of the frontend identity for use by the backend API.

    AddMicrosoftIdentityWebApp(o => ...) sets the credentials or token to authenticate to the backend API.

    EnableTokenAcquisitionToCallDownstreamApi sets the OBO token to be passed to, and used by, the backend.

    So, in this scenario, the backend will be using the frontend identity to call the Graph API. Therefore, it is the backend principal which needs the relevant delegated Graph permissions. The frontend is delegating its credential to the backend. The frontend needs the app permissions to Graph for the backend to be able to perform the necessary action. For example, if the frontend needs to read a user’s name:

    Frontend Permission: Type App, Permission User.ReadBasic.All

    Backend Permission: Type Delegated, Permission User.ReadBasic.All

    An example where you might further refine security is where all the users signed in to your frontend are Entra users, and instead of using the frontend token, you use the signed in user’s token. You do this with acquireTokenSilent, and then your frontend doesn’t need explicit Graph permissions unless it is doing something which the user isn’t permitted. But this is for another post.

    You can update your code as follows:

    services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApp(o =>
        {
            o.Instance = "https://login.microsoftonline.com/";
            o.Domain = "{MyDomain}";
            o.TenantId = "common";
            o.ClientId = "{FrontendClientId}";
            o.ClientSecret = "{FrontendClientSecret}";
            o.CallbackPath = "/signin-oidc";
            o.Scope.Add("api://{BackendClientId}/.default");
        })
        .EnableTokenAcquisitionToCallDownstreamApi(o =>
        {
            o.Instance = "https://login.microsoftonline.com/";
            o.TenantId = "common";
            o.ClientId = "{FrontendClientId}";
            o.ClientSecret = "{FrontendClientSecret}";
            o.Scopes = new[] { "api://{BackendClientId}/.default" }; // Scopes needed to call Azure Function API
        })
        .AddInMemoryTokenCaches();
        .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
    

    Here’s an example of token acquisition for your downstream API call:

    private readonly ITokenAcquisition _tokenAcquisition;
    private readonly HttpClient _httpClient;
    
    public YourFrontendController(ITokenAcquisition tokenAcquisition, IHttpClientFactory httpClientFactory)
    {
        _tokenAcquisition = tokenAcquisition;
        _httpClient = httpClientFactory.CreateClient();
    }
    
    public async Task<IActionResult> YourAction()
    {
        var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { "api://{BackendClientId}/.default" });
    
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    
        var response = await _httpClient.GetAsync("https://{YourAzureFunction}.azurewebsites.net/api/{FunctionName}");
    
        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            return Ok(content);
        }
    
        return BadRequest();
    }
    

    Here’s an example of validation of the front end calling credential, and OBO token acquisition in the downstream API:

    // Authenticate frontend
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = false,
                ValidateAudience = false,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecretKey"]))
            };
        });
    
    // Implement OBO flow
    private readonly ITokenAcquisition _tokenAcquisition;
    private readonly HttpClient _httpClient;
    
    public YourAzureFunction(ITokenAcquisition tokenAcquisition, IHttpClientFactory httpClientFactory)
    {
        _tokenAcquisition = tokenAcquisition;
        _httpClient = httpClientFactory.CreateClient();
    }
    
    // Example using the me API using the delegated token in OBO flow
    
    [FunctionName("YourFunctionName")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        string accessToken = await _tokenAcquisition.GetTokenOnBehalfOfUserAsync(req.Headers["Authorization"]);
    
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    
        var response = await _httpClient.GetAsync("https://graph.microsoft.com/v1.0/me");
    
        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            return new OkObjectResult(content);
        }
    
        return new BadRequestResult();
    }
    

    This downstream function API is a bit rushed, but you should get the idea.

    Login or Signup to reply.
  2. I tried to configure downstream API to use client id of backend
    server, but call to graph API failed with insufficient privileges.

    To resolve permission not enough issue, I’m afraid that we could set the client to the known client applications list in the backend application.

    Then when we sign in the client app and in the consent popup window, we require to consent for the backend app. Using openid api://backend_app_client_id/.default as the scope when generate the access token in the client app.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search