Search code examples
reactjs.netazure-active-directory

Dotnet API requires auth both for application and React


I must be really stupid, But I have been struggling for weeks to try solve this issue, and all the digging I have done (in Stack overflow and MS Documentation) has yielded no results (or I'm too stupid to implement auth correctly)

I have a dotnet service which needs to act as an API - both for an application to post data to (an exe which logs exception data), and for a UI (react app) to get the posted exceptions

the exe can successfully send data to the dotnet app after first getting a token from login.microsoftonline.com and then sending the token (and secret) in the http request.

A sample postman pre-request script of the auth used (I've set all the secret stuff as environment variables):

pm.sendRequest({
    url: 'https://login.microsoftonline.com/' + pm.environment.get("tenantId") + '/oauth2/v2.0/token',
    method: 'POST',
    header: 'Content-Type: application/x-www-form-urlencoded',
    body: {
        mode: 'urlencoded',
        urlencoded: [ 
            {key: "grant_type", value: "client_credentials", disabled: false},
            {key: "client_id", value: pm.environment.get("clientId"), disabled: false},
            {key: "client_secret", value: pm.environment.get("clientSecret"), disabled: false}, //if I don't configure a secret, and omit this, the requests fail (Azure Integration Assistant recommends that you do not configure credentials/secrets, but does not provide clear documentation as to why, or how to use a daemon api without it)
            {key: "scope", value: pm.environment.get("scope"), disabled: false}
        ]
    }
}, function (err, res) {
   const token = 'Bearer ' + res.json().access_token;

    pm.request.headers.add(token, "Authorization");
});

Now in React, I am using MSAL(@azure/msal-browser) in order to login a user, get their token, and pass the token to one of the dotnet endpoints using axios as my http wrapper, but no matter what I do, it returns http status 401 with WWW-Authenticate: Bearer error="invalid_token", error_description="The signature is invalid".

A simplified code flow to login user and request data from the API:

import {publicClientApplication} from "../../components/Auth/Microsoft";//a preconfigured instance of PublicClientApplication from @azure/msal-browser

const data = await publicClientApplication.loginPopup();
// ... some data validation
publicClientApplication.setActiveAccount(data.account);
// .. some time and other processes may happen here so we don't access token directly from loginPopup()
const activeAccout = publicClientApplication.getActiveAccount();
const token = publicClientApplication.acquireTokenSilent(activeAccount).accessToken;

const endpointData = await api()/*an instance of Axios.create() with some pre-configuration*/.get(
    '/endpoint',
    { headers: {'Authorization': `bearer ${token}`} }); // returns status 401

The dotnet service has the following configurations

public void ConfigureServices(IServiceCollection services){
...
  var authScheme = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
  authScheme.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
...
}

namespace Controllers{
  public class EndpointController : ControllerBase{
    ...
    [Authorize]
    [HttpGet]
    public IActionResult GetEndpoint(){
      return Ok("you finally got through");
    }
  }
}

I've literally tried so many things that I've lost track of what I've done... I've even cried myself to sleep over this - but that yielded no results

i can confirm that running the request in postman, with the pre request script, it is possible to get the response from the endpoint


Solution

  • So.... After much digging and A-B Testing I was able to solve this issue.

    I discovered that I was not sending the API scope to the OAuth token endpoint. To do this I needed to change the input for acquireTokenSilent.

    The updated code flow to login user and request data from the API:

    import {publicClientApplication} from "../../components/Auth/Microsoft";//a preconfigured instance of PublicClientApplication from @azure/msal-browser
    
    const data = await publicClientApplication.loginPopup();
    // ... some data validation
    publicClientApplication.setActiveAccount(data.account);
    // .. some time and other processes may happen here so we don't access token directly from loginPopup()
    const activeAccout = publicClientApplication.getActiveAccount();
    const token = publicClientApplication.acquireTokenSilent({scopes:["api://XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/.default"],account:activeAccount}).accessToken;//here scopes is an array of strings, Where I used the api URI , but you could directly use a scope name like User.Read if you had it configured
    
    const endpointData = await api()/*an instance of Axios.create() with some pre-configuration*/.get(
        '/endpoint',
        { headers: {'Authorization': `bearer ${token}`} }); // returns status 401